Effective Modern C++

Effective Modern C++

第一章:类别推导

条款1:理解模板类型推导

函数声明。

template<typename T>
void f(ParamType param);

函数调用

void f(expr);

情况1:ParamType含引用或指针。

template<typename T>
void f([const] T& param);

template<typename T>
void f([const] T* param);

模板推导出来也一定是指针或者引用。

不含const模板可以推导出含constexpr调用。

template<typename T>
void f(T* val) {
    printf("void f(T* %d)\n", *val);
}

int main() {
    int* a = new int(1);
    f(a);

    const int* b = new int(2);
    f(b);
}
/* result
void f(T* 1)
void f(T* 2)
*/

但注意,如果函数违法了const,会报错。

template<typename T>
void f(T* val) {
    *val = 10; //Read-only variable is not assignable
    printf("void f(T* %d)\n", *val);
}

int main() {
    const int* b = new int(2);
    f(b);
}

情况2: ParamType是个万能引用。

template<typename T>
void f(T&& param);

如果expr

  • 左值,则参数类型最终为[const] T&
  • 右值,则参数类型最终为[const] T&&

即,万能引用能够推导是左值还是右值。

const的推到类似情况一,根据expr是否有const决定。

情况3: 按值传递

template<typename T>
void f(T param);

通过copy constructor构造处理

由于是副本,因此该expr本身的

  • const
  • 引用
  • volatile

都会被忽略(对它的副本改变并不破坏它本身的const)。

值得一提的是,仅仅会移除形参本身的const

const char* const ptr = "Fun with pointers";
f(ptr); //推到类型为 const char*

即,函数参数是一个const char*,是ptr的副本。

数组实参

类似C中语法,数组会退化成首元素指针。

const char arr[] = "array";

template<typename T>
void f(T Param)

f(arr);

arr是个数组,会被推导成指针。

但如果是引用,则正常推导。

template<typename T>
void f(T& Param)

f(arr); //推导成 const char (&)[size] 类型,其中size是arr的大小 + 1 (\0)。

获取数组大小。

template<typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) {
    retur N;
}

函数实参

除了数组退化成指针,函数和数组处理相同。

void someFunc(int, double);

template<typename T>
void f1(T param);

template<typename T>
void f2(T& param);

f1(someFunc); //函数指针 void (*)(int, double);
f2(someFunc); //函数引用 void (&)(int, double);

条款2:理解auto类别推导

总的来说,auto推导和模板推导是一个原理。

仅有一处例外:auto能够推导出initializer_list,而模板不能。

auto x = {1, 2, 3}; //x type : initializer_list
template<typename T>
void f(T param);

f({1, 2 ,3}); // error can't detect.

不过,模板可以推导initializer_list<T> T 的类型。

template<typename T>
void f(std::initializer_list<T> initList);

f({1, 2, 3});

但在如下情况,auto也不能推导。

  1. 函数返回值。

    auto createInitList() {
        return {1, 2, 3, 4}; //error
    }
  2. lambda函数形参

    auto resetV = [&v](const auto& newValue) { v = newValue; };
    resetV({1, 2, 3}); //error

条款3:理解decltype

一般来说,decltype推导出来的就是被推导的类型,包括constvolatile、引用等特性。

template<typename Container, typename Index>
auto authAndAccess(Container& c, Index i) -> decltype(c[i]) { //有问题,auto会移除引用的特性
    return c[i];
}

vector<int> vec{1, 2, 3};
authAndAccess(vec, 1) = 10; //会出问题,因为返回值移除了引用特性,等号左边是一个右值。
//(读者注:本人测试的时候,authAndAccess返回的是引用类型。

搭配decltype即可保留引用的特性。

template<typename Container, typename Index>
decltype(auto) authAndAccess(Container& c, Index i) { //C++14 feature。
    return c[i];
}

值得一提的是,decltype在如下情况不是预期值。

  • C++硬性规定最小取址单位为字节,vector<bool>的元素不是bit大小,使用的是代理类模拟其他vector实现的功能。

  • 对对变量加上小括号即为引用类型。

    int x = 10;
    decltype((x)) ref = x;//ref's type is int&

条款4:掌握查看类别推到结果的方法

  • 利用IDE、type_inex查看类型,但有可能出错。

第二章:auto

条款5:优先选用auto,而非显示类别声明

  • auto变量必须初始化,可以避免一些打字出错导致的隐式类型转换。也可以简化重构流程。
  • auto变量具有条款2和条款6所说的毛病。

条款6:当auto推导不符合要求时,显式声明

  • 隐私类型转换会导致auto推导不符合预期。如条款2中提到的vector<bool>元素,不能使用auto
  • 使用static_cast强制类型转换成预期类型。

第三章: 转向现代C++

条款7:在创建对象时区分(){}

  • 大括号初始化可以阻止隐式窄化类别转换,还有解析语法友好。
  • 在构造函数重载决议期间,含initializer_list的f构造函数有特别高的匹配优先级。

条款8:优先选用nullptr,而非0NULL

  • 防止模板推导和一些隐式转换的问题。

条款9:优先选用别名声明,而非typedef

  • tyepdef不支持模板化,但类型别名using支持。
  • 别名模板可以免写::type后缀,内嵌时也需要加上typename前缀。

条款10:优先选用限定作用域的枚举类型,而非不限作用域的枚举类型

  • 限定作用域的枚举类型仅在枚举类别内可见。它们只能通过强制类型转换到其他类型。
  • 两种都支持指定底层类型。限定作用域的默认为int,而不限的则没有默认的。
  • 限定作用域的可以前置声明,而不限范围的不行。

条款11:优先选用删除函数,而非private未定义

  • 任何函数都可以删除,包括非成员函数和模板特例。

条款12:为意在重写的函数添加override

  • 成员函数引用饰词(&&&)使得对于左值和右值对象(*this)能够区分。

    auto val = makeWidget().data(); //对于右值应该有不一样的行为。
    
    class Widget{
      void f() &; //自身是左值Widget左值调用
      void f() &&; //自身是右值Widget右值调用
    };

条款13:优先选用const_iterator,而非iterator

  • 由于C++11未对某些容器做const_iterator(C++14有), 对于特定情况下可用非成员版本的beginendrbegin等。

条款14:只要函数不会发射异常,就为其加上noexcept声明

  • 对于移动操作,swap,内存释放函数和析构函数最有价值。

条款15:只要有可能使用constexpr,就用

条款16:保证const成员函数的线程安全性

条款17:理解特殊成员函数的生成机制

  • 含默认构造函数,析构函数,复制操作(复制构造和复制赋值),移动操作(移动构造和移动赋值)。生成的前提都是没有显式生成。
    • 默认构造函数:没有显示声明任何构造函数。
    • 复制构造:没有声明移动操作,但已经存在析构或复制赋值情况下生成已成为被废弃(deprecated)的行为
    • 复制赋值:没有声明移动操作,但已经存在析构或复制构造情况下生成已成为被废弃(deprecated)的行为。
    • 移动操作:五个函数都不含。
  • 成员模板在任何情况下都不会抑制特殊成员函数的生成(如完美转发可能出现的问题)

第四章:智能指针

条款18:使用unique_ptr

  • 小巧、高速、只移的智能指针。
  • 推荐使用工厂方法make_unique生成,但不能指定删除器。
  • 可转化为shared_ptr

条款19:使用shared_ptr

std::enable_shared_from_this<T>的使用。

class Widget;
vector<shared_ptr<Widget>> task;

class Widget {
    void process(vector<int>& ) {
        /*
         * do some init work;
         */
        task.emplace_back(this); // wrong!!! 
        // this will create more than one shared_ptr from the same raw ptr;
        // delete more than once.
    }
};

用法

class Widget;
vector<shared_ptr<Widget>> task;

class Widget: public std::enable_shared_from_this<Widget> {
    void process(vector<int>& ) {
        /*
         * do some init work;
         */
        task.emplace_back(shared_from_this); 
    }
};

条款20:对于类似但可能空悬的指针使用std::weak_ptr

Usage:

std::weak_ptr<Widget> wpw(spw);
spw = nullptr; //deconstructor.
if (wpw.expired()) {
    ...
}

std::shared_ptr<Widget> spw1 = wpw.lock(); //若wpw失效, 则spw1为nullptr。
atuo spw2 = wpw.lock(); //同上

std::shared_ptr<Widget> spw3(wpw); //若wpw失效,则抛出异常。

优势

  1. 缓存读取
  2. 观察者列表
  3. 避免环路

条款21:优先选用std::make_uniquestd::shared_ptr而非直接构造

  • new和智能指针的步骤整合。但无法自定义析构器
  • 由于经过了一次完美转发,因此无法使用大括号初始化(见条款2)。

条款22:使用Pimpl习惯用法时,将特殊成员的定义放在实现文件中

  • Pimpl模式降低类的设计者和使用者的依赖性
  • 使用采用std::unique_ptr来实现pImpl指针,须在类的头文件中声明特种成员函数,但在实现文件中实现他们。即使默认的函数具有正确的行为。
  • 上述建议仅仅适用unique_ptr但不适用shared_ptr

第五章:右值引用、移动语义和完美转发

条款23:std::movestd::forward

  • move实施的是无条件的强制右值转换,就本身而言,并不执行移动操作。
  • 仅当传入的实参被绑定到右值时,std::forward才执行该实参向右值类型的强制类型转换。
  • 运行期,std::movestd::forward都不会执行任何操作。

条款24:区分万能引用和右值引用

  • 如果函数模板具备T&&类型并且T的类别由推导而来,或如果对象使用auto&& 推导,则为万能引用。
  • 反之为右值引用。
  • 万能引用:区分左值右值,左值推导为左值,右值推导为右值。

条款25:针对右值引用使用std::move,万能引用使用std::forward

条款26:避免依万能引用进行重载

  • 把万能引用作为重载候选类别时,几乎总会意外被调用
  • 特别是完美转发构造函数。

条款27:条款26的替代方案

略…

条款28:理解引用折叠

  • 在四种语境下:模板实例化,auto类型推导,创建和使用typedef和别名(using)声明,以及decltype
  • 上述语境下,引用的引用会折叠为一个引用。

条款29:假定移动操作不存在、成本高、未使用

  • 提高兼容性
  • 如果明确支持移动,则无需假定。

条款30:完美转发失败的情形

  • 大括号初始化物{}
  • 0NULL作空指针。
  • 仅有声明的整形static const成员变量(常量无法取址,由于引用底层实际是指针,需要地址)。
  • 转发给重载的函数名字和模板名字(可手动指定static_cast成指定函数)。
  • 位域(位域单位为bit,C++规定非const不能指定到单个bit,因为操作最小单位为1byte)。

第六章:lambda表达式

条款31:避免默认捕获模式

在类中捕获成员,实际捕获的是this。当该对象被析构时,行为未定义。

(经测试,有些编译器会给出编译错误: “‘this’ cannot be implicitly captured in this context”)。

vector<function<void()>> vec;
//经测试,读者的编译器会给出编译错误: "'this' cannot be implicitly captured in this context"。
class Widget {
public:
    int val = 10;
    void f()  {
        vec.emplace_back([] () {
            cout << val << endl;
        });
    }
};

//error
class Widget {
public:
    int val = 10;
    void f()  {
        vec.emplace_back([=] () {
            cout << val << endl;
        });
    }
};

解决办法:

//本地copy
class Widget {
public:
    int val = 10;
    void f()  {
        int copy = this->val;
        vec.emplace_back([=] () {
            cout << copy << endl;
        });
    }
};

//初始化捕获,也称广义捕获
class Widget {
public:
    int val = 10;
    void f()  {
        vec.emplace_back([val = this->val] () {
            cout << val << endl;
        });
    }
};

条款32:使用初始化捕获将对象移入闭包

//初始化捕获,也称广义捕获
class Widget {
public:
    int val = 10;
    void f()  {
        vec.emplace_back([val = this->val] () {
            cout << val << endl;
        });
    }
};

条款33:对auto&&类别的形参使用decltypeforward

forward<decltype(param)>(param);

条款34:优先选用lambda,而非bind

第七章:并发API

这部分建议阅读:C++ Concurrency in Action: Practical Multithreading by Anthony Williams.

My Note: messenger1th/Concurrency (github.com)

条款35:优先选用基于任务而非基于线程的程序

  • 任务是更高层的API,可以获取运算线程的结果。而基于线程,则无返回值。

  • 基于线程,需要手动管理线程耗尽,超订,负载均衡,以及新平台适配。

  • 基于任务,启动方式多样,更自由。

条款36:如果异步是必要的,则指定std::launch::async

条款37:使std::thread型对象所在路径皆不可联结

条款38:对变化多端的线程句柄析构函数行为保持关注

条款39:考虑针对一次性时间通信使用void为模板类型实参的promise

条款40:对并发使用std::atomic,特种内存使用volatile

第八章:微调

条款41:针对可复制的形参,在移动成本低且一定会被复制的前提,考虑按值传递

  • 实际上,需要对应用场景进行具体评估。

条款42:考虑emplace而非插入

  • 直接使用参数构造,而非构造后移动或复制。

读者总结

前面几章能够理解,后面的并发有些抽象和底层了,我感觉不如C++ Concurrency In Action来的透彻。


Effective Modern C++
https://messenger1th.github.io/2024/07/24/C++/Effective Modern C++/
作者
Epoch
发布于
2024年7月24日
许可协议