《Effective Modern C++》学习记录(三):小特性2
1 限域enum
用法:
1 | enum class 枚举名 {...}; |
1.1 优点
- 避免枚举名泄露
1 | //不限域 |
- 限域enum在其作用域中是强类型,而不限域的enum会隐式转换为整型甚至浮点型导致扭曲的效果。
1 | enum Color { black, white, red }; |
- 限域enum可以直接前置声明,而非限域enum需要指定底层类型(C++11以后才可以指定底层类型)才可以前置声明。
1 | enum Color; //错误 |
原理
编译器通常在确保能包含所有枚举值的前提下为enum选择一个最小的底层类型,减少内存使用;也有一些情况会优化速度,舍弃大小,则不一定选择尽可能小的底层类型。
C++98只支持enum定义,是因为编译器需要根据列出来的枚举值在使用之前为enum选择一个底层类型。而在C++11之后,可以指定底层类型,也就可以前置声明enum。
限域enum可以直接前置声明而不需要指定底层类型,是因为它会有一个默认的底层类型。
不能前置声明enum的缺点
增加编译依赖。一个头文件的定义修改时(枚举定义增加/减少枚举值),其他包含它的头文件都需要重新编译。通过enum前置声明,告诉编译器某个枚举类型的存在,之后在其他地方定义/修改,包含声明的头文件都不需要重新编译,使用这个enum的,只要没用到新添加的枚举值,也不需要重新编译。
1.2 缺点
- 配合C++11的
std::tuple使用时(std::tuple的用法是定义一个变量存储不同数据类型的元素)
1 | //tuple一般使用,此时很难记住获取的字段代表什么含义 |
改良方法
既避免冗长的表示,且又能使用限域enum避免命名空间污染:编写函数将限域枚举类型的值转换为std::size_t类型的值,或者直接转换为可以作为std::get模板实参的枚举的底层整数类型。
1 | //详解: |
2 deleted函数
使用场景1:对于成员函数,如果不想被客户端调用,可以声明为私有的;如果也不想被其他成员函数或类的友元调用,就可以不定义它们。用
= delete可以直接将函数声明标记为“删除的函数”,实现以上功能,并且deleted函数一般要声明为public(一定要声明为public),这样被调用时会更清晰的报错而不是报private错误。使用场景2:相比较于private,deleted可以标记所有函数,包括普通函数。因此可以避免C++无意义的类型转换。
1 | //如果有这样一个函数去判定是否幸运数 |
- 使用场景3:禁止特定的模板实例化。
1 | //对于以下的模板函数 |
3 override和final
override作用: 派生类重写基类函数时加上此关键字,以防想要重写函数,但写错了被视为派生类自己的成员函数,进而编译器无法报错提示。
1 | class Base { |
final作用: 1、给虚函数添加final可以防止派生类重写。2、final用于类,则此类无法作为基类。
其他:这两个关键字只在特定上下文才被视为关键字。例:override只在成员函数结尾处才被视为关键字。
1 | class Warning { //C++98潜在的传统类代码 |
4 引用限定
作用: 可以限定成员函数只能用于左值或者右值
1 | class Widget { |
5 constexpr关键字
- 作用:用于指定变量、函数、构造函数可以在编译时求值,大大优化性能。
5.1 constexpr变量
constexpr int max_size = 100; // 编译时常量
- 必须初始化,且初始化表达式必须是一个常量表达式。
1 | int sz; //non-constexpr变量 |
5.2 constexpr函数
当传递的是编译期可知的值时,constexpr函数可以产出编译期可知的值。传递的值是运行时可知的,constexpr产出的值就也是运行时才可知。
1 | constexpr int square(int x) { return x * x; } |
5.3 constexpr构造函数
1 | class Point { |
- 类对象必须是
const或constexpr。 - 初始化表达式必须是一个常量表达式
5.4 if constexpr
作用:可以在编译时根据常量表达式决定执行的代码块
基本语法:
1 | if constexpr (常量表达式) { |
- 实例:根据模板的类型来决定执行哪块代码
1 |
|
5.5 constexpr和const的关系
所有constexpr对象都是const,但不是所有const对象都是constexpr。
1 | int sz; |
6 noexcept
6.1 noexcept声明的方式
1 | // 函数声明 |
6.2 声明noexcept的好处和作用
性能优化:
编译器可以对 noexcept 函数进行额外的优化。例如,在某些情况下,编译器可以省略设置异常处理代码(如栈展开所需的元数据),从而提高程序的执行效率。
在移动操作中,如果一个类型声明了其移动构造函数或移动赋值操作符为 noexcept,那么标准库容器(如 std::vector)在需要重新分配内存时会优先使用移动而非拷贝,这通常能带来显著的性能提升。
异常安全性:
避免栈展开:当异常抛出时,如果函数被声明为 noexcept,程序会调用 std::terminate()以未定义的方式终止,避免了栈展开过程,这有助于减少程序崩溃的风险。
接口清晰性:
明确地声明一个函数为 noexcept 可以使接口更加清晰,使用者知道这个函数不会抛出异常,因此不需要为其编写异常处理代码。这也使得代码的意图更加明确,增强了可读性和维护性。
标准库兼容性:
标准库中的某些功能依赖于 noexcept 保证。例如,std::move_if_noexcept 会根据移动构造函数是否被声明为 noexcept 来决定是使用移动还是拷贝。如果移动操作被声明为 noexcept,则优先使用移动;否则使用拷贝。
7 const_iterator
- 对于迭代器使用上优先使用 const_iterator, 而非 iterator
const_iterator 是表示迭代器指向的对象是一个常量,相当于常量指针(左const),指针指向的对象是常量; 而 const iterator 是表示迭代器本身是一个常量,相当于指针常量(右const),指针本身是一个常量,指针本身不可修改,但是指向的对象,该对象可以修改。
8 特殊成员函数的生成
C++11后会自动生成的特殊函数:默认构造函数、析构函数、拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符。
两个拷贝操作是独立的:声明一个不会限制编译器生成另一个,例如声明了拷贝构造函数,但没有声明拷贝赋值运算符,但写的代码用到了拷贝赋值,编译器就会生成拷贝赋值运算符;两个移动操作不是相互独立:如果声明了其中一个,编译器就不再生成另一个。
对于除默认构造函数之外的五个特殊函数在C++11之后有0/3/5原则:
- 一个也不声明。
- 两个拷贝和析构,声明了其中一个就要声明另外两个。
- 两个拷贝两个移动和析构全部声明。
当下面条件成立时才会生成移动操作
- 类中没有拷贝操作
- 类中没有移动操作
- 类中没有用户定义的析构
对于编译器可能不会生成的特殊函数,default关键字总能强制编译器生成这些函数的默认实现(不需要定义)。
1 | class Base { |
- 其他自动生成规则:
- 默认构造函数:当类不存在用户声明的构造函数时才自动生成。
- 析构函数:当基类析构为虚函数时,改类析构才为虚函数。
- 拷贝构造函数:当类没有用户定义的拷贝构造时才生成,如果类声明了移动操作,它就是delete的。
- 拷贝赋值运算符:没有用户定义的拷贝赋值时才生成,如果类声明了移动操作,它就是delete的。



