非限定枚举器的整型转换

在上一课(《非限定枚举》)中,我们提到枚举器是符号常量。当时我们没有告诉你的是,这些枚举器的值是整型的。

这与字符(4.11 课《字符》)的情况类似。考虑以下代码:

char ch{ 'A' };

字符本质上是一个 1 字节的整型值,字符 'A' 被转换为整型值(此例中为 65)并存储。

当我们定义枚举时,每个枚举器会根据其在枚举器列表中的位置自动关联一个整数值。默认情况下,第一个枚举器被赋予整数值 0,后续每个枚举器的值比前一个枚举器的值大 1:

enum Color {
    black,   // 0
    red,     // 1
    blue,    // 2
    green,   // 3
    white,   // 4
    cyan,    // 5
    yellow,  // 6
    magenta, // 7
};
int main() {
    Color shirt{ blue }; // shirt 实际存储整数值 2

    return 0;
}

可以显式定义枚举器的值。这些整数值可以是正数或负数,并且可以与其他枚举器共享相同的值。未定义值的枚举器将被赋予比前一个枚举器的值大 1 的值:

enum Animal {
    cat = -3,    // 值可以是负数
    dog,         // -2
    pig,         // -1
    horse = 5,
    giraffe = 5, // 与 horse 共享相同值
    chicken,     // 6
};

在此例中,horsegiraffe 被赋予了相同的值。当这种情况发生时,枚举器变得不唯一——本质上,horsegiraffe 是可以互换的。尽管 C++ 允许这样做,但通常应避免在同一个枚举中为两个枚举器赋予相同的值。

大多数情况下,默认的枚举器值正是你想要的,因此除非有特定原因,否则不要为枚举器提供自己的值。

最佳实践

除非有充分理由,否则避免为枚举器赋予显式值。

值初始化枚举类型

如果枚举类型被零初始化(使用值初始化时会发生),即使没有对应的枚举器具有该值,枚举类型也会被赋予值 0。

#include <iostream>

enum Animal {
    cat = -3,    // -3
    dog,         // -2
    pig,         // -1
    // 注意:此列表中没有值为 0 的枚举器
    horse = 5,   // 5
    giraffe = 5, // 5
    chicken,     // 6
};

int main() {
    Animal a{}; // 值初始化将 a 初始化为值 0
    std::cout << a; // 打印 0

    return 0;
}

这有两个语义上的后果:

  1. 如果有值为 0 的枚举器,值初始化会使枚举类型默认为该枚举器的含义。例如,在前面的 Color 枚举示例中,值初始化的 Color 将默认为 black。因此,考虑将值为 0 的枚举器设为枚举类型的最佳默认含义是个好主意。
  2. 像下面这样的代码可能会引发问题:
enum UniverseResult {
    destroyUniverse, // 默认值 (0)
    saveUniverse
};

如果没有任何枚举器的值为 0,值初始化很容易创建一个语义上无效的枚举类型。在这种情况下,我们建议添加一个值为 0 的“无效”或“未知”枚举器,以便明确记录该状态的含义,并在适当的地方明确处理该状态。

enum Winner {
    winnerUnknown, // 默认值 (0)
    player1,
    player2,
};

// 在代码的其他地方
if (w == winnerUnknown) // 适当处理该情况

最佳实践

将值为 0 的枚举器设为枚举类型的最佳默认含义。如果没有合适默认含义,考虑添加一个值为 0 的“无效”或“未知”枚举器,以便明确记录并处理该状态。

非限定枚举会隐式转换为整数值

尽管枚举类型存储的是整数值,但它们并不被视为整型(它们是复合类型)。然而,非限定枚举会隐式转换为整数值。由于枚举器是编译时常量,这种转换是 constexpr 转换(我们在 10.4 课《窄化转换、列表初始化与 constexpr 初始化器》中讨论过)。

考虑以下程序:

#include <iostream>

enum Color {
    black, // 赋值 0
    red,   // 赋值 1
    blue,  // 赋值 2
    green, // 赋值 3
    white, // 赋值 4
    cyan,  // 赋值 5
    yellow,// 赋值 6
    magenta,// 赋值 7
};

int main() {
    Color shirt{ blue };

    std::cout << "Your shirt is " << shirt << '\n'; // 这会做什么?

    return 0;
}

由于枚举类型存储的是整数值,因此正如你所期望的,这会打印:

Your shirt is 2

当枚举类型用于函数调用或运算符时,编译器会首先尝试查找与枚举类型匹配的函数或运算符。例如,当编译器尝试编译 std::cout << shirt 时,它会首先检查 operator<< 是否知道如何将 Color 类型的对象打印到 std::cout(因为 shirtColor 类型)。它不知道。

由于找不到匹配项,编译器会检查 operator<< 是否知道如何打印非限定枚举转换为的整型值的对象。由于它知道,shirt 中的值被转换为整数值并作为整数值 2 打印。

相关阅读

我们在 13.4 课《枚举与字符串的相互转换》中展示了如何将枚举类型转换为字符串。 我们在 13.5 课《输入/输出运算符的重载》中教 std::cout 如何打印枚举器。

枚举大小与底层类型(基)

枚举器的值是整型的。但具体是哪种整型呢?用于表示枚举器值的具体整型称为枚举的底层类型(或基)。

对于非限定枚举,C++ 标准并未指定应使用哪种特定的整型作为底层类型,因此选择由实现决定。大多数编译器会使用 int 作为底层类型(这意味着非限定枚举的大小将与 int 相同),除非需要更大的类型来存储枚举器的值。但你不应假设这适用于每个编译器或平台。

可以显式指定枚举的底层类型。底层类型必须是整型。例如,如果你处于某种带宽敏感的上下文中(例如通过网络发送数据),你可能希望为枚举指定一个较小的类型:

#include <cstdint>  // 用于 std::int8_t
#include <iostream>

// 使用 8 位整数作为枚举的底层类型
enum Color : std::int8_t {
    black,
    red,
    blue,
};

int main() {
    Color c{ black };
    std::cout << sizeof(c) << '\n'; // 打印 1(字节)

    return 0;
}

最佳实践

仅在必要时指定枚举的底层类型。

警告

由于 std::int8_tstd::uint8_t 通常是字符类型的类型别名,使用这些类型作为枚举的底层类型可能会导致枚举器以字符值而非整数值打印。

整数到非限定枚举器的转换

虽然编译器会隐式地将非限定枚举转换为整数,但它不会隐式地将整数转换为非限定枚举。以下代码会产生编译错误:

enum Pet // 未指定底层类型
{
    cat, // 赋值 0
    dog, // 赋值 1
    pig, // 赋值 2
    whale, // 赋值 3
};

int main() {
    Pet pet{ 2 }; // 编译错误:整数值 2 不会隐式转换为 Pet
    pet = 3;       // 编译错误:整数值 3 不会隐式转换为 Pet

    return

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"