自学内容网 自学内容网

Effective Modern C++

文章目录

Effective Modern C++

第1章 型别推导

1.理解模板型别推导

1.1 参数类型是个指针或引用,但不是万能引用
template <typename T>
void f(T& param);

int x = 27;
const int cx = x;
const int& rx = x;

f(x);// T的类型为int, param的类型为 int&
f(cx);//T的类型为const int, param的类型为 const int&
f(rx);// T的类型为const int,param的类型为 const int&

rx的引用性会在型别推导过程中被忽略。

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

int x = 27;
const int cx = x;
const int& rx = x;

f(x);// T的类型为int, param的类型为 const int&
f(cx);//T的类型为int, param的类型为 const int&
f(rx);// T的类型为int,param的类型为 const int&

如前例,rx的引用性在型别推导过程中是被忽略的。

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

int x = 27;
const int* px = &x;

f(&x);// T:int, param:int*
f(px);// T:const int, param: const int*
1.2 参数类型是个万能引用
template <typename T>
void f(T&& param);

int x = 27;
const int cx = x;
const int& rx = x;

f(x);// x是左值,所以 T:int& , param : int&
f(cx);// cx是左值,所以T:const int&,param:const int&
f(rx);// rx是左值,所以T:const int&,param:const int&
f(27);// 右值,所以T是int, param:int&&
1.3 参数类型既非指针也非引用
template <typename T>
void f(T param);

int x = 27;
const int cx = x;
const int& rx = x;

f(x);// T和param的类别都是int
f(cx);// T和pram的型别都是int
f(rx);// T 和param的型别都是int

引用和const都会被忽略掉。

1.4 数组实参
const char name[] = "J.P.Briggs"; //name:const char[13]

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

f(name);//name是数组,T:const char*

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

f(name);// T: const char[13] :param: const char (&)[13]

获取数组含有的元素个数

template <typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept
{
return N;
}
1.5 函数实参
void someFunc(int, double);

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

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

f1(someFunc);// param:void(*)(int,double)

f2(someFunc);//param:void(&)(int, double)
1.6总结
  • 在模板型别推导过程中,具有引用型别的实参会被当成非引用型别来处理换言之,其引用性会被忽略。
  • 对万能引用形参进行推导时,左值实参会进行特殊处理。
  • 对按值传递的形参进行推导时,若实参型别中带有const或volatile 饰词则它们还是会被当作不带 const 或 volatile 饰词的型别来处理。
  • 在模板型别推导过程中,数组或函数型别的实参会退化成对应的指针,除非它们被用来初始化引用。

2.理解 auto 型别推导

与模版性别推导一样,可看模板型别推导。

但是有个例外,如下

auto x1 =27;//型别是int,值为27

auto x2(27);//同上

auto x3 = {27};//型别是std::initializer list<int>,值是{27 }
auto x4{27};//同上
2.1 小结
  • 在一般情况下,auto 型别推导和模板型别推导是一一样的,但是auto型别推导会假定用大括号括起的初始化表达式代表一个std::initializer list但模板型别推导却不会。
  • 在函数返回值或 lambda 式的形参中使用 auto,意思是使用模板型别推导而非auto 型别推导。

3.理解decltype

const int i = 0;//decltype(i):const int

bool f(const Widget& w);//decltype(w):const Widget&

Widget w;//decltype(w):Widget

//C++11

template <typename T1,typename T2>
auto func(T1& t1, T2& t2)->decltype(t1 + t2)
{
return t1 + t2;
}

//C++ 14
template <typename T1,typename T2>
decltype(auto) func(T1& t1, T2& t2)
{
return t1 + t2;
}
int x;

decltype(x); // int
decltype((x)); // int&
  • 绝大多数情况下,decltype 会得出变量或表达式的型别而不作任何修改。
  • 对于型别为丅的左值表达式,除非该表达式仅有一个名字,decltype总是得出型别 T&。
  • C++14 支持 decltype(auto),和auto 一样,它会从其初始化表达式出发来推导型别,但是它的型别推导使用的是 decltype 的规则。

第 2 章 auto

5.优先选用auto,而非显式型别声明

让auto代替一些特别长的类型声明。

  • auto变量必须初始化,基本上对会导致兼容性和效率问题的型别不匹配现象免疫,还可以简化重构流程,通常也比显式指定型别要少打一些字。
  • auto型别的变量都有着条款2和条款6中所描述的毛病

6.当 auto 推导的型别不符合要求时,使用带显式型别的初始化物习惯用法

第3章 转向现代C++

7 在创建对象时注意区分()和{}

  • 大括号初始化可以应用的语境最为宽泛,可以阻止隐式窄化型别转换,还对最令人苦恼之解析语法免疫。
  • 在构造函数重载决议期间,只要有任何可能,大括号初始化物就会与带有std::initializer list型别的形参相匹配,即使其他重载版本有着貌似更加匹配的形参表。
  • 使用小括号还是大括号,会造成结果大相径庭的一个例子是:使用两个实参来创建一个std::vector<数值型别>对象。
  • 在模板内容进行对象创建时,到底应该使用小括号还是大括号会成为一个棘手问题。

8.优先选用 nullptr,而非0或NULL

9.优先选用别名声明,而非 typedef

  • typedef 不支持模板化,但别名声明支持,
  • 别名模板可以让人免写“::type”后缀,并且在模板内,对于内嵌typedef的引用经常要求加上 typename 前缀。

10.优先选用限定作用域的枚举型别,而非不限作用域的枚举型别

  • C++98风格的枚举型别,现在称为不限范围的枚举型别,
  • 限定作用域的枚举型别仅在枚举型别内可见。它们只能通过强制型别转换以转换至其他型别。
  • 限定作用域的枚举型别和不限范围的枚举型别都支持底层型别指定。限定作用域的枚举型别的默认底层型别是int,而不限范围的枚举型别没有默认底层型别。
  • 限定作用域的枚举型别总是可以进行前置声明,而不限范围的枚举型别却只有在指定了默认底层型别的前提下才可以进行前置声明。

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

  • 优先选用删除函数,而非private 未定义函数。
  • 任何函数都可以删除,包括非成员函数和模板具现

12.为意在改写的函数添加 override 声明

  • 为意在改写的函数添加 override 声明。
  • 成员函数引用饰词使得对于左值和右值对象(*this)的处理能够区分开来

13.优先选用 const iterator,而非 iterator

  • 优先选用const iterator,而非iterator。
  • 在最通用的代码中,优先选用非成员函数版本的begin、end和rbegin等,而非其成员函数版本。

14.只要函数不会发射异常,就为其加上noexcept 声朋

  • noexcept声明是函数接口的组成部分,这意味着调用方可能会对它有依赖。
  • 相对于不带 noexcept声明的函数,带有noexcept声明的函数有更多机会得到优化。
    noexcept 性质对于移动操作、swap、函数释放函数和析构函数最有价值
  • 大多数函数都是异常中立的,不具备noexcept性质

15.只要有可能使用 constexpr,京就使用它

  • constexpr对象都具备const属性,并由编译期已知的值完成初始化。
  • constexpr函数在调用时若传入的实参值是编译期已知的,则会产出编译期结果。
  • 比起非constexpr对象或constexpr函数而言,constexpr对象或是constexpr函数可以用在一个作用域更广的语境中。

16.保证const成员函数的线程安全性

  • 保证const成员函数的线程安全性,除非可以确信它们不会用在并发语境中。
  • 运用std::atomic型别的变量会比运用互斥量提供更好的性能,但前者仅适用对单个变量或内存区域的操作。

17.理解特种成员函数的生成机制

在C++官方用语中,特种成员函数是指那些C++会自行生成的成员函数

综上**,声明一个移动构造函数会阻止编译器去生成移动赋值运算符而声明一个移动赋值运算符也会阻止编译器去生成移动构造函数。**

移动操作的生成条件(如果需要生成)仅当以下三者同时成立:

  • 该类未声明任何复制操作
  • 该类未声明任何移动操作
  • 该类未声明任何析构函数
  1. 特种成员函数是指那些 C++会自行生成的成员函数:默认构造函数、析构函数、复制操作,以及移动操作。
  2. 移动操作仅当类中未包含用户显式声明的复制操作、移动操作和析构函数时才生成。
  3. 复制构造函数仅当类中不包含用户显式声明的复制构造函数时才生成,如果该类声明了移动操作则复制构造函数将被删除。复制赋值运算符仅当类中不包含用户显式声明的复制赋值运算符才生成,如果该类声明了移动操作则复制赋值运算符将被删除。在已经存在显式声明的析构函数的条件下,生成复制操作已经成为了被废弃的行为。
  4. 成员函数模板在任何情况下都不会抑制特种成员函数的生成。

第4章 智能指针

18.使用 std::unique_ptr 管理具备专属所有权的资源

  • std::unique_ptr 是小巧、高速的、具备只移型别的智能指针,对托管资源实施专属所有权语义。
  • 默认地,资源析构采用 delete运算符来实现,但可以指定自定义删除器有状态的删除器和采用函数指针实现的删除器会增加std::unique ptr型别的对象尺寸。
  • 将 std::unique ptr转换成 std::shared ptr是容易实现的。

19.使用 std::shared_ptr 管理具备共享所有权的资源

  • std::shared_ptr提供方便的手段,实现了任意资源在共享所有权语义下进行生命周期管理的垃圾回收。
  • 与 std::unique ptr相比,std::shared_ptr 的尺寸通常是裸指针尺寸的两倍,它还会带来控制块的开销,并要求原子化的引用计数操作。
  • 默认的资源析构通过 delete运算符进行,但同时也支持定制删除器。删除器的型别对 std::shared_ptr的型别没有影响。避免使用裸指针型别的变量来创建std::shared ptr 指针。

20.对于类似 std::shared_ptr 但有可能空悬的指针使用 std::weak_ptr

  • 使用 std::weak_ptr来代替可能空悬的std::shared_ptr。
  • std::weak_ptr可能的用武之地包括缓存,观察者列表,以及避免std::shared_ptr指针环路。

21.优先选用 std::make unique 和而非直接使用 newstd::make shared,

  • 相比于直接使用 new表达式,make系列函数消除了重复代码、改进了异常安全性,并且对于std::make shared和std::allcoated shared 而言,生成的目标代码会尺寸更小、速度更快。
  • 不适于使用 make 系列函数的场景包括需要定制删除器,以及期望直接传递大括号初始化物。
  • 对于 std::shared ptr,不建议使用 make 系列函数的额外场景包括:

①自定义内存管理的类;

②内存紧张的系统、非常大的对象、以及存在比指涉到相同对象的 std::shared ptr生存期更久的 std::weak ptr。

22.使用 Pimpl习惯用法时,将特殊成员函数的定义放到实现文件中

Pimpl定义:这种技巧就是把某类的数据成员用一个指涉到某实现类(或结构体)的指针替代,尔后把原来在主类中的数据成员放置到实现类中,并通过指针间接访问这些数据成员。

例子如下:

class Widget
{
public:
    Widget();
    ....
private:
    string name;
    vector<double> data;
    Gadget g1,g2,g3;
};

因为 Widget 的数据成员属于 std::stringstd::vector Gadget 等多种型别,这些型别所对应的头文件必须存在,Widget才能通过编译,这就说明Widget的客户必须#include<string>、<vector>,以及 gadget.h。这些头文件增加了 Widget 的客户的编译时间,此外,它们也使得这些客户依赖于这些头文件的内容。假如某个头文件的内容发生了改变,则 widget 的客户必须重新编译。标准头文件和不会经常改变,但 gadget.h却有可能会经常修订。

优化后:

class Widget
{
public:
    Widget();
    ~Widget();
    ....
private:
    struct Impl;
    Impl* pImpl;

};

由于 Widget 不再提及 std::string,std::vector和Gadget型别,Widget 客户不再需要 #include 这些型别的头文件。这会使编译速度提升,同时也意味着即使这些头文件的内容发生了改变,widget的客户也不会受到影响。

Pimpl习惯用法的第一部分,是声明一个指针型别的数据成员,指涉到一个非完整型别。第二部分是动态分配和回收持有从前在原始类里的那些数据成员的对象,而分配和回收代码则放在实现文件中。例如,对于Widget 而言,这部分代码就位于widget.cpp内.

#include "widget.h"//位于实现文件“widget.cpp”内
#include "gadget.h"
#include <string>
#include <vector>
struct Widget::Impl// Widget::Impl的实现   //包括此前在 Widget 中的数据成员
{
    std::string name;
    std::vector<double> data;
    Gadget g1,g2,g3;
}
Widget::widget()
:pImpl(new Impl)//为本Widget对象分配数据成员所需内存
{}
Widget::~widget()//为本widget 对象析构数据成员
{
    delete pImpl;
}

  • Pimpl惯用法通过降低类的客户和类实现者之间的依赖性,减少了构建遍数
  • 对于采用 std::unique ptr来实现的pImpl指针,须在类的头文件中声明特种成员函数,但在实现文件中实现它们。即使默认函数实现有着正确行为:也必须这样做。
  • 上述建议仅适用于 std::unique ptr,但并不适用 std::shared ptr。

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

23 理解std::move和std::forward

std::move 并不进行任何移动,std::forward也不进行任何转发。这两者在运行期都无所作为。它们不会生成任何可执行代码,连一个字节都不会生成。
std::move和 std::forward 都是仅仅执行强制型别转换的函数(其实是函数模板)
std::move 无条件地将实参强制转换成右值,而std::forward 则仅在某个特定条件满足时才执行同一个强制转换

这就是为何说 std::forward是有条件强制型别转换:仅当其实参是使用右值完成初始化时,它才会执行向右值型别的强制型别转换。

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

24.区分万能引用和右值引用

  • 如果函数模板形参具备T&&型别,并且丅的型别系推导而来,或如果对象使用 auto&&声明其型别,则该形参或对象就是个万能引用。
  • 如果型别声明并不精确地具备 type&&的形式,或者型别推导并未发生,则type&& 就代表右值引用。
  • 若采用右值来初始化万能引用,就会得到一个右值引用。若采用左值来初始化万能引用,就会得到一个左值引用。

25.针对右值引用实施 std::move,针对万能引用实施 std::forward

  • 针对右值引用的最后一次使用实施std::move,针对万能引用的最后一次使用实施 std::forward。
  • 作为按值返回的函数的右值引用和万能引用,依上一条所述采取相同行为。
  • 若局部对象可能适用于返回值优化,则请勿针对其实施std::move或std::forward.

26.避免依万能引用型别重载

第6章 Lambda表达式

31.避免默认捕获模式

  • 按引用的默认捕获会导致空悬指针问题。
  • 按值的默认捕获极易受空悬指针影响(尤其是this),并会误导人们认为lambda 式是自洽的。

32.使用初始化捕获将对象移入闭包

  • 使用 C++14 的初始化捕获将对象移入闭包。
  • 在 C++11 中,经由手工实现的类或 std::bind 去模拟初始化捕获

33.对 auto&&型别的形参使用 decltype以 std::forward 之

34.优先选用 lambda 式,而非std::bind

第7章 并发API

35 优先选用基于任务而非基于线程的程序设计

  • std::thread的API未提供直接获取异步运行函数返回值的途径,而且如果那些函数抛出异常,程序就会终止。
  • 基于线程的程序设计要求手动管理线程耗尽、超订、负载均衡,以及新平台适配。
  • 经由应用了默认启动策略的std::async进行基于任务的程序设计,大部分这类问题都能找到解决之道。

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

  • std::async的默认启动策略既允许任务以异步方式执行,也允许任务以同步方式执行。
  • 如此的弹性会导致使用 thread local 变量时的不确定性,隐含着任务可能永远不会执行,还会影响运用了基于超时的 wait 调用的程序逻辑。
  • 如果异步是必要的,则指定std::launch::async。

原文地址:https://blog.csdn.net/ccb1372098/article/details/142871586

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!