C++

Effective Modern C++(3): Modern C++(1)

Posted by keys961 on May 30, 2022

1. (){}初始化

{}:统一初始化

  • 可用于初始化非静态数据成员

  • 能防止隐式变窄转换(如double -> int

  • 调用构造函数,一般和()初始化效果一样,但优先匹配std::initializer_list参数的构造函数

    • 例子:std::vector

    • 无法转化时(除了拒绝变窄转换),才会回退匹配其它构造函数

    • 若调用类似T w{},优先匹配无参构造函数

()初始化:

  • 符合旧版本C++

  • 避开不经意调用std::initializer_list的问题

所以{}()初始化的选择:

  • 广泛使用{},但需要确保构造函数重载不要和std::initializer_list弄混

  • 两种初始化大部分效果一样,但某些情况可能会有很大不同(如std::vector

2. 优先使用nullptr

优先用nullptr,而不用NULL0

原因:

  • NULL是一个宏,不同编译器的行为可能不同

  • 重载时,传入NULL可能不会调用参数是指针的函数

    • nullptr类型为std::nullptr_t,可以隐式转换为指向任何内置类型的指针,因此不会有这个问题
  • 模板推导时,NULL可能被推导为intlong类型,当参数要求为指针时,会编译不通过

3. 优先使用using声明类型别名

C++98提供typedef实现这个功能,C++11后推荐使用using

1
using Alias = Type; 

using声明别名:

  • 提供了typedef的能力,还能被模板化

  • using在模板中使用,可避免使用::type类似的后缀,

    • 使用typedef时不仅需要加后缀,而且使用模板时,还要在前面加上typename

    • 此外使用typedef时,后面的::type类似的后缀可能和成员名冲突

  • C++14提供了C++11所有type traits转换的别名声明版本

4. 优先使用限域enum

非限域enum是C++98风格,如下所示。其枚举类型+枚举值都会作用到其定义的作用域:

1
2
enum Enum { Value1, Value2, };
auto Value1 = false; // cannot compile

限域enum是C++11风格,如下所示。其枚举值只会作用在大括号内:

1
2
enum class Enum { Value1, Value2, };
auto Value1 = false; // OK

因此优先使用enum class限域枚举:

  • 减少枚举值泄露的污染

  • 强类型

    • 未限域enum会隐式转换为整数,从而歪曲语义
  • 可前置声明(例如enum class Enum;

    • 未限域enum需要指定底层类型才能前置,无默认底层类型
  • 底层类型编译期确定,默认int

    • 若需更改,只需继承某个类型即可

5. 优先标注函数为delete,而非未定义私有声明

若不想让对方调用某函数:

  • C++98:将其定义为private,且不定义

  • C++11:将其标注为= delete

改进和优点:

  • = delete后,友元调用它不能通过编译,而之前只能在链接中检查出来

  • 任何函数都能被标记,不仅仅是类成员函数

  • 可以禁止一些模版的特化(即禁止模板中的T为某个值)

所以= delete比之前的方法做的更多,所以优先使用它。

6. 使用override声明重写父类的函数

重写函数的要求:

  • 父类函数为virtual

  • 函数名+参数类型一致

  • 函数是否const一致

  • 函数返回值+异常声明兼容

  • 函数引用限定符一致(C++11)

    • 即后面加上&或者&&,前者只能在*this为左值时调用,后者只能在*this为右值时调用

      w.func():左值调用

      returnval().func():右值调用

    • 若被右值调用,且返回内部值,考虑使用std::move操作数据,并将其和左值调用区分(即重载)

由于要求很多,所以很容易犯错,导致想重写却没实现重写,编译器可能连warning都不报。

因此,想要重写函数,就在声明时注明override,编译器会帮你检查错误。

同样,final关键字也能帮助函数不被重写,也推荐使用

7. 优先使用常量迭代器

能用const就用const,对于迭代器也是一样。

因此:

  • 若不对容器元素修改,优先使用常量迭代器

    • 例如调用cbegin, cend
  • 若考虑代码通用性(例如在模板中使用),优先考虑非成员函数的begin, end