C++11 enum class 语法解析

C++11 enum class 语法解析

要了解enum class的出现,则需要首先了解enum,方才知道为何有这东西。那么Meyers首先举出一个
例子来阐述:

enum Color {black, white, red};
auto white = false; // error

其缘由在于black, white, red等并没有属于Color这个枚举体的作用域,而是与Color在一个作用域中。而为何会这样呢?若再追踪于到C,追踪到enum没出现之前,enum的功能则需要需要一系列的#define来完成,而enum则完成了这一系列#define的“打包收集”,所以我想你可以这样来理解enum,即“解包”

#define black 0
#define white 1
#define red 2

auto white = false

也正是如此,对于两个不一样的枚举体,它们即使枚举体的名字不同,里面的内容也不能重名。如:

enum Direction {TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT};
// error!
enum WindowsCorner {TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT};

这对于我用户来说,这是不可理解的,因为在我心中,WindowsCorner的TOP_LEFT和Direction的TOP_LEFT是不同的。所以,在完成C的#define -> enum的进化后,那么如何更进一步的约束enum,即对于不同的枚举体有更好的类型安全与约束,就成为了C++11的任务,而C++11的答案则是enum class。

而在进入C++11 enum class的时候,我想再稍微提一下C和C++在enum的不同。在一些C代码中,会有这样的用法:

enum Direction {TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT};

int main()
{
    enum Direction d = 1;
}

C是允许的,而C++是不允许的,C++只允许Direction d 由枚举体的TOP_RIGHT等值或者另外一个枚举体变量赋值,所以你可以认为C++的enum比C的enum更加类型安全。

那么收回这个小注意点的思绪,回到C++11的enum class。enum class的用法非常简单,如上文Meyers的例子:

enum class Color {black, white, red};

auto white = false; // OK!

引入了这样的语法后,black, white, red等则隶属于Color这个enum class的作用域,那么下面的auto white的white则与enum class的white是不同的作用域,所以是OK的了。而我们的Direction和WindowsCorner也是可以有同名的TOP_LEFT等值了。

但是,需要注意的一个使用点在于,你若想要赋值一个enum class,你需要指定枚举体作用域,如:

Color c = Color::white; // not white

若我记得没错的话,在以前的Visual C++中,你若是enum,你也可以写Color::white,但是这是不对的。

而有了更加类型安全的enum class后,还有什么益处呢?Meyers则举了一例:

Color c = red;

// You can not stop this
if (c < 14.5) 
{
  //...
}

// if it is enum class
Color c = Color::red;

// Oops
if (c < 14.5)
{
  //...
}

// Unless...
if(static_cast<double>(c) < 14.5)
{
  //...
}

程序只要能暴露错误在编译期间都是好消息,最怕的则是逃逸到了程序员未曾想到的局面,最后运行的结果不对,所以enum class的类型安全是值得推广使用的。

接下来,让我们思考一个问题,那就是如何打印出来enum class?若是在以前的话,你只需要直接cout << c << endl;即可。那么现在呢?

在回答这个问题之前,让我们使用clang,看看我们翻译enum和enum class的中间文件是什么?

enum Color{black,white,red};

int main()
{
  Color c = black;
}

LLVM IR是这样的

; Function Attrs: nounwind ssp uwtable
define i32 @main() #0 {
  %c = alloca i32, align 4
  store i32 0, i32* %c, align 4
  ret i32 0
}

然后再看看enum class

enum class Color{black,white,red};

int main()
{
  Color c = Color::black;
}
LLVM IR:

; Function Attrs: nounwind ssp uwtable
define i32 @main() #0 {
  %c = alloca i32, align 4
  store i32 0, i32* %c, align 4
  ret i32 0
}

可以发现无论是enum或者enum class都变成了i32。那么,若是对enum有比较深的了解的人,应该记得enum其实内部是以整形来存储的,而在C++98/03,你是没有办法指定enum内部以什么样的整形大小来存储,各个编译器实现不一致。而在ABI层面,Enum其实也的确会被转为整形,如:https://github.com/llvm-mirror/clang/blob/master/lib/CodeGen/TargetInfo.cpp#L607

606  // Treat an enum type as its underlying type.
607  if (const EnumType *EnumTy = Ty->getAs<EnumType>())
608    Ty = EnumTy->getDecl()->getIntegerType();

而在这里,我们可以学会一个名词,那就是underlying type。这正是我们的重点,因为我们知道enum / enum class最后都会被转为整形,而这个整形则是underlying type. 所以,我们要打印enum class,则需要enum class的underlying type,那么这就是从C++11引入的 std::underlying_type,那么如这个链接所说:If T is a complete enumeration type, provides a member typedef type that names the underlying type of T. Otherwise, the behavior is undefined. 所以,我们可以通过这个拿到enum class的underlying type.

std::underlying_type<Color>::type // Color underlying_type
// we can use using syntax to simplify.
using color_rep_type = std::underlying_type<Color>::type;
std::cout << static_cast<color_rep_type>(c) << std::endl;

那么既然谈到了underlying_type,我们也知道了enum class内部的表示是整形,那么我们也应该想到enum class可以让我们来指定所需要的underlying_type。的确如此,其表示也很简单,如:

enum [class] color : std::uint8_t
{
  black,
  white,
  red
};

注意我这里的的class是[class], 表示可选的,即这样的语法是支持enum color : std::uint8_t的,但是这样的做法是从C++11开始,C++98/03不支持。

最后,再提一点给Java等语言的程序员,在Java等语言,可以在enum声明方法,但是在C++11的enum class是不能声明函数的,这一点需要注意。

// Wrong
enum class color
{
void foo(){}
};

这一点其实也好解释,因为enum class内部其实会被转为int,如何能与函数相连呢?而Java则不同,如下所示:

// Hello.java
public class Hello {
    public static void main(String args[]) {
        Color c = Color.Black;
        System.out.println("hello from main");
        c.foo();
    }
}

enum Color {
    Black, White, Red;
    public void foo()
    {
        System.out.println("from enum class foo");
    }
}

我们使用javac Hello.java,会产生Hello.class和Color.class文件,随后我们使用javap -v Color>out, 然后打开out文件

Compiled from "Hello.java"
final class Color extends java.lang.Enum<Color>
minor version: 0
major version: 52
flags: ACC_FINAL, ACC_SUPER, ACC_ENUM
Constant pool:
#1 = Fieldref           #4.#39         // Color.$VALUES:[LColor;
#2 = Methodref

// ...

可以看到在Java中,enum的underlying type是class,这与C++的underlying type是整形不同。

那么最后依然是引用Meyers的Best Practice, 在这里第四点我没有进行笔记,那就是enum class可以前置声明,感兴趣的读者可以参考Effective Modern C++书籍,但是我相信你理解了underlying type,理解C++98/03不能前置声明这个问题应该不难,如试想我们在C++98 / 03中,我们不知道每一个枚举值所需要的存储大小…… (那么为什么class / struct可以?That’s another one question ^_^ )

C++98-style enums are now known as unscoped enums.
Enumerators of scoped enums are visible only within the enum. They convert to other types only with a cast.
Both scoped and unscoped enums support specification of the underlying type. The default underlying type for >scoped enums is int. Unscoped enums have no default underlying type.
Scoped enums may always be forward-declared. Unscoped enums may beforward-declared only if their declaration >specifies an underlying type.

猜你喜欢

转载自blog.csdn.net/zgl390963305/article/details/79936350
今日推荐