13-3 无作用域枚举器整型转换

在上一课(13.2——无作用域枚举)中,我们提到枚举器是符号常量。但我们当时没有告诉大家的是,这些枚举器的值是整型的。

这与字符的情况类似(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 actually stores integral value 2

    return 0;
}

可以显式定义枚举值。这些整数值可以是正数或负数,并且可以与其他枚举值相同。任何未定义的枚举值都比前一个枚举值大 1。

enum Animal
{
    cat = -3,    // values can be negative
    dog,         // -2
    pig,         // -1
    horse = 5,
    giraffe = 5, // shares same value as horse
    chicken,     // 6
};

注意,在这种情况下, horsegiraffe 被赋予了相同的值。当这种情况发生时,枚举值就变得不再唯一——本质上,horsegiraffe可以互换。虽然 C++ 允许这样做,但通常应该避免在同一个枚举中为两个枚举值赋相同的值。

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

最佳实践
除非有充分的理由,否则避免为枚举器分配显式值。

枚举值的初始化

如果枚举被初始化为零(当我们使用值初始化时就会发生这种情况),则枚举将被赋予值0,即使没有具有该值的对应枚举器。

#include <iostream>

enum Animal
{
    cat = -3,    // -3
    dog,         // -2
    pig,         // -1
    // note: no enumerator with value 0 in this list
    horse = 5,   // 5
    giraffe = 5, // 5
    chicken,     // 6
};

int main()
{
    Animal a {}; // value-initialization zero-initializes a to value 0
    std::cout << a; // prints 0

    return 0;
}

这会产生两个语义上的影响:

  • 如果枚举器的值是 0,则值初始化会将枚举的默认值设置为该枚举器的含义。例如,以前面的enum Color例子为例,值初始化后的枚举器 black 将默认为 0。因此,最好将值为 0 的枚举器设置为最能代表枚举默认含义的枚举器。

类似这样的事情很可能会引发问题:

enum UniverseResult
{
    destroyUniverse, // default value (0)
    saveUniverse
};
  • 如果没有值为 0 的枚举器,值初始化很容易导致创建语义无效的枚举。在这种情况下,我们建议添加一个值为 0 的“无效”或“未知”枚举器,以便您可以记录该状态的含义,并为该状态指定一个可以显式处理的名称。
enum Winner
{
    winnerUnknown, // default value (0)
    player1,
    player2,
};

// somewhere later in your code
if (w == winnerUnknown) // handle case appropriately

最佳实践
将表示 0 的枚举器设置为最符合枚举默认含义的枚举器。如果没有合适的默认含义,请考虑添加一个值为 0 的“无效”或“未知”枚举器,以便明确记录该状态,并在适当的时候进行显式处理。

无作用域的枚举值将隐式转换为整数值。

尽管枚举存储的是整数值,但它们并不被视为整数类型(它们是复合类型)。然而,无作用域枚举会隐式转换为整数值。由于枚举器是编译时常量,因此这是一个 constexpr 转换(我们将在第10.4 课——范围转换、列表初始化和 constexpr 初始化器中介绍这些内容)。

请考虑以下程序:

#include <iostream>

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

int main()
{
    Color shirt{ blue };

    std::cout << "Your shirt is " << shirt << '\n'; // what does this do?

    return 0;
}

由于枚举类型存储的是整数值,正如你所预料的那样,这将输出:

image

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

由于编译器找不到匹配项,它会接着检查是否 operator<< 知道如何打印非作用域枚举所转换的整形integral的对象。如果知道如何打印,则将shirt值转换为整形值并打印为整形值2。

相关内容:
我们在第13.4 课——枚举与字符串之间转换——中演示了如何将枚举转换为字符串。我们在第13.5 课——I/O 运算符重载介绍——中讲解了std::cout如何打印枚举器。

枚举大小和底层类型underlying type(基类型base

枚举器的值是整数类型。但是是什么整数类型呢?用于表示枚举器值的具体整数类型称为枚举的底层类型underlying type(或基类型base)。

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

可以为枚举显式指定底层类型。底层类型必须是整型。例如,如果您在对带宽要求较高的环境中工作(例如通过网络发送数据),则可能需要为枚举指定一个较小的类型:

#include <cstdint>  // for std::int8_t
#include <iostream>

// Use an 8-bit integer as the enum underlying type
enum Color : std::int8_t
{
    black,
    red,
    blue,
};

int main()
{
    Color c{ black };
    std::cout << sizeof(c) << '\n'; // prints 1 (byte)

    return 0;
}

最佳实践:
仅在必要时才指定枚举的基类型。

警告:
因为 std::int8_tstd::uint8_t 通常是 char 类型的别名,所以使用这两种类型中的任何一种作为枚举基数都可能导致枚举器打印为 char 值而不是 int 值。

整数到非作用域枚举器的转换

编译器会将非作用域枚举隐式转换为整数,但不会将整数隐式转换为非作用域枚举。以下代码会产生编译器错误:

enum Pet // no specified base
{
    cat, // assigned 0
    dog, // assigned 1
    pig, // assigned 2
    whale, // assigned 3
};

int main()
{
    Pet pet { 2 }; // compile error: integer value 2 won't implicitly convert to a Pet
    pet = 3;       // compile error: integer value 3 won't implicitly convert to a Pet

    return 0;
}

image

image

有两种方法可以解决这个问题。

首先,您可以使用static_cast方法将整数显式转换为无作用域枚举器:

enum Pet // no specified base
{
    cat, // assigned 0
    dog, // assigned 1
    pig, // assigned 2
    whale, // assigned 3
};

int main()
{
    Pet pet { static_cast<Pet>(2) }; // convert integer 2 to a Pet
    pet = static_cast<Pet>(3);       // our pig evolved into a whale!

    return 0;
}

image

我们将在第 13.4 课中看到一个例子——枚举与字符串之间的转换,其中我们将用到这个功能。

对于目标枚举的枚举器所表示的任何整数值,进行 static_cast 转换都是安全的。由于我们的Pet枚举包含值为 0、1、2、 和 3 的枚举器,因此static_cast将整数值0、1、2、和3 转换为Pet是有效的。

即使没有枚举器表示某个值,也可以安全地将目标枚举底层类型范围内的任何整数值进行static_cast转换。将超出底层类型范围的值进行静态类型转换会导致未定义行为。

适合高级读者:
如果枚举类型明确定义了底层类型,则枚举类型的范围与底层类型的范围相同。
如果枚举没有显式的底层类型,情况会稍微复杂一些。在这种情况下,编译器会选择底层类型,它可以选择任何有符号或无符号类型,只要所有枚举值都符合该类型即可。因此,只有当整数值符合能够容纳所有枚举值的最小位数范围时,才能安全地使用 static_cast 函数将其转换为整数。
我们举两个例子来说明这一点:

  • 对于值为 2、9 和 12 的枚举器,这些枚举器最小只能放入一个范围为 0 到 15 的无符号 4 位整数类型中。因此,只有将整数值 0 到 15 静态转换为此枚举类型才是安全的。
  • 对于值为 -28、2 和 6 的枚举器,这些枚举器最小可以放入一个范围为 -32 到 31 的有符号 6 位整数类型中。因此,只有将整数值 -32 到 31 静态转换为此枚举类型才是安全的。

其次,从 C++17 开始,如果一个非作用域枚举显式指定了基数,那么编译器将允许你使用整数值来初始化一个非作用域枚举:

enum Pet: int // we've specified a base
{
    cat, // assigned 0
    dog, // assigned 1
    pig, // assigned 2
    whale, // assigned 3
};

int main()
{
    Pet pet1 { 2 }; // ok: can brace initialize unscoped enumeration with specified base with integer (C++17)
    Pet pet2 (2);   // compile error: cannot direct initialize with integer
    Pet pet3 = 2;   // compile error: cannot copy initialize with integer

    pet1 = 3;       // compile error: cannot assign with integer

    return 0;
}

测验时间

问题 1

对(true)或错(false)。枚举器可以是:

  • 给定一个整数值
    显示解决方案
true
  • 没有给出明确的值
    显示解决方案
true
正确。未显式赋值的枚举器将被隐式赋值为前一个枚举器的整数值加 1。如果没有前一个枚举器,则该枚举器的值为 0。
  • 给定一个浮点值
    显示解决方案
false
  • 给定一个负值
    显示解决方案
true
  • 给定一个非唯一值
    显示解决方案
true
  • 给定先前枚举值(例如,品红色 = 红色)
    显示解决方案
true

没错。枚举器不必唯一。由于枚举器会隐式转换为整数,而整数可以传递给枚举器,因此可以使用其他枚举器来初始化枚举器(尽管通常没有必要这样做!)。
  • 给定一个非constexpr值
    显示解决方案
false
错误。由于枚举器是 constexpr 类型,因此它们的值也必须是 constexpr 类型。
posted @ 2025-12-21 10:22  游翔  阅读(11)  评论(0)    收藏  举报