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

本文介绍了C++编程中的最佳实践,包括使用花括号初始化以防止类型转换问题,优先使用nullptr代替0或NULL以避免类型混淆,采用别名声明(using)而非typedef,选择限域枚举以增强类型安全性,使用deleted函数禁止不必要的函数调用,使用override关键字确保重载函数正确,优先使用const_iterator,声明noexcept函数以保证不抛出异常,以及理解并控制特殊成员函数的生成。这些实践有助于提升代码质量和可维护性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

这部分内容是《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”, 编译器就会帮助生成了。

Coming to grips with C++11 and C++14 is more than a matter of familiarizing yourself with the features they introduce (e.g., auto type declarations, move semantics, lambda expressions, and concurrency support). The challenge is learning to use those features effectively—so that your software is correct, efficient, maintainable, and portable. That’s where this practical book comes in. It describes how to write truly great software using C++11 and C++14—i.e. using modern C++. Topics include: The pros and cons of braced initialization, noexcept specifications, perfect forwarding, and smart pointer make functions The relationships among std::move, std::forward, rvalue references, and universal references Techniques for writing clear, correct, effective lambda expressions How std::atomic differs from volatile, how each should be used, and how they relate to C++'s concurrency API How best practices in "old" C++ programming (i.e., C++98) require revision for software development in modern C++ Effective Modern C++ follows the proven guideline-based, example-driven format of Scott Meyers' earlier books, but covers entirely new material. "After I learned the C++ basics, I then learned how to use C++ in production code from Meyer's series of Effective C++ books. Effective Modern C++ is the most important how-to book for advice on key guidelines, styles, and idioms to use modern C++ effectively and well. Don't own it yet? Buy this one. Now". -- Herb Sutter, Chair of ISO C++ Standards Committee and C++ Software Architect at Microsoft
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值