[读书笔记]《Effective Modern C++》—— 移步现代 C++

前言

这部分内容是《Effective Modern C++》 第 3 章的内容,为了方便与原书对应这里完整地保留了它们的条款名,这部分主要是介绍一些现代C++比较推荐的编程习惯。

每一个条款书中都介绍的比较细节,这里就是整理摘录一些核心知识点,来说明这些条款的理由。

系列推荐阅读:
[读书笔记]《Effective Modern C++》—— 类型推导、auto、decltype
[读书笔记]《Effective Modern C++》—— 智能指针

item7:区别使用 () 和 {} 创建对象

现在 C++ 的初始化对象语法有好几种,有 ()、{} 还有 = :

// 内置类型的初始化
int x(0);
int y = 0;
int z{0};
int n = {0};

// =号初始化,在自定义类可能造成混淆
Widget w1;  // 调用默认构造函数
Widget w2 = w1;  // 这里不是赋值运算,调用的是拷贝构造函数
w1 = w2;  // 调用赋值构造函数(operator=)

并且一些初始化语法,在 C++98 的情况下是没有办法表达的,例如想直接初始化保存值为 1,3,5 的 vector,就必须得一个一个 push 进去。

C++11 使用统一初始化的概念来整合上面的混乱现象,其是指使用单一初始化语法可以用在任何地方,它是基于花括号{} 的。

// 指定容器元素
vector<int> v{1,3,5};

// 初始化非静态类数据成员
class Widget {
    int x{0};  // 允许
    int y = 0;  // 允许
    int z(0);  // 不允许
};

// 不可拷贝对象初始化
std::atomic<int> a1{0};  // 允许
std::atomic<int> a2(0);  // 允许
std::atomic<int> a3 = 0;  // 不允许

花括号初始化可以防止数据类型的变窄变换(double 转 float), () 和 = 都是允许变窄变换的。

花括号初始化还可以防止出现调用无参数构造函数,可能会被识别成函数声明的现象。

Widget w1(); // 会被识别成函数声明
Widget w2{}; // 调用没有参数的构造函数

在前面 auto 类型推导部分介绍,{} 会被 auto 推导成 std::initializer_list 类型,所以如果类的构造函数中包含带有 std::initializer_list 的形参就要小心了。

class Widget {
public:
 Widget(int i, bool b);
 Widget(int i, double d);
 Widget(std::initializer_list<long double> il);
 ...
};

Widget w1(10,true); // 调用第一个构造函数
Widget w2{10,true}; // 调用第三个构造函数,10 和 true 会转成 long double
Widget w3(10,5.0); // 调用第二个构造函数
Widget w4{10,5.0}; // 调用第三个构造函数,10 和 5.0 会转成 long double

vector(5,1) 初始化与 vector{5,1} 初始化的不同表现就是因为上面的原因,vector 在实现时添加了包含 std::initializer_list 形参的构造函数。

总结一下:

  • 花括号初始化可以防止变窄变换,无参数初始化解析成函数声明
  • 构造函数决议中,花括号初始化会最大可能地与带 std::initializer_list 形参的构造函数匹配
  • 编程中最好选择用一个初始化方式,并坚持使用
item8:优先考虑使用 nullptr 而不是 0 或者 NULL

显然字面值 0 是 int 型,不是指针, NULL 也是一个宏定义 0L(long 类型的 0),,当出现重载函数或者模板,涉及到类型匹配或者推导的时候,0 和 NULL 可能会带来混淆,它们并不会被推导成指针类型。

nullptr 则没有这个问题,它实际是 std::nullptr_t 类型,可以表示所有类型的指针。

item9:优先考虑别名声明而非 typedefs

C++11 提供的类型别名就是 using。

// C++98 写法
typedef std::unique_ptr<std::unordered_map<std::string, std::string>> UPtrMapSS;
typedef void(FP*)(int, const std::string&);

// C++11
using UPtrMapSS = std::unique_ptr<std::unordered_map<std::string, std::string>>;
using FP = void(*)(int, const std::string&)

以上的两种用法是等价的,类型别名主要是方便人们可以少打字,并且发生修改时可以统一修改。

using 一个由于 typedef 的地方是可以在模板中使用,即别名声明可以模板化,但是 typedef 不能。

// C++98,《STL 源码解析》中 type traits 就是用这种方式实现的
template <typename T>
struct MyAllocList{
    typedef std::list<T, MyAlloc<T>> type;
};
MyAllocList<Widget>::type lw; // 用户代码

// C++11
template <typename T>
using MyAllocList = std::list<T, MyAlloc<T>>;

MyAllocList<Widget> lw; // 用户代码

在模板类中,声明一个 typedef 声明一个对象,如果这个对象使用了模板形参,就需要在 typedef 前面加上 typename。

// C++98
template <typename T>
class Widget {
 typename MyAllocList<T>::type list;
};

// C++11
template <typename T>
class Widget {
 MyAllocList<T> list; // 没有 typename 和 ::type
};

现在标准库 type_traits 中的函数 C++14 之后就没有了 ::type 的用法,也是得益于 using 在模板中的使用。

std::remove_const<T>::type          //C++11: const T → T 
std::remove_const_t<T>              //C++14 等价形式

std::remove_reference<T>::type      //C++11: T&/T&& → T 
std::remove_reference_t<T>          //C++14 等价形式

总结一下:

  • using 可以模板化,typedef 不可以
  • 在模板中使用 typedef 要加上 typename
  • c++14 提供了 c++11 所有 type_traits 转换的别名声明版本
item10:优先考虑限域枚举而非限域枚举

限域枚举(enum class)相较于非限域枚举(enum)一个是可以防止隐式转换,还以一个就是防止命名空间的污染。

// 命名空间
enum Color1{black, white, red};
auto white = false; // 错误,white 已经在 enum 中声明过了

enum class Color2{yellow, green};
auto green = false; // 没问题
Color2 c = Color2::green;

// 隐式转换
Color1 c1 = white;
if(c1 < 3) // 正确 枚举会隐式转换成整型

Color2 c2 = Color2::green;
if(c2 < 3) // 错误,强类型,不会隐式转,强转要用 static_cast

最后一点就是 enum class 可以被前置声明,这样就可以减少编译依赖了,enum 则只有当指定它们的底层类型时才能前置。

enum Color1 : std::uint8_t;
enum class Color2 : std::uint_8; // 默认是 int,指定底层数据类型

基于上面的特点,enum 也并不是只有不方便,就是因为可以隐式转换也有它易用的地方,就是在部分存在魔数的地方,可以使用 enum 明确表示每个魔数的含义。例如 std::tuple 中:

using UserInfo = std::tuple<std::string,std::string,std::size_t>;

UserInfo uinfo;
auto val = std::get<1>(uinfo); // get 方法直接输入魔数,意义不是很清晰

enum infoFiled(Name,Email,age);
auto val = std::get<Email>(uinfo); //利用隐式转换避免魔数

item11:优先考虑使用 deleted 函数而非使用未定义的私有声明

主要是 C++ 可能会自动生成一些函数,例如类的构造函数等,如果不想类的使用者调用,一般就是不定义然后直接声明成私有,这种方式也不是完全调用不到,friend友元的情况还是会存在可能会调用到的风险,这时未定义的私有声明就会有问题。

比较推荐的做法是使用 C++11 中的 “=delete”,直接将相关构造函数标记为 deleted。注意这里使用 deleted 要把相关成员函数声明成 public。

deleted 可以用在任何函数,其可以阻止部分函数的重载,以及模板一些特例化的生成。

// 删除普通函数的重载
bool isLucky(int number);
if(isLucky('a')) ... // OK
if(isLucky(True)) ... // OK
if(isLucky(2.0)) ... // OK

bool isLucky(char number) = delete;
bool isLucky(bool number) = delete;
bool isLucky(double number) = delete;
// 增加上述函数声明之后
if(isLucky('a')) ... // 错误
if(isLucky(True)) ... // 错误
if(isLucky(2.0)) ... // 错误

// 删除模板的特例化
template<typename T>
void processPointer(T* ptr);

template<>
void processPointer<char>(char*) = delete;
item12:使用 override 声明重载函数

在子类需要对父类的虚函数进行重写的情况下,加上 override 关键字,编译器会帮助检查相关函数签名等是否与基类保持一致,避免一些编码错误,否则可能函数签名拼写错误都不知道。

item13:优先考虑使用 const_iterator 而非 iterator

STL 中的 const_iterator 指指向常量的指针,它们都指向不能被修改的值,这条建议的理由就是能加 const 的地方就加上 const。不过对 const_iterator 的支持 C++11 之后才支持的比较完整。

item14:如果函数不抛出异常请使用 noexcept

这个关键字是程序的编写者确认这个接口不会抛出异常,可以加上 nonexcept 关键字,其作为函数接口的一部分,这意味着调用者可以会依赖它(所以一旦加上 nonexcept 的函数就不要随意更改它的 nonexcept 性了)。

  • 加 nonexcept 的函数想比于不加的函数,更容易优化
  • nonexcept 对于移动语义,swap,内存释放和析构函数都非常有用。(主要还是从效率出发)
  • 大多数函数是异常中立的(可能抛出异常也可能不抛出异常)
item15:尽可能的使用 constexpr

constexpr 对象就是 const,它是在编译期就可知的值的初始化,constexpr 对象和 constexpr 函数可以使用的范围比 non-constexpr 对象和函数大的多。

item16:让 const 成员函数线程安全

这里主要是多线程情况下可以同时在一个对象上执行一个 const 成员函数,如果不是为独占线程编写的,那么 const 性可能就是线程不安全的,这时要确保 const 成员函数的线程安全。

  • 加锁,但是这样的开销会很高
  • std::atomic 变量有比锁更好地性能,但是它只适合操作单个变量或内存位置。
item17:理解特殊成员函数的生成

这条主要关注点集中在 C++ 为类自动生成的构造函数,它们之间有潜在的制约关系,因此不了解这些机制,极可能你以为编译器会自动帮你生成更高效的移动构造,但实际底层还是调用的是低效的拷贝。

c++11 对于特殊成员函数的处理规则如下:

  • 默认构造函数:仅当类不存在用户声明的构造函数时才自动生成
  • 析构函数:仅当基类析构函数为虚函数时该类析构才为虚函数
  • 拷贝构造函数:逐成员拷贝非静态数据。仅当类没有用户定义的拷贝构造时才生成,如果类声明了移动操作它就是 delete 的。当用户声明了拷贝赋值或者析构,该函数自动生成会被废弃。
  • 拷贝赋值运算符:逐成员拷贝非静态数据。仅当类没有用户定义的拷贝赋值时才生成,如果类声明了移动操作它就是 delete 的。当用户声明了拷贝构造或者析构,该函数自动生成会被废弃。
  • 移动构造函数和移动赋值:都对非静态数据执行逐成员移动。仅当没有用户定义的拷贝操作,移动操作或析构时才自动生成。

成员函数模板不会抑制特殊成员函数的生成。
所以这里推荐使用 “=default”, 如果有需要使用编译器自动生成的构造函数,将对应需要使用的显式的声明成 “default”, 编译器就会帮助生成了。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值