随着 C++ 标准的不断演进,枚举类型(enum class)作为一种重要的数据结构,在 C++ 社区中扮演着越来越重要的角色。从 C++17 到 C++23,我们见证了枚举类型的多项改进和完善,这些变化不仅增强了语言本身的表达能力,也为开发者提供了更强大、更安全的编程工具。

原文链接:https://www.cppstories.com/2024/enum-improvements/

声明:未经允许,禁止转载。

作者 | Bartłomiej Filipek

翻译 | 郑丽媛

C++语言的演进不断带来强大的新特性,提升了代码的安全性、可读性和可维护性。在这些改进中,我们见证了从 C++17、C++20 到 C++23 中对 enum class 功能的修改和扩展。在这篇文章中,我们将探讨这些进展,重点介绍 C++17 中的初始化改进、C++20 中引入的 using enum 关键字,以及 C++23 中的 std::to_underlying 实用工具。

11c1671f5bef8a50926bc1ab38dbbc69.jpeg

一、enum class 简介

在深入了解这些改进之前,让我们先简要回顾一下 enum class 是什么。enum class(限定作用域的枚举)提供了一种类型安全的方式来定义一组命名常量。与传统的(无作用域)枚举不同,enum class 不会隐式转换为整数或其他类型,从而防止了意外的误用。下面是一个基本示例:

#include <iostream>


enum class Color {
    Red,
    Green,
    Blue
};


int main() {    
    Color color = Color::Red;


    if (color == Color::Red)
        std::cout << "The color is red.\n";


    color = Color::Blue;


    if (color == Color::Blue)
        std::cout << "The color is blue.\n";


    // std::cout << color; // error, no matching << operator
    // int i = color;      // error: cannot convert
}

注意,在 main 函数的末尾有两行代码。由于没有隐式转换为整数类型,因此会出现编译器错误。

作为对比,下面是一个类似的例子,但使用的是无作用域枚举:

#include <iostream>


enum Color {
    Red,
    Green,
    Blue
};


int main() {    
    Color color = Red;


    if (color == Red)
        std::cout << "The color is red.\n";


    color = Blue;


    if (color == Blue)
        std::cout << "The color is blue.\n";


    std::cout << color; // fine, prints integer value!
    int i = color;      // fine, can convert...
}

简而言之,enum class 为所有枚举值提供了单独的作用域,同时也加强了类型安全。没有隐式的整数转换,这样你就能更好地控制设计。

以上的基础部分很简单,接下来让我们来看看最新 C++ 版本中的一些实用改进。

二、C++17:使用大括号初始化基础类型

有时候,enum class 可能显得过于限制,某些情况下的转换可能会很方便。

在 C++17 中,P0138 提案被接受,以下是其中的一个示例:

enum class Handle : uint32_t { Invalid = 0 }; 
Handle h { 42 }; // OK

简而言之,当你使用 enum class 来定义强类型时,允许从基础类型进行初始化而不产生任何错误——这在 C++17 之前是无法实现的。

这个变化仍能确保枚举仍然是安全的,因为它们只能用于统一/大括号初始化。请看看下面的代码:

#include <iostream>


enum class Handle : uint32_t { Invalid = 0 }; 


void process(Handle h) {


}


int main() {    
    Handle h { 42 }; // OK


    // process({10}); // error
    process(Handle{10});
}

你不能直接将 {10} 作为 process 函数的参数,仍需要明确指定类型。

在 C++14 中,你可以使用 process(static_cast<Handle>(10));——如你所见,C++17 版本的改进要好得多。

三、C++20:使用 using enum

C++20 引入了 using enum 语法,这个特性允许你将一个枚举的所有枚举值引入当前作用域,同时不失去作用域枚举的优点。请看下面的示例:

enum class ComputeStatus {
    Ok,
    Error,
    FileError,
    NotEnoughMemory,
    TimeExceeded,
    Unknown
};

在早期的 C++ 版本中,使用这些枚举值时,需要使用枚举类的名称进行限定:

ComputeStatus s = ComputeStatus::NotEnoughMemory;

C++20 通过 using enum 声明简化了这一点:

int main() {
    using enum ComputeStatus;
    ComputeStatus s = NotEnoughMemory;
}

上面的简单代码可能没什么实际意义,但看看下面这个例子:

int main() {    
    ComputeStatus s = ComputeStatus::Ok;
    switch (s) {
        case ComputeStatus::Ok: 
            std::cout << "ok"; break;
        case ComputeStatus::Error: 
            std::cout << "Error"; break;
        case ComputeStatus::FileError: 
            std::cout << "FileError"; break;
        case ComputeStatus::NotEnoughMemory: 
            std::cout << "NotEnoughMemory"; break;
        case ComputeStatus::TimeExceeded: 
            std::cout << "Time..."; break;
        default: std::cout << "unknown...";
    }
}

我们可以将其转换为如下形式:

int main() {    
    ComputeStatus s = ComputeStatus::Ok;
    switch (s) {
        using enum ComputeStatus;  // << <<
        case Ok: 
            std::cout << "ok"; break;
        case Error: 
            std::cout << "Error"; break;
        case FileError: 
            std::cout << "FileError"; break;
        case NotEnoughMemory: 
            std::cout << "NotEnoughMemory"; break;
        case TimeExceeded: 
            std::cout << "Time..."; break;
        default: std::cout << "unknown...";
    }
}

或者,也可以看看下面这个例子:

struct ComputeEngine {
    enum class ComputeStatus {
        Ok,
        Error,
        FileError,
        NotEnoughMemory,
        TimeExceeded,
        Unknown
    };
    using enum ComputeStatus;
};


int main() {    
    ComputeEngine::ComputeStatus s = ComputeEngine::Ok;
}

你可以将所有枚举值引入 ComputeEngine 的作用域中,同时享受 enum class 带来的类型安全特性。

C++20 的这一改进使代码更加简洁,并减少了冗余,尤其是在某个作用域内频繁使用多个枚举值的情况下。它提供了一种更加流畅和可读的方式,同时不牺牲作用域枚举所提供的类型安全性。

四、C++23:std::to_underlying

C++23 通过引入 std::to_underlying 进一步增强了 enum class 的可用性,这个实用函数可以将枚举值转换为其基础的整型类型,这个特性解决了将枚举值转换为整数以用于存储、比较或与其他期望整型的 API 交互的常见需求。

这个想法最早出现在 Scott Meyers 的经典著作《Effective Modern C++: 42 Specific Ways to Improve Your Use of C++11 and C++14》一书中。终于在 C++23 中,我们可以享受到这个被标准化的功能。

在 C++23 之前,将枚举转换为其基础类型需要显式的类型转换:

enum class Permissions : uint8_t {
    Execute = 1,
  Write = 2,
    Read = 4
};


uint8_t value = static_cast<uint8_t>(Permissions::Read);

而有了 std::to_underlying,这个转换变得更加直接和清晰:

#include <type_traits>


int main() {
    Permissions p = Permissions::Read;
    auto value = std::to_underlying(p); // C++23
}

std::to_underlying 函数提高了代码的可读性,并减少了与类型转换相关的样板代码。它还明确了意图,使得人们能一目了然地知道这是在获取枚举的基础值。

五、未来的改进

接下来,一个即将到来的重要更新可能是支持 C++26 反射——可查看提案 P2996(虽然这个提案还未被接受,但预计很快就会获得批准)。反射带来了许多激动人心的可能性,比如将枚举转换为字符串的能力。

请看提案中的这个例子:

template <typename E>
  requires std::is_enum_v<E>
constexpr std::string enum_to_string(E value) {
  template for (constexpr auto e : std::meta::enumerators_of(^E)) {
    if (value == [:e:]) {
      return std::string(std::meta::name_of(e));
    }
  }
  return "<unnamed>";
}


enum Color { red, green, blue };
static_assert(enum_to_string(Color::red) == "red");
static_assert(enum_to_string(Color(42)) == "<unnamed>");

当然,你也可以不必等到 C++26,依赖第三方库就能体验这个功能,如 Neargye/magic_enum @GitHub。   

推荐阅读:

▶30+年前的Windows 3.1立大功?被曝依赖“古董级”系统,美国航空急需80亿美元升级!

▶三年成为“高级”软件工程师?「职称膨胀」下的头衔已没有意义

侯捷C++系列精品课程」即将上市!报名该系列课程的学员将获得侯捷老师亲授的 4 门 C++ 精品课程、100+线上精选视频课、纸质版讲义、侯捷老师定期的线上答疑直播、每年一次的线下面对面讲师与学员交流机会。

讲师简介:

2a3ff44e3a8e3a405c1f2a2c17cfb5c2.png

立即扫码进入系列课程预约群👇

512d9c0232872a471bc7fc42cf3de040.png

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐