Effective Modern C++ 笔记

目录

1 型别推导


条款1 理解模板型别推导

理解如下形式的模板型别推导:

template<typename T>
void f(ParamType param);
f(expr); // 从expr来推导ParamType和T

1.ParamType是个指针或引用T&、T*,但不是万能引用。
若expr具有引用型别,先忽略它的引用型别。

template<typename T>
void f(T& param);
int x = 72;
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&

2.ParamType是个万能引用
(1)如果expr是个左值,T和ParamType都会被推导为左值引用。
(2)如果expr是个右值,常规推导。
关键是,万能引用会区分expr是左值还是右值,非万能引用则不会区分。

template<typename T>
void f(T&& param);
int x = 72;
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); // 27是个右值,所以T的型别是int,param型别是int&&

3.ParamType既非指针,也非引用。
(1)若expr具有引用型别,可忽略。
(2)在(1)的基础上,若expr是个const或者volatile对象,也忽略之。

template<typename T>
void f(T param);
int x = 72;
const int cx = x;
const int& rx = x;
f(x); // T和param型别都是int
f(cx); // T和param型别还是int!!
f(rx); // T和param型别还是int!!

(3)特例,C风格字符串,整体保留。

template<typename T>
void f(T param);
const char* const ptr = "FUN";
f(ptr); // 此时T和param为const char*

注意:当ptr传给f时,会按值传递指针本身,所以指针的const被忽略,但指涉对象的const被保留!!

4.数组实参
数组可退化成指涉其首元素的指针。

void myFunc(int param[]); 
void myFunc(int* param);
// 上述声明等价。
template<typename T>
void f(T param);
const char name[] = "abs";
f(name); // name是个数组,但T被推导为const char*

特例:按引用方式传递数组!!

template<typename T>
void f(T& param);
f(name); // T的型别推导结果是const char[4], param被推导为const char(&)[4]

即引用方式传递数组可保留其大小,且传递的是一个数组对象,不会退化为指针。
可利用此特性,写一个在编译期获得数组大小的模板。

// 以编译期常量形式返回数组尺寸
template<typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept { // noexcept参考条款14
  return N;
}

// 使用
int keyVals[] = { 1, 3, 7, 9 };
int mappedVals[arraySize(keyVals)];
// C++程序员应该用std::array替换内建数组
std::array<int, arraySize(keyVals)> mappedVals;

5.函数实参
函数实参的指针退化和引用特例,均和数组差不多。

void someFunc(int, double); // someFunc是函数,型别是void(int, double)
template<typename T>
void f1(T param);
template<typename T>
void f2(T& param);
f1(someFunc); // T和param均被推导为函数指针,型别是void(*)(int, double)【退化】
f2(someFunc); // T被推导为函数对象,param型别是void(&)(int, double)【引用保留】

条款2 理解auto型别推导

auto的推导基本等同于模板型别推导(且在编译期推导)

template<typename T>
void f(ParamType param);
const auto& rx = x;
// auto 扮演了T的角色,变量的型别修饰则是ParamType的角色

所以可以类比以下情况

auto x = 27;
const auto cx = x;
const auto* rx = x;

auto&& uref1 = x; // uref1为int&
auto&& uref2 = cx; // uref2为const int&
auto&& uref3 = 27; // uref3为int&&

const char name[] = "abs"; // name为const char[4]
auto arr1 = name; // arr1为const char*
auto& arr2 = name; // arr2为const char(&) [13]
void someFunc(int, double);
auto func1 = someFunc; // func1为void(*)(int, double)
auto& func2 = someFunc; // func2为void(&)(int, double)

特例是在使用大括号进行初始化时。
C++11以下形式含义相同。

int x1 = 27;
int x2(27);
int x3 = {27};
int x4{27};

但对于auto而言不同
1.auto可以用于{}的推导

auto x1 = 27;
auto x2(27);
auto x3 = {27}; // 型别是std::initializer_list<int>
auto x4{27};

auto x5 = { 1, 2, 3.0 } //推导错误,推导不出std::initializer_list<T>中的T

以上其实蕴含了两个推导,首先x5推导为std::initializer_list<T>,然后推导T。
2.模板型别推导不能推导{}

auto x = { 11, 23, 9 }; // 型别是std::initializer_list<int>
template<typename T>
void f(T param);
f({ 11, 23, 9 }); // 错误,无法推导T的型别。

template<typename T>
void f(std::initializer_list<T> param);
f({ 11, 23, 9 }); // 正确。

3.C++14允许auto作为返回值,允许lambda形参中用auto,但仍然不适用于{}的情况

auto createInitList() {
  return { 1, 2, 3 }; // 错误。
}
std::vector<int> v;
auto resetV = 
  [&v](const auto& newValue) { v = newValue; };
resetV({ 1, 2, 3 }); // 错误。

条款3 理解decltype

decltype:“声明类型”(Declared Type)
1.绝大多数情况下,decltype不做任何修改
除了vector<bool>的情况,它的operator[]返回的是一个对象而非引用
和auto推导的区别,auto推导可能会去掉引用来推导。

2.返回值推导
(1)c++11写法

template<typename Container, typename Index>
auto authAndAccess(Container& c, Index i)
-> decltype(c[i]) {  // 推导类型为int&而非int!!!
  authenticateUser();
  return c[i];
}

(2)c++14写法1(单纯去掉decltype)(和上面有区别)

template<typename Container, typename Index>
auto authAndAccess(Container& c, Index i) 
{
  authenticateUser();
  return c[i];  // 推导类型为int!!!
}

(3)C++14写法2(和c++11真正等价的写法)

template<typename Container, typename Index>
decltype(auto) authAndAccess(Container& c, Index i) 
{
  authenticateUser();
  return c[i];  // 推导类型为int&!!!
}

decltype(auto) 的用法中,auto指定了想实施推导的型别,而推导过程采用的是decltype的规则(区别于写法一的普通auto规则)。
(4)再探decltype(auto) 和auto的区别:

Widget w;
const Widget& cw = w;
auto myWidget1 = cw; // auto推导规则,myWidget1型别是Widget
decltype(auto) myWidget2 = cw; // decltype推导规则,myWidget2型别是const Widget& !!!

(5)但这个C++14写法2有种弊端,就是它不用用来推导右值类型,因为右值是不能绑定到左值引用的(除非是对常量的左值引用)
例如下面这种情况就不行:

std::deque<std::string> makeStringDeque(); // 工厂函数!
// 制作makeStringDeque返回的deque的第5个元素的副本。
auto s = authAndAccess(makeStringDeque(), 5);

所以应该是以万能引用+std::forward完美转发的形式去实现:
支持右值的版本:

// C++14版本decltype(auto)
template<typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c, Index i) 
{
  authenticateUser();
  return std::forward<Container>(c)[i];
}

// C++11版本
template<typename Container, typename Index>
auto authAndAccess(Container&& c, Index i) 
-> decltype(std::forward<Container>(c)[i]) {
  authenticateUser();
  return std::forward<Container>(c)[i];
}

3.对于左值表达式而言,decltype推导的类型是一个左值引用!!

decltype(auto) f1() {
  int x = 0;
  return x;  // decltype(x)是int,所以f1返回的是int
}

decltype(auto) f1() {
  int x = 0;
  return (x);  // decltype((x))是int&,所以f2返回的是int& !!!
}

条款4 掌握查看型别推导结果的方法

查看型别推导结果分为三个阶段:撰写代码阶段、编译阶段、运行时阶段

1.IDE编码器(鼠标悬停在auto上)

const int theAnswer = 42;
auto x = theAnswer;  // IDE显示 int
auto y = &theAnswer; // IDE显示const int*

2.编译器诊断信息
使用某个型别导致编译错误,报错信息里会有型别(适用于复杂的型别现身)

template<typename T>
class TD; // 只声明不定义去诱发错误信息

TD<decltype(x)> xType; // 错误信息会告诉TD<int>
TD<decltype(y)> yType; // 错误信息会告诉TD<const int*>

3.运行时输出
(1)使用typeid

std::cout << typeid(x).name() << '\n';
std::cout << typeid(y).name() << '\n';

typeid结构的name成员变量是const char*类型
对于GNU和Clang编译器,会显示x的型别是“i”(代表int),y的型别是“PKi”(代表pointer to konst/const),微软则是intint const*
(2)typeid方式不可靠,因为它推导型别的处理方式就和按值传递形参一样。
比如:

template<typename T>
void f(const T& param); // f是打算调用的函数模板
std::vector<Widget> createVec(); // 工厂函数
const auto vw = createVec(); // 使用工厂函数的返回值初始化w
if (!vw.empty()) {
  f(&vw[0]); // 调用f
}

推导:

template<typename T>
void f(cosnt T& param) {
  using std::cout;
  cout << "T = " << typeid(T).name() << '\n';
  cout << "param = " << typeid(param).name() << '\n';
}

// GNU和Clang显示结果:
T = PK6Widget (6是后面的字符数)
param = PK6Widget
// 微软编译器的结果
T = class Widget const *
param = class Widget const *

此时发现T和param的型别都是Widget const *,主要原因是typeid方式不可靠,因为它推导型别的处理方式就和按值传递形参一样(见条款一的第三小条),引用型会被忽略。当忽略引用性后,const和volatile也会被忽略。
(3)运行时可借助Boost.TypeIndex去精确推导
使用方法:

#include <boost/type_index.hpp>
template<typename T>
void f(const T& param) {
  using std::cout;
  using boost::typeindex::type_id_with_cvr;
  // 这里prettty_name是一个std::string类型
  cout << "T = " << type_id_with_cvr<T>().pretty_name() << '\n';
  cout << "param = " << type_id_with_cvr<decltype(param)>().pretty_name() << '\n';
}

// GNU和Clang
T = Widget const*
param = Widget const* const&
// 微软
T = class Widget const*
param = class Widget const* const&

Boost.TypeIndex不会移除引用饰词、const、volatile。


2 auto


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

1.在模板中使用auto,可以避免使用一些费解的traits。

template<typename It>
void dwim(It b, It e) {
  while (b != e) {
    // 使用traits
    typename std::iterator_traits<It>::value_type currValue = *b;
    // 换成使用auto
    auto currValue = *b;
  }
}

2.auto很方便的和闭包函数结合。
可以先了解什么是闭包函数,有状态(比如包含某变量)的函数,主要包括(1)类函数,(2)修改某变量的lambda,(3)使用bind

// auto方便的和lambda结合
auto derefUPLess = 
  [](const std::unique_ptr<Widget>& p1,
     const std::unique_ptr<Widget>& p2) {
  return *p1 < *p2;
};

上面的这种形式也可以使用std::function

// 使用std::function
std::function<bool(const std::unique_ptr<Widget>&,
                   const std::unique_ptr<Widget>&)> derefUPLess = 
  [](const std::unique_ptr<Widget>& p1,
     const std::unique_ptr<Widget>& p2) {
  return *p1 < *p2;
};

绑定闭包lambda时,使用auto和使用std::function的区别:
(1)auto所需的内存量和该闭包一样;但使用std::function声明的变量是具有固定尺寸的内存,它是std::function的一个实例,如果这个固定尺寸不够用,它还会在堆上分配内存来存储该闭包。通常std::function申请的内存会比auto大。
(2)使用std::function,编译器通常会限制内联,所以调用会比auto更慢。
(3)书写方便。
另外上述例子使用还可以在函数形参上进一步使用auto,实现了类似模板推导的功能(详情见条款33):

// auto方便的和lambda结合
auto derefUPLess = 
  [](const auto& p1, // 可以应用于任何类似指针之物。
     const auto& p2) {
  return *p1 < *p2;
};

3.可以避免一些型别捷径的问题。
如问题1:

std::vector<int> v;
unsigned sz = v.size();

使用unsigned看似没问题,但其实在windows32中unsigned和std::vector<int>::size_type都是32位,但在windows64中,unsigned是32位,但std::vector<int>::size_type是64位的,所以可能会导致移植问题,此时使用auto可以避免这样的问题。
问题2:

std::unordered_map<std::string, int> m;
for (const std::pair<std::string, int>& p : m) {
  ...
}

std::unordered_map的键值部分是const,所以这里std::pair的型别是std::pair<const std::string, int>,而非 std::pair<std::string, int>
所以编译器会对m中的每个对象都创建一个临时对象,然后用p引用绑定这个临时对象,这会造成拷贝,以及可能有指涉对象的指针为临时对象指针的问题。
4.重构方便,auto型别可以随着初始化表达式的型别改变而改变。

缺点:代码可读性问题?但其实大多数时候我们只需要知道某个对象是个容器或智能指针就行了,不需要知道它具体是那种型别的容器或智能指针。


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

1.auto不适用于推导可隐形转换的代理型别表达式。

Widget W;
// feature(w)[5]返回的是一个引用的代理表达式std::vector<bool>::reference,然后隐式转换为bool。
bool highPriority = feature(w)[5];
// highPriority直接被auto推导为了std::vector<bool>::reference
auto highPriority = feature(w)[5];
processWidget(w, highPriority);

vector
注意vector<bool>很特殊,他的返回值并不是一个元素的引用,而是std::vector<bool>::reference的对象,它可以隐式转换为bool,之所以这样实现,是因为C++禁止bit位的引用,所以使用了std::vector<bool>::referencestd::bitset::reference同理)。
它的一种实现是,该reference对象拥有一个指针,指涉到一个临时对象的机器字的指针,和在该机器字上对应的偏移量。
所以,对features的调用会返回一个std::vector<bool>型别的临时对象temp,然后(auto)highPriority是std::vector<bool>::reference的一个副本,highPriority含有一个指涉到temp的机器字的指针和第5个比特所对应的机器字的偏移量,所以highPriority是一个空悬指针,因此在对processWidget调用时将会出现未定义行为。

类似的情况还出现在C++的一些为了提高数值计算的效率的库中,比如Matrix型别对象的m1、2、3、4求和。

Matrix sum = m1 + m2 + m3 + m4;

两个Matrix对象的operator+会返回一个代理类对象Sum<Matrix, Matrix>,所以如果这里使用auto会生成一个Sum<Sum<Sum<Matrix, Matrix>, Matrix>, Matrix>这样的型别,它可以隐式转换为Matrix,但是使用auto会获得原始的型别。
因此需要避免写出这样的代码:

auto someVar = “隐形”代理型别表达式;

2.使用显示转换来解决代理型别表达式的auto问题。

auto highPriority = static_cast<bool>(features(w)[5]);
auto sum = static_cast<Matrix>(m1 + m2 + m3 + m4);

显式转换和auto结合还可以用在其他地方,如处理数值精度转换的问题上。

double calEpsilon();
auto ep = static_cast<float>(calEpsilon());

3 转向现代C++


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

一般情况下,初始化时用等号和用小括号等价。
下面几点中,1-4是使用大括号的优点(因为这些优点所以又叫统一初始化,Uniform Initialization),5是使用大括号的缺点。
1.大括号可以用来为非静态成员指定默认初始化值(小括号不可以)

class Widget {
private:
int x{0}; // 可行
int y = 0; // 可行
int z(0); // 不可行!!
};

2.大括号可以用来初始化不可复制的对象(等号不可以初始化不可复制的对象!

std::atomic<int> ai1{0}; // 可行
std::atomic<int> ai2(0); // 可行
std::atomic<int> ai3 = 0; // 不可行!!

等号是复制初始化,小括号/大括号是直接初始化。直接初始化一般会调用普通构造函数,而复制初始化也可以调用普通的构造函数,只是不能调用带explicit的普通构造函数,且在不能复制的对象中也不能调用复制初始化。
3.大括号会禁止隐式窄化型别转化,其他符号不会(可以是优点)

double x, y, z;
int sum1{ x + y + z }; // 大括号禁止了double到int的隐式窄化型别转化。

int a, b, c;
double sum2{ x + y + z }; // 大括号没有禁止int到double的隐式宽化型别转化。

4.大括号可以避免解析语法免疫(most vexing parse)的问题,这个问题在没有形参的小括号初始化中存在!

Widget w1(10); // 可行
Wiget w2{}; // 可行,调用没有形参的构造函数。
Wiget w3(); // 不可行,C++会把他当做声明。

5.只要有一个构造函数有std::initializer_list型别的形参,那大括号初始化都会强烈的优先选用std::initializer_list的版本!
(1)1)即使是其中有元素会发生隐式窄化型别转化,也会调用std::initializer_list的版本,除非找不到任何隐式转化时才会调用非std::initializer_list的版本)。
(1)2)即使是平时执行拷贝/移动构造函数也有可能被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}; // 调用std::initializer_list版本
Widget w3{10, 5.0}; // 调用std::initializer_list版本!!(隐式宽化型别转化)
Widget w4(w2); // 调用拷贝构造函数!
Widget w5{w2}; // 调用std::initializer_list版本
Widget w4(std::move(w2)); // 调用移动构造函数!
Widget w5{std::move(w2)}; // 调用std::initializer_list版本

即使存在隐式窄化类型转换也会调用std::initializer_list版本:

class Widget {
public:
  Widget(int i, bool b);
  Widget(int i, double d);
  Widget(std::initializer_list<bool> il); 
};
Widget w{10, 5.0}; // 错误,调用了std::initializer_list版本,但又不支持隐式窄化类型转换

除非是这样的实现,Widget(std::initializer_list<std::string> il); ,找不到任何的隐式类型转化,才会调用其他版本。
有一个特殊情况:空大括号表示的是“没有实参”,不会调用std::initializer_list,会调用默认构造函数。

class Widget {
public:
  Widget();
  Widget(std::initializer_list<int> il); 
};
Widget w1; // 调用默认构造函数。
Widget w2{}; // 调用的仍是默认构造函数!!
Widget w3(); // 令人讨厌的解析语法免疫问题,变成了声明语句!
Widget w4({}); // 这种形参为空的形式才会调用std::initializer_list版本。
Widget w5{{}}; // 这种形参为空的形式也会调用std::initializer_list版本。

(2)std::initializer_list这个问题很常见,即使在vector中都需要注意!!!

std::vector<int> v1(10, 20); // 10个元素的vector,元素值是20!!
std::vector<int> v1{10, 20}; // 调用std::initializer_list版本,元素值为10、20的vector!!

所以向类中添加std::initializer_list形参的构造函数时要尤其注意。
(3)若作为模板的设计者,选择小括号还是大括号需要考虑清楚:

template<typename T,
         typename... Ts>
void doSomeWork(Ts&&... params) {
   利用params构造局部对象T
}
T localObject(std::forward<Ts>(params)...); // 采用小括号
T localObject{std::forward<Ts>(params)...}; // 采用大括号

std::vector<int> v;
// 若上述实现采用小括号,则构造的是10个值为20的元素的vector;若是大括号,则是两个元素的vector!!
doSomeWork<std::vector<int>>(10, 20);

(4)扩展:更具弹性的设计是,模板的设计允许调用者自行决定从模板生成的函数内使用小括号还是大括号。(可见Andrzej的C++博客2013年6月5日的文章“Intuitive interface–Part I”)


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

1.0、NULL、nullptr的型别区别:
0的型别是int,NULL的型别是包含int、long等的整形,他们均不具备指针型别,语境中是勉强解释为空指针。
nullptr的优点在于,它不具备整形型别,虽然也不具备指针型别,真实型别是std::nullptr_t,但std::nullptr_t可以隐私转换到所有裸指针
2.所以引出一个问题,尽量不要在指针型别和整形型别之间做重载。

void f(int);
void f(bool);
void f(void*);
f(0); // 调用的是f(int),而不是f(void*)
f(NULL); // 可能通不过编译,但一般会调用f(int),从来不会调用f(void*)

3.尤其在设计auto变量的时候,指针尽量用nullptr,防止含义不清晰。

auto result = findRecord();
if (result == 0) // 不清晰
if (result == nullptr) // 清晰知道是指针

4.特别是在有模板型别推导的时候,需要用nullptr。

int f1(std::shared_ptr<Widget> spw);
double f2(std::unique_ptr<Widget> upw);
bool f2(Widget* pw);
std::mutex f1m, f2m, f3m;
using MuxGuard = std::lock_guard<std::mutex>;
{
  MuxGuard g(f1m);
  auto result = f1(0); // 暂时不会有问题,可以转为智能指针。
}
{
  MuxGuard g(f2m);
  auto result = f2(NULL); // 暂时不会有问题
}
{
  MuxGuard g(f3m);
  auto result = f3(nullptr); // 不会有问题
}
// 将以上用模板实现, c++11
template<typename FuncType,
         typename MuxType,
         typename PtrType>
auto lockAndCall(FuncType func,
                 MuxType& mutex,
                 PtrType ptr) -> decltype(func(ptr)) {
  MuxGuard g(mutex)
  return func(ptr)
};
// 将以上用模板实现, c++14
template<typename FuncType,
         typename MuxType,
         typename PtrType>
decltype(auto) lockAndCall(FuncType func,
                           MuxType& mutex,
                           PtrType ptr) {
  MuxGuard g(mutex)
  return func(ptr)
};

auto result1 = lockAndCall(f1, f1m, 0); // 错误!! ,形参ptr被定义为int
auto result2 = lockAndCall(f2, f2m, NULL); // 错误!!,形参ptr被定义为int
auto result3 = lockAndCall(f3, f3m, nullptr); // 没问题,形参ptr被定义为std::nullptr_t,可隐式转换。

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

1.别名声明using的优点:
(1)比较容易理解,特别是对于函数指针的别名而言。
(2)别名声明可以模板化,但是typedef不行。

template<typename T>
using MyAllocList = std::list<T, MyAlloc<T>>;
MyAllocList<Widget> lw;

如果上述代码要换成typedef,则需要下面的复杂实现:

template<typename T>
struct MyAllocList {
  typedef std::list<T, MyAlloc<T>> type;
};
MyAllocList<Widget>::type lw;

如果MyAllocList它容纳的对象是个模板形参的话,则需要加上typename前缀:

template<typename T>
class Widget {
private:
  typename MyAllocList<T>::type list;
};

这里的MyAllocList<T>::type是个带依赖性别(effective c++ 里叫做嵌套从属类型),需要加上typename,不然不知道是类型名还是变量名,因为有些特化版本可能会导致歧义,所以需要加上typename告诉编译器是类型名。
比如这个特化版本就可能导致问题:

class Wine {...};
template<>
class MyAllocList<Wine> {
private:
  enum class WineType {
    White, Red, Rose
  };
  WineType type;
};

2.注意C++11里的<type_traits>是用typedef实现的,需要加typename和从属名,C++14中才有_t的using版本。

std::remove_const<T>::type // 由const T生成T,C++11
std::remove_const_t<T> // C++14
std::remove_reference<T>::type // 由T&或T&&生成T
std::remove_reference_t<T> // C++14
std::add_lvalue_reference<T>::type // 由T生成T&
std::add_lvalue_reference_t<T> // C++14

C++14是这样实现using的<type_traits>的:

template<class T>
using remove_const_t = typename remove_const<T>::type;
template<class T>
using remove_reference_t = typename remove_reference<T>::type;
template<class T>
using add_lvalue_reference_t = typename add_lvalue_reference<T>::type;

条款10 优先选用限定作用域的枚举型别enum class,而非不限作用域的枚举型别

1.限定作用域枚举型别的优点:
(1)防止污染枚举类型所在的作用域
非限定作用域的枚举类型问题:

enum Color { black, white, red }; // black, white, red所在作用域和Color相同!!
auto white = false; // 错误。

限定作用域的枚举类型:

enum class Color { black, white, red };  // black, white, red在Color的作用域内!!
auto white = false; // 没问题
Color c = Color::White

(2)限定作用域的枚举类型可以进行前置声明(非限定作用域的枚举类型需指明底层类型后才可前置声明
因为非限定作用域的枚举类型,编译器不知道enum的底层型别,一般会节省内存,选择最小内存的(如用char),但有时候也会用空间换时间。
限定作用域的枚举类型默认底层型别为int,所以可以前置声明,当然,也可以修改底层型别:

// 声明式
enum class Status: std::uinit32_t;
// 或者直接在定义式中指出。
enum class Status: std::uinit32_t { good = 0, failed = 1 };

当指定型别后,不限定作用域的枚举类型也可以前置声明。

// 声明式
enum Color: std::uint8_t;
// 定义式同上

(3)限定作用域的枚举类型,它的枚举量是更强型别的,不能隐式转换为整数类型(不限定作用域的枚举类型可以隐式转换为int,从而转为浮点型别)
限定作用域的枚举类型强制类型转换时需要使用static_cast。

Color c = Color::red;
if (static_cast<double>(c) < 14.5) {} // 不自然的代码,但合法。

不能隐式转换这也是他的缺点:(比如在tuple的应用中)

using UserInfo = 
  std::tuple<std::string, // 名字
          std::string, // 电子邮件
          std::size_t>; // 声望值
UserInfo uInfo;
auto val = std::get<1>(uInfo); // 取用域1的值

当采用不限定枚举类型时,因为可以隐式转换,所以可以直接使用。

enum UserInfoFields { uiName, uiEmail, uiReputation };
UserInfo uInfo;
auto val = std::get<uiEmail>(uInfo); // 不需要任何显示转换。

但使用限定作用域的枚举类型时,必须用static_cast:

enum class UserInfoFields { uiName, uiEmail, uiReputation };
UserInfo uInfo;
auto val = std::get<static_cast<std::size_t>(UserInfoFields::uiEmail)>(uInfo); 

注意,std::get是个模板,模板内的参数需要在编译期计算出结果。
这里限制了转换的型别是std::size_t,要想写个通用的函数,则有一点麻烦了,因为必须在编译期计算出结果,函数需要用constexpr。
而且,为了配合任意枚举类型,我们还需要获得底层型别,需要使用std::underlying_type这个traits去获取底层型别。最后,我们还需要把它声明为noexcept。
所以使用方式为:(C++11写法)

template<typename E>
constexpr typename std::underlying_type<E>::type
  toUType(E enumerator) noexcept {
  return static_cast<typename std::underlying_type<E>::type>(enumerator);
}

结合条款9(使用traits的using版本)和条款3(使用auto返回值),可以更精简:

template<typename E>
constexpr auto
  toUType(E enumerator) noexcept {
  return static_cast<std::underlying_type_t<E>>(enumerator);
}

使用时仍然比不限定作用域的枚举类型繁琐一些:

auto val = std::get<toUType(UserInfoFields::uiEmail)>(uInfo);

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

结合Effective C++ 第6条。
C++输入输出流不可复制复制,在C++98是将基类basic_ios的拷贝和赋值函数声明为private(编译过程阻止),且不定义它的实现(链接过程阻止友元和成员函数调用),在C++11里是将它们定义为public的delete函数
(1)删除函数被声明为public的好处
1)当代码尝试使用某个函数时,C++会先校验它的可访问性,后校验删除状态;如果声明为public,可以得到更好的错误提示信息。
(2)任何函数都可以声明为删除函数(不一定是类里),但只有成员函数可以声明为private。
考虑以下一个普通函数:

// 定义参数为int的函数,但可以传入char、bool、float/double
bool isLucky(int number);
// 定义删除函数
bool isLucky(char) = delete;
bool isLucky(bool) = delete;
bool isLucky(double) = delete; // 会同时拒绝float和double型别!!!因为float找不到时会转换为double而不是int!!!

(3)删除函数可以阻止哪些不应该进行的模板具现(禁用特化版本)(不一定是类里)

template<typename T>
void processPointer(T* ptr);
// 一般指针类型的模板,会禁用void*(无法解引用、自增、自减)和char*(C风格字符串)!!
template<>
void processPointer(void* ptr) = delete;
template<>
void processPointer(char* ptr) = delete;
// 一般同时会禁用const 版本!!!
template<>
void processPointer(const void* ptr) = delete;
template<>
void processPointer(const char* ptr) = delete;
// 更精细化,还会禁用const volatile void* 和 const volatile char*,其他标准字符型别指针的重载版本,std::wchar_t、std::char16_t、std::char32_t

如果是类里的模板,也不能用private去禁用。原因是,不可能给予成员函数模板的某个特化(private)以不同于主模板的访问层级(public)
(4)delete还可以用于类外部/相同名字空间的函数

class Widget {
public:
  template<typename T>
  void processPointer(T* ptr) {}
};
template<>
void Widget::processPointer<void>(void*) = delete;

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

此章节可结合Effective C++的33条,无论函数是不是虚函数/纯虚函数/非虚函数,无论函数的参数怎样,只要名字相同,父类的函数都会被子类遮掩。
注意,改写动作发生的条件:
(1)基类的函数必须是虚函数
(2)基类和派生类的函数名字必须完全相同(析构函数除外)
(3)基类和派生类的函数形参型别必须完全相同
(4)基类和派生类的函数常量性必须完全相同
(5)基类和派生类的函数返回值和异常规格必须兼容
以上是C++98的要求,而C++11还有一条:
(6)基类和派生类的函数引用饰词必须完全相同。
扩展,引用饰词,类似于const成员函数,用于成员函数,但不必是虚函数。

class Widget{
public:
  void doWork() &; // 仅在*this是左值时调用。
  void doWork() &&; // 仅在*this是右值时调用。
};
Widget makeWidget(); // 返回右值
Widget w;
w.doWork(); // 以左值调用上面那个函数
makeWidget().doWork(); // 以右值调用下面那个函数

错误的改写:

class Base {
public:
  virtual void mf1() const;
  virtual void mf2(int x);
  virtual void mf3() &;
  void mf4() const;
};

class Derived: public Base {
public:
  virtual void mf1();
  virtual void mf2(unsigned int x);
  virtual void mf3() &&;
  void mf4() const;
}

上面四种改写都是错误的,而且有可能编译器没有提示。
要想编译器吹毛求疵的检查所有和改写有关的问题并提示,需要使用override。
PS:(1)override和final都是个语境关键字(contextual keyword),他们仅在出现在成员函数声明的末尾才有保留意义。所以声明一个override的名字的函数也不会有问题。
(2)final应用于虚函数,会阻止它在派生类中被改写。final也可用于一个类,类会被禁止作为基类。
(3)右值引用饰词的作用:可以调用右值的this对象,避免拷贝。

class Widget {
public:
  using DataType = std::vector<double>;
  // 注意这里DataType需要写为&
  DataType& data() & {
    return values;
  }
  // 注意这里DataType不需要写为&&
  DataType data() && {
    return std::move(values);
  }
private:
  DataType values;
};
auto vals1 = w.data(); // 调用左值重载版本,vals1使用复制构造函数完成初始化。
auto vals2 = makeWidget().data(); // 调用右值重载版本,vals2使用移动构造函数完成初始化!!

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

const_iterator是底层const。
在C++98前const_iterator不太好用,比如获得一个const_iterator需要强转(没有cbegin),插入一个元素只能用iterator,而且const_iterator没法强转为iterator。
C++11插入一个元素可以用const_iterator了,且可以使用cbegin、cend、rbegin、rend、crbegin、crend。
但C++11仍有一个缺陷,他没有cbegin、cend的非成员函数版本,仅添加了begin、end的非成员函数版本,这一点在C++14才解决。
(非成员函数有很多好处,如Effective C++23条,且C++11也对内建数组实现了非成员函数的begin、end版本,比较统一)

比如:

template<typename C, typename V>
void findAndInsert(C& container,
                   const V& targetVal,
                   const V& insertVal) {
  using std::cbegin;
  using std::cend;
  suto it = std::find(cbegin(container), // 这里只有C++14能用,C++11不行
                      cend(container),
                      targetVal);
  container.insert(it, insertVal);
}

在C++11中自己实现非成员函数的cbegin/cend:

template<class C>
auto cbegin(const C& container)->decltype(std::begin(container)) {
  // 推荐:
  return std::begin(container); // 会由decltype推导为const类型,begin非成员函数有内建数组的特化版本。
  // 不推荐:
  return container.cbegin(); // 用成员函数版本不太好,不适用于内建数组。
}

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

1.noexcept优点:
(1)接口设计中客户方关注的核心。
(2)可以让编译器生成更好的目标代码。

int f(int x) throw();  // f不会发射异常,C++98风格,优化不够
int f(int x) noexcept;  // f不会发射异常,C++11风格,最优化

C++98下,调用栈会开解到f的调用方,然后执行一些与本函数无关的动作以后,程序执行终止。
但在C++11的异常规格下,程序终止之前,栈只是可能会开解,所以编译器便可对此优化。这一点区别可能对代码生成影响很大,在noexcpet声明的函数中,优化器不需要再异常传出函数的前提下,将执行期栈保持在可开解状态;也不需要在异常逸出函数的前提下,保证所有其中的对象以其被构造顺序的逆序完成析构。
2.std::vector中的异常安全
vector的push_back,当capacity和size相等时,vector会分配一个新的更大的内存块来存储其元素,然后把元素从现在的内存块转移至新的。
在C++98中,是通过复制的方式,先复制到新内存,再析构老内存,这使得push_back是强异常安全保证的。如果复制元素的中途发生异常,老内存的对象不会被改变。
移动构造的缺点:在C++11中,可使用移动构造函数,但在移动的过程中发生异常,原始对象已经被修改,则不能再恢复原样。所以它需要知道内部元素的移动不需要发生异常(需要加上noexcept或者throw()声明才能使用移动)
C++11的策略是能移动则移动,必须复制才复制。在C++11中,std::vector::push_back在空间不足时会调用std::move_if_noexcept,后者是std::move的一个变体,它会在一定条件下不强制转换至右值(条款23),取决于型别的移动函数是否含有noexcept声明。具体检测方式是,std::move_if_noexcept会向std::is_nothrow_move_constructible求助,这个模板特征的值由移动构造函数是否指定了noexcept或者throw()来设置。
类似的除了std::vector::push_back,还有std::vector::reverse、std::deque::insert。
3.swap中的异常安全
swap运用非常广泛,它是许多stl中的核心组件。所以更需要noexcept声明。
而swap某个对象是否有noexcept声明,取决于用户自定义的 这个对象内部的对象的 swap是否带有noexcept声明。(高阶数据结构的swap型别的noexcept性质,取决于低阶数据结构的swap是否具有noexcept性质
比如数组,比如std::pair

// 比如要对Widget数组进行swap,它是否具有noexcept的形式取决于内部元素的交换是否是noexcept。
template<class T, size_t N>
void swap(T (&a)[N],
          T (&b)[N]) noexcept(noexcept(swap(*a, *b)));
// pair的swap是否noexcept取决于first和second的swap是否noexcept
template<class T1, class T2>
struct pair {
  void swap(pair& p) noexcept(noexcept(swap(first, p.first)) && 
                              noexcept(swap(second, p.second)));
};

4.是否要给函数加noexcept?
如果你确实能保证某个函数从不发射异常,你绝对应该为它加上noexcept声明。
大部分函数是异常中立的(exception-neutral),则看加上noexcept的收益是否更高。
标准库容器中的移动操作的接口规格并不带有noexcept声明。实际上,我们可以自己为一部分容器加上noexcept声明。
但不要为了加noexcept声明而本末倒置,写很多扭曲的实现代码(比如捕获所有异常,将其替换为状态码,或者特殊返回值)。
5.析构函数一般不会发射异常。
析构函数发射异常是一种差劲的编码风格,甚至在C++11中成为了一条语言规则。
默认地,内存释放函数和所有析构函数(无论自定义还是编译器自动生成的)都默认具备noexcept性质
析构函数发射异常的唯一场合,就是所在类的数据成员的型别显示将其析构函数声明为可能发射异常的(noexcept(false)),这种场合很少见,标准库是没有的。
6.一般库设计者会把函数区分为宽松契约和狭隘契约的种类。
宽松契约的函数一般可以加上noexcept声明。
但狭隘契约的函数一般对前置条件有要求,比如一个f函数调用一个string,而string限制了不超过32个字符,那在设计时可能会对超过32个字符的情况发射一个“前置条件已违例”的异常,所以一般不加noexcept声明。
7.即使内部函数没有声明noexcept,该函数也可以加上noexcept

void setup(); // 没有声明noexcept
void cleanup(); // 没有声明noexcept

// doWork也可以加上noexcept声明。
void doWork() noexcept {
  setup();
  cleanup();
}

有的函数不加noexcept声明是有原因的。这一点其实在C++使用C语言撰写的库(即使已经移动至std空间,比如std::strlen就不带noexcept声明)的时候,经常会遇到很多函数没有加noexcept声明。


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

对于constexpr变量来说,是在编译期就已知。const可能是运行期才已知,它并不一定是由编译期已知值来初始化。
但对于constexpr函数来说,是否在编译期已知取决于函数的参数是否在编译期已知。
所有的constexpr对象都是const对象,但不是所有的const对象都是constexpr对象。
1.constexpr变量
在编译期已知的对象可以存在只读内存中,它可以用在比如数组的尺寸规格,整形模板实参(包括std::array型别对象的长度),枚举量的值,对齐规格。

int sz;
constexpr auto arraySize1 = sz; // 错误,sz的值在编译期未知。
std::array<int, sz> data1;  // 错误
constexpr auto arraySize2 = 10; // 正确
std::array<int, arraySize2> data1;  // 正确

2.constexpr函数
(1)在调用constexpr函数中,若有任意一个参数在编译期未知,则它的运作方式和普通函数无异。可以利用这个性质避免写两个函数,constexpr可以满足所有需求。
(2)用constexpr函数可以在编译期做很多事,比如计算乘方。

// 如果base、exp是编译期的量,则pow的返回结果就可以当做编译期量使用!!
constexpr int pow(int base, int exp) noexcept {}
constexpr auto numConds = 5;
std::array<int, pow(3, numConds)> results;

在C++11中,constexpr函数不得包含多于一个可执行语句,即一条return语句。需要用条件运算符代替if-else,用递归代替循环。

constexpr int pow(int base, int exp) noexcept {
  return (exp == 0 ? 1 : base * pow(base, exp - 1));
}

在C++14中,没有上述限制。

constexpr int pow(int base, int exp) noexcept {
  auto result = 1;
  for (int i = 0; i < exp; ++i) result *= base;
  return result;
}

(3)constexpr函数仅限于传入和返回字面型别
在C++11中,所有的内建型别,除了void外,都符合这个条件。
对于自建型别来说,它的构造函数(和其他需要用到的成员函数)也需要是constexpr函数,才是字面型别。

// C++11
class Point {
public:
  constexpr Point(double xVal = 0, double yVal = 0) noexcept
  : x(xVal), y(yVal) {}
  constexpr double xValue() const noexcept { return x; }
  constexpr double yValue() const noexcept { return y; }
  // C++11中修改内部变量的函数不能是constexpr,constexpr首先是const成员函数。
  void setX(double newX) noexcept { x = newX; }
  void setY(double newY) noexcept { y = newY; }
private:
  double x, y;
};

constexpr Point p1(9.4, 27.7); // 编译期已知。
constexpr Point p1(28.8, 5.3); // 编译期已知。
constexpr Point midpoint(const Point& p1, const Point& p2) noexcept {
  return { (p1.xValue() + p2.xValue()) / 2, 
           (p1.yValue() + p2.yValue()) / 2 };
}
constexpr auto mid = midpoint(p1, p2); // p1和p2是编译期已知,则midpoint返回结果也在编译期已知。

在C++14中,void也可以是字面型别,且constexpr函数不一定是const函数。

// C++14
class Point {
public:
  ...
  constexpr void setX(double newX) noexcept { x = newX; }
  constexpr void setY(double newY) noexcept { y = newY; }
  ...
};

// reflection即使修改了变量,也可以是constexpr。
constexpr Point reflection(const Point& p) noexcept {
  Point result;
  result.setX(-p.xValue());
  result.setY(-p.yValue());
  return result;
}

constexpr Point p1(9.4, 27.7);
constexpr Point p1(28.8, 5.3);
constexpr auto mid = midpoint(p1, p2);
constexpr auto reflectedMid = reflection(mid); // 编译期已知!!

(4)在接口设计中,constexpr返回值是接口的一部分,要尽可能避免以后会修改,否则改了之后可能导致客户代码编译不通过(编译期未知)。


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

我们常常会借助mutable用于const对象中,作为缓存的某个成员(可能会在过程中修改它的值)。

class Polynominal {
public:
  using RootsType = std::vector<double>;

  RootsType roots() const {
    if (!rootsAreVlid) { // 如果缓存无效。
      ...                // 则计算根,并将其存入rootVals。
      rootsAreValid = true;
    }
    return rootVals;
  }
private:
  mutable bool rootsAreValid{ false };
  mutable RootsType rootVals{};
};

如果有两个线程都调用这个root()成员函数,则有可能会造成数据竞险(data race)。
解决方式:
1.引入一个mutex互斥量。

class Polynominal {
public:
  using RootsType = std::vector<double>;

  RootsType roots() const {
    std::lock_guard<std::mutex> g(m); // 加上互斥量
    if (!rootsAreVlid) { // 如果缓存无效。
      ...                // 则计算根,并将其存入rootVals。
      rootsAreValid = true;
    }
    return rootVals;
  } // 解除互斥量
private:
  mutable std::mutex m;
  mutable bool rootsAreValid{ false };
  mutable RootsType rootVals{};
};

mutex缺点:
(1)只能移动,不能复制,导致原对象Polynomial失去了可复制性。
(2)开销较大,当只有一个要求同步的变量或内存区域时,可以用atomic更高效。
2.使用atomic去计算调用次数。

class Point {
public:
  ...
  double distanceFromOrigin() const noexcept {
    ++callCount;
    return std::sqrt((x * x) + (y * y));
  }
private:
  mutable std::atomic<unsigned> callCount{ 0 };
  double x, y;
};

atomic用于单个要求同步的变量或区域时比较高效。
缺点:但如果用于前面roots计算的场景,使用两个atomic的时候,则可能导致一些不正确的问题(因为我们需要他们两个一起改变或者不改变),所以对于两个和更多个变量或内存区域需要作为一个整体单位进行操作时,需要使用互斥量。
当前的系统,const成员函数越来越多的场景都是在并发条件下执行,需要考虑线程安全。


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

特种成员函数是指那些C++编译器会自动生成的函数,生成的特种成员函数是inline且public的。(一般是非虚的)
当类中有显示声明某个同种类型的函数时,编译器不会自动生成默认版本。
移动/赋值操作作用的是非静态成员
“移动操作”并不等于最终是移动真的会发生,核心在于它是用std::move作用于每一个移动源对象std::move的返回值被用于函数的重载决议,然后决定是否是移动还是复制操作。成员可被分为两部分:一部分支持移动操作的成员上执行移动操作,另一部分是在不支持移动操作的成员上执行复制操作。
C++11可以通过default显示的表示编译器需要自动生成这些函数。
两种拷贝操作是独立的,声明一个不应影响另一个自动生成,但C++11基本都会把另一个的自动生成当做被废弃的行为。
两种移动操作不是独立的,声明一个会影响另一个的生成。

总而言之,C++11关于特种成员函数的机制如下:
1.默认构造函数:与C++98的机制相同,仅当类中不包含用户声明的构造函数时才生成
2.析构函数:与C++98的机制基本相同,唯一的区别在于析构函数默认为noexcept(条款14)。与C++98的机制相同,仅当基类的析构函数是虚的,派生类的析构函数才是虚的。
3.复制构造函数:运行期行为与C++98相同:按成员进行非静态数据成员的复制构造。仅当类中不包含用户声明的复制构造函数时才生成。如果改类声明了移动操作,则复制构造函数将被删除。在已经存在复制赋值运算符或析构函数的条件下,仍然生成复制构造函数已经成为了被废弃的行为。
4.复制构造函数:运行期行为与C++98相同:按成员进行非静态数据成员的复制赋值。仅当类中不包含用户声明的复制赋值函数时才生成。如果改类声明了移动操作,则复制赋值函数将被删除。在已经存在复制构造函数或析构函数的条件下,仍然生成复制赋值函数已经成为了被废弃的行为。
5.移动构造函数和移动赋值运算符:都按成员进行非静态数据成员的移动操作。仅当类中不包含用户声明的复制操作或移动操作或析构函数时才自动生成(只要三个有一个,这两种移动函数都不会自动生成)。

另外,成员函数模板不会阻止编译器生成任何特种函数(假设支配其生成的条件都得到了满足),即使这些模板的具现结果生成了复制构造函数或赋值运算符的签名!!

// 编译器会始终生成Widget的复制和移动操作。
class Widget {
  ...
  template<typename T>
  Widget(const T& rhs);
  template<typename T>
  Widget& operator=(const T& rhs);
};

4 智能指针

裸指针的缺点:
1.声明中并不知道裸指针指向的是单个对象还是一个数组。
2.裸指针的声明中看不出来它是否拥有这个对象,也就不知道是否要析构它。
3.不知道如何析构它。
4.和1类似的原因,不知道应该使用delete还是delete[]。
5.只要少在某一个分支的路径少忘记析构,就可能导致资源泄露;如果多析构了一次,则会导致未定义的行为。
6.没有正规的方式检测指针是否是空悬的(dangle)。
智能指针包括std::auto_ptrstd::unique_ptrstd::shared_ptrstd::weak_ptr
其中std::auto_ptr是从C++98中残留下来的弃用特性。因为C++98没有移动语义,所以std::auto_ptr是用复制操作代替移动语义的。会导致令人异常的代码(std::auto_ptr对象执行复制操作会将其值置空)和令人烦恼的使用限制(不能在容器中存储std::auto_ptr)。


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

std::unique_ptr是个只移型别,资源的析构是通过对std::unique_ptr内部的裸指针调用delete完成的。
std::unique_ptr可以自定义删除器。比如实现一个工厂函数。

class Investment{};
class Stock: public Investment{};
class Bond: public Investment{};
class RealEstate: public Investment{};

// makeInvestment的实现。
// 先定义一个删除器,使用lambda
auto delInvmt = [](Investment* pInvestment) {
  makeLogEntry(pInvestment);
  delete pInvestment;
};
// 定义一个工厂函数(模板形式)
template<typename... Ts>
std::unique_ptr<Investment, decltype(delInvmt)>
makeInvestment(Ts&&... params) {
  std::unique_ptr<Investment, decltype(delInvmt)>
    pInv(nullptr, delInvmt);
  if () {
    pInv.reset(new Stock(std::forward<Ts>(params)...));
  } else if () {
    pInv.reset(new Bond(std::forward<Ts>(params)...));
  } else {
    pInv.reset(new RealEstate(std::forward<Ts>(params)...));
  }
  return pInv; // ??可以直接return不用release吗。
}
// 另,C++14中可以用auto代替。
template<typename... Ts>
auto
makeInvestment(Ts&&... params) {}

// 调用方式:
{
  auto pInvestment = makeInvestment{ arguments };
}

删除器的使用:
(1)所有的删除函数都需要接收一个指涉到欲析构对象的裸指针。
(2)使用std::unique_ptr和裸指针尺寸基本相同;
但如果析构器是函数指针,则需要增加一到两个字长(字长大小取决于是多少位的计算机,64位则为64bits)的尺寸
但如果析构器是函数对象,则带来的尺寸变化取决于函数对象存储了多少状态;
但无状态的函数对象(如无捕获的lambda),不会增加任何的尺寸,所以使用无捕获的lambda表达式来实现是很好的选择。

注意数组形式的std::unique_ptr不提供提领运算符operator*operator->,单个对象形式不提供索引运算符operator[]

除此之外,std::unique_ptr还有很方便的一点是,可以高效的转换为std::shared_ptr

std::shared_ptr<Investment> sp = makeInvestment(arguments);

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

1.引用计数
std::shared_ptr的构造函数会增加引用计数,析构函数会减少引用计数,复制赋值运算符会减少左边的引用计数,增加右边的引用计数。
引用计数的性能影响:
(1)std::shared_ptr的尺寸是裸指针的两倍,它内部包含一个指涉该资源的裸指针,和一个指涉该资源引用计数所在的控制块的裸指针。
(2)引用计数的内存必须动态分配。
(3)引用计数的递增和递减都是原子操作,一般引用计数只有一个字长。
std::shared_ptr的移动构造函数会将源std::shared_ptr置空,但不会对引用计数有任何操作。
2.std::shared_ptrstd::unique_ptr删除器的不同
(1)删除器的传入形式不同

auto loggingDel = [](Widget *pw) {
  makeLogEntry(pw);
  delete pw;
};
// 析构器的型别是智能指针型别的一部分。
std::unique_ptr<Widget, decltype(loggingDel)> upw(new Widget, loggingDel);
// 析构器的型别不是智能指针型别的一部分!!
std::shared_ptr<Widget> spw(new Widget, loggingDel);

(2)std::shared_ptr用在vector中时,可以指代不同的删除器(因为型别相同),std::unique_ptr则不行。

auto costomDeleter1 = [](Widget *pw) {};
auto costomDeleter2 = [](Widget *pw) {};
std::shared_ptr<Widget> pw1(new Widget, customDeleter1);
std::shared_ptr<Widget> pw2(new Widget, customDeleter2);
std::vector<std::shared_ptr<Widget>> vpw{ pw1, pw2 };

(3)自定义析构器不会改变std::shared_ptr的尺寸,而std::unique_ptr则可能改变。
无论析构器是什么型别,std::shared_ptr的对象尺寸都相当于裸指针的两倍。
原因是因为std::shared_ptr的控制块。
3.std::shared_ptr的控制块。
(1)std::shared_ptr的内部结构
std::shared_ptr<T>有一个指涉到T型别的对象的指针,和一个指涉到控制块的指针。
控制块包括 引用计数、弱计数、其他数据(例如自定义删除器的一个复制、自定义分配器的一个复制等)。
控制块分配在堆上,不属于std::shared_ptr的一部分
(2)控制块的创建规则和使用成本。
一个对象的控制块由创建首个指涉该对象的std::shared_ptr的函数来确定。
A.std::shared_ptr总是创建一个控制块。
B.从具备专属所有权的指针(std::unique_ptrstd::auto_ptr)触发构造一个std::shared_ptr时,会创建一个控制块,这时那个专属所有权的智能指针会被置空。
C.当std::shared_ptr构造函数使用裸指针作为实参来调用时,会创建一个控制块。
D.注意,如果std::shared_ptr的构造函数传递std::shared_ptr或者std::weak_ptr作为实参,则不会创建新的控制块。
(3)指涉的控制块占用的堆的尺寸通常只有几个字长,尽管自定义析构器和内存分配器可能会使其变得更大。
在使用默认析构器和默认内存分配器,并且std::shared_ptrstd::make_shared创建的前提下,控制块的尺寸只有三个字长,并且分配操作没有任何成本(这些成本被并入所指涉对象的内存分配中去了)
(4)控制块的实现较为复杂,使用了继承,甚至会用到虚函数。所以使用std::shared_ptr也有虚函数的使用成本,但它的虚函数机制一般只会被每个托管给std::shared_ptr的对象使用一次:在析构时使用
(5)提领一个std::shared_ptr的成本和提领一个裸指针成本差不多。
(6)进行复制赋值构造或者复制赋值,析构需要一个或多个原子化操作,成本会高一些,但一般都是单指令,不会高很多。

4.尽量不要在裸指针来构造std::shared_ptr

auto pw = new Widget;
// 可能会造成重复析构。
std::shared_ptr<Widget> spw1(pw, loggingDel);
std::shared_ptr<Widget> spw2(pw, loggingDel);

(1)尽可能避免将裸指针传入std::shared_ptr的构造函数,常用的替代首发是使用std::make_shared
(2)当使用自定义析构函数时,无法用std::make_shared,不可避免会用到裸指针,但应该直接传递new运算符的结果。

std::shared_ptr<Widget> spw1(new Widget, loggingDel);

5.std::enable_shared_from_this需要注意的点
shared_from_this的使用:

// 这种继承模式叫做CRTP(The Curiously Recurring Template Pattern,奇异递归模板模式)
class Widget: public std::enable_shared_from_this<Widget> {
public:
  void process();
};

std::shared_from_this函数或创建一个std::shared_ptr指涉该对象,但不会创建新的控制块。

void Widget::process() {
  processWidget.emplace_back(shared_from_this());
}

注意:std::shared_from_this的内部实现是查询当前对象的控制块,并创建一个std::shared_ptr指涉该控制块。实现这一点之前,就必须有一个指涉到当前对象的std::shared_ptr如果这样的std::shared_ptr不存在,则该行为未定义
为了避免在std::shared_ptr指涉到该对象前就调用了std::shared_from_this成员函数,继承自std::shared_from_this的类通常会把自己的构造函数定义为private访问层级,只允许用户通过工厂函数来创建对象(在工厂函数中构建一个shared_ptr,相当于提前构建了控制块)。

class Widget: public std::enable_shared_from_this<Widget> {
public:
  template<typename... Ts>
  static std::shared_ptr<Widget> create(Ts&&... params);
  void process();
private:
  ... // 构造函数
};

6.std::shared_ptr不能处理数组,没有std::shared_ptr<T[]>
也不要手动定义析构器来完成数组的删除,虽然能通过编译,但这个操作有很多缺点。(不能使用operator[],不能支持派生类指针到基类指针的转换)(注意:std::shared_ptr<T[]>也禁止派生类指针到基类指针的转换)。
代替方式是使用,std::arraystd::vectorstd::string


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

std::weak_ptr不影响std::shared_ptr的引用计数,也可以判断其所指涉的对象是否不符纯在。但std::weak_ptr并不是一种独立的智能指针,它不能提领,也不能检查是否为空,它一般是通过std::shared_ptr创建的。

auto spw = std::make_shared<Widget>();
std::weak_ptr<Widget> wpw(spw);
spw = nullptr; // 此时spw引用计数变为0,wpw空悬。

1.检测是否空悬的三种方式。
(1)expired

if (wpw.expired()) ...

这种方式只能检测是否失效,std::weak_ptr缺乏提领操作,即使有提领操作,也可能会导致data race。
需要一个原子操作来完成std::weak_ptr是否失效的校验,以及在未失效的条件下提供对所指涉对象的访问。
(2)lock
返回一个std::shared_ptr,如果std::weak_ptr已经失效,则std::shared_ptr为空

std::shared_ptr<Widget> spw1 = wpw.lock(); // 如果wpw失效,则spw1为空,需要检测spw1是否为空。
auto spw2 = wpw.lock(); // 和上面一样

(3)用std::weak_ptr来构造std::shared_ptr
如果std::weak_ptr已经失效,抛出异常(std::bad_weak_ptr

std::shared_ptr<Widget> spw3(wpw); // 如果wpw失效,抛出std::bad_weak_ptr异常

2.std::weak_ptr的三种用途
(1)实现缓存管理器
调用者决定这些对象的生存期,但缓存管理器也需要一个指涉这些对象的指针,并且它能够校验什么时候会空悬。
下面是缓存管理器的粗糙版本。

std::unique_ptr<const Widget> loadWidget(WidgetID id);
std::shared_ptr<const Widget> fastLoadWidget(WidgetID id) {
  static std::unordered_map<WidgetID, 
                           std::weak_ptr<const Widget>> cache;
  // objPtr的型别是std::shared_ptr,指涉到缓存的对象
  auto objPtr = cache[id].lock();
  //(如果对象不在缓存中,则返回空指针!!)
  if (!objPtr) {
    objPtr = loadWidget(id);
    cache[id] = objPtr;
  }
  return objPtr;
}

上面是一个粗糙版本,因为还有一个优化是在缓存的Widget不再有用时将其删除。
否则会造成缓存中失效的std::weak_ptr不断积累。
(2)观察者设计模式(Observer design pattern)
该模式包括 主题(subject,可以改变对象的状态),观察者(observer,对象状态发生改变后通知的对象)
在多数实现中,每个主题包含了一个数据成员,这个成员持有指涉到观察者的指针,这使得这些主题能够很容易地在其发生状态改变时发出通知。
主题不会控制其观察者的生存期(不关心它们什么时候被析构),但需要确认当一个观察者被析构后,主题不会去访问它。
一个合理的实现是让每个主题 持有一个容器来放置指涉其观察者的std::weak_ptr
(3)避免循环引用
比如A\C共享B的所有权。
A–>(shared_ptr)–>B<–(shared_ptr)<–C
为了使用方便,假设有一个指针从B指回A,这个时候应该用std::weak_ptr
因为如果用裸指针,则A被析构后,B检测不出来。
如果用shared_ptr,则会因为循环引用造成内存泄露。
而用std::weak_ptr,则B持有的指针不会影响A的计数。
PS:
如果是严格的继承谱系,其实父节点(B)到子节点(A)(父存子)可以用std::unique_ptr,而子节点到父节点(子存父)可以用裸指针,因为子节点的生存期不会比父节点更长。
但非严格继承谱系还是需要使用std::weak_ptr
3.std::weak_ptr的尺寸
std::weak_ptr的尺寸和std::shared_ptr相同,他们和std::shared_ptr有着相同的控制块。std::weak_ptr虽然不修改引用计数,但会操作弱引用计数,见条款21。


条款21 优先选用std::make_uniquestd::make_shared,而非直接使用new

std::make_shared是C++11的一部分,std::make_unique是C++14的一部分,但是也可以在C++11中自己实现make_unique(不要写在std中,方便以后升级C++14)

// 主要是做了完美转发
template<typename T, typename... Ts>
std::unique_ptr<T> make_unique(Ts&&...params) {
  return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
}

有三个make函数,都用了完美转发趋势线,make的第三个函数是std::allocate_shared,和std::make_shared一样,只不过它的第一个实参是用以动态分配内存的分配器对象。
1.使用make函数相对于使用new的优点:
(1)new会降被创建对象的型别写两遍,而make只用写一遍。

auto upw1(std::make_unique<Widget>());
std::unique_ptr<Widget> upw2(new Widget);

(2)有助于异常安全
这点std::make_sharedstd::make_unique都适用

void processWidget(std::shared_ptr<Widget> spw, int priority);
int computePriotity();
processWidget(std::shared_ptr<Widget>(new Widget), computePriority()); // 潜在的资源泄露
// 有可能会先new Widget,然后执行computePritotiy,最后运行std::shared_ptr构造
// 如果computePriority发生异常,则会导致动态分配的Widget内存泄漏。

使用std::make_shared可以避免问题。

// make_shared和computePriority肯定会有一个被首先调用
processWidget(std::make_shared<Widget>(),
              computePriority());

(3)性能的提升
std::make_shared分析同样适用于std::allocate_shared
这两个make函数只需要一次内存分配,会分配单块的内存既保存Widget对象又保存控制块,这种优化减少了程序的静态尺寸,还可以增加代码运行速度,减少内存碎片。
2.使用make函数相对于使用new的缺点:
(1)自定义析构器不能使用make函数

std::unique_ptr<Widget, decltype(widgetDeleter)>
  upw(new Widget, widgetDeleter);
std::shared_ptr<Widget> spw(new Widget, widgetDeleter);

(2)make内部模板使用了小括号进行完美转发,所以初始化不能直接传入大括号。

// 创建10个值为20的int组成的vector
// 而非一个10 20的int vector
auto upv = std::make_unique<std::vector<int>>(10, 20);
// 使用initializer_list需要显示指出
auto initList = { 10, 20 };
auto upv = std::make_unique<std::vector<int>>(initList);

(3)对于std::make_shared而言,还有以下两种场景会有问题。
A.自定义类的operator new和operator delete
待扩展std::allocate_shared的用法?
对于使用自定义类的operator new和operator delete,若会分配精确尺寸sizeof(Widget)的内存块,则不适用于std::shared_ptr那种所支持的自定义分配器(std::allocate_shared)和释放器(自定义析构器)。因为std::allocate_shared所要求的尺寸,除了动态分配对象的尺寸外,还需要加上控制块的尺寸。
B.对象的析构和内存的释放存在延迟(对于大内存对象,这种延迟可能无法忍受)
引用计数控制着对象的析构(但不控制对象的内存释放),弱引用计数控制着控制块的析构(也控制着总内存的释放)(?shared_ptr会影响弱引用计数吗)
若使用make_shared,则由于分配在同一块内存中,所以是一起释放的,因此在所有weak_ptr也为0的时候,才会释放内存。

class ReallyBigType{...};
auto pBigObj = std::make_shared<ReallyBigType>();
... // 创建指涉大对象的多个std::shared_ptr和std::weak_ptr
... // 最后一个指涉该对象的shared_ptr被析构,但指涉对象的若干weak_ptr仍存在
... // 这个阶段,大对象所占用的内存仍处于分配未回收状态!!
... // 最后一个指涉该对象的weak_ptr被析构,控制块和对象所占用的同一块内存在此释放

class ReallyBigType{...};
std::shared_ptr<ReallyBigType> pBigObj(new ReallyBigType);
... // 创建指涉大对象的多个std::shared_ptr和std::weak_ptr
... // 最后一个指涉该对象的shared_ptr被析构,但指涉对象的若干weak_ptr仍存在,大对象内存在此回收!!
... // 这个阶段,控制块的内存仍处于分配未回收状态
... // 最后一个指涉该对象的weak_ptr被析构,控制块占用的内存在此释放

3.特殊情况考虑

processWidget(std::shared_ptr<Widget>(new Widget, cusDel), computePriority()); // 非异常安全

这种情况由于computePriority存在,为了异常安全,不能用new;但又由于自定义分配器的存在,不能用make_shared_ptr,则需要这样实现:
可以将shared_ptr的构建和函数调用分离。

std::shared_ptr<Widget> spw(new Widget,cusDel);
processWidget(spw, computePriority());

由于上面进行了一次std::shared_ptr的拷贝,为了保证像直接调用一样有效率,可以使用移动std::move,此时引用计数不加减。

std::shared_ptr<Widget> spw(new Widget,cusDel);
processWidget(std::move(spw), computePriority());

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

1.定义
Pimpl方法是指pointer to implementation。
(1)普通指针的用法,是声明一个指针,可指向非完整型别。
(2)unique_ptr的用法,需要指向完整型别。(h文件声明析构函数(unique_ptr需要完整型别,析构器也需要),h文件声明但不定义移动构造函数(不声明会默认阻止自动的移动构造,但不可定义在h中),析构或移动构造定义放入cpp)
(3)shared_ptr的用法,可指向非完整型别

// 普通指针
class Widget {
public:
  Widget();
  ~Widget();
private:
  struct Impl;
  Impl *pImpl;
};

// 实现在cpp中
#include "widget.h"
#include "gadget.h"
#include <string>
#include <vector>
struct Widget::Impl {
  std::string name;
  std::vector<double> data;
  Gadget g1, g2, g3;
};
Widget::Widget() 
  :pImpl(new Impl) {}
Widget::~Widget() {
  delete pImpl;
}

2.unique_ptr的Pimpl方法
(1)错误用法:

class Widget {
public:
  Widget();
private:
  struct Impl;
  std::unique_ptr<Impl> pImpl;
};

// cpp
#include "widget.h"
#include "gadget.h"
#include <string>
#include <vector>
struct Widget::Impl {
  std::string name;
  std::vector<double> data;
  Gadget g1, g2, g3;
};
Widget::Widget() 
  :pImpl(std::make_unique<Impl>()) {}
}
// 客户代码
#include "widget.h"
Widget w; // 这里会报错!!

错误原因:(编译器的提示可能是在非完整型别上实施了sizeof或者delete)
具体原因是,因为我们没有声明析构函数,所以使用的是默认地析构器。默认析构器是在std::unique_ptr内部使用delete运算符来针对裸指针实施析构的函数。然而,在实施delete前,典型的实现会使用C++11中的static_assert去确保裸指针未指涉到非完整型别。
注意!!还因为std::unique_ptr的析构器是型别指针的一部分,这就导致指针所指对象必须要是完整型别(需要为析构函数生成代码)。
错误位置:一般会在析构函数调用中。因为默认析构函数是隐式inline的,所以会在Widget w生成的那一行代码为析构函数产生代码,而这一行就会显示static_assert错误。
(2)修改方式:手动声明和定义析构函数!

class Widget {
public:
  Widget();
  ~Widget(); // 区别部分
private:
  struct Impl;
  std::unique_ptr<Impl> pImpl;
};

// cpp
#include "widget.h"
#include "gadget.h"
#include <string>
#include <vector>
struct Widget::Impl {
  std::string name;
  std::vector<double> data;
  Gadget g1, g2, g3;
};
Widget::Widget() 
  :pImpl(std::make_unique<Impl>()) {}
}
Widget::~Widget() {} // 区别部分。
// 这里的实现文件也可以定义为default
Widget::~Widget() = default;

(3)声明析构函数会导致组织编译器自动生成移动操作,所以需要手动生成移动操作。
错误实现:

class Widget {
public:
  Widget();
  ~Widget();
  Widget(Widget&& rhs) = default; // 错误!
  Widget operator=(Widget&& rhs) = default; // 错误!
private:
  struct Impl;
  std::unique_ptr<Impl> pImpl;
};

注意,编译器在移动构造函数内抛出异常的时间中生成析构pImpl的代码,而对pImpl的析构也需要完整型别,所以不能在h文件里定义移动操作,需要移动到实现中。

class Widget {
public:
  Widget();
  ~Widget();
  Widget(Widget&& rhs); // 仅声明
  Widget operator=(Widget&& rhs);  // 仅声明
private:
  struct Impl;
  std::unique_ptr<Impl> pImpl;
};
// cpp文件
Widget::Widget() 
  :pImpl(std::make_unique<Impl>()) {}
}
Widget::~Widget() = default;
Widget(Widget&& rhs) = default; // 注意!!
Widget operator=(Widget&& rhs) = default;

注意:使用unique_ptr会使得类失去拷贝功能,需要手动去实现拷贝构造和拷贝赋值(深拷贝unique_ptr指向的内容)
3.shared_ptr的Pimpl方法

class Widget {
public:
  Widget();
private:
  struct Impl;
  std::shared_ptr<Impl> pImpl;
};
// cpp文件
Widget::Widget() 
  :pImpl(std::make_shared<Impl>()) {}
}
Widget w1;
auto w2(std::move(w1)); // 会自动生成移动构造函数,所以这里是移动!!

注意,std::shared_ptr不需要在Widget中声明析构函数了,所以也不会阻止移动操作,因为shared_ptr,可指向非完整型别
区别原因:std::unique_ptr,析构器型别是指针型别的一部分,虽然有更小的尺寸的运行期数据,和更快的运行期代码,但它只能指向完整型别
std::shared_ptr,析构器型别不是指针型别的一部分,有更大的尺寸的运行期数据,和更慢的运行期代码,但它可以指向非完整型别


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

移动语义使得编译器可以使用不那么昂贵的移动操作,并使得创建只移型别成为可能,比如std::unique_ptrstd::futurestd::thread等。
(1)std::move并不移动任何东西,只做强制型别转换。
(2)完美转发并不完美
(3)移动操作成本并不总是比复制低,即使低也不一定像期望那样低。
(4)移动操作能够成立的语境中,并不一定能够调用到移动操作。比如type&&这样的结构,并不总是表示右值引用。
(5)右值引用指向右值,但本身是左值。所以形参总是左值,即使其型别是右值引用(且指向右值)。比如void f(Widget&& w);,形参w是左值。


条款23 理解std::movestd::forward

1.std::move
(1)std::move的简化实现

// C++11
template<typename T>
typename remove_reference<T>::type&&
move(T&& param) {
  using ReturnType = 
    typename remove_reference<T>::type&&;
  return static_cast<ReturnType>(param);
}
// C++14
template<typename T>
decltype(auto) move(T&& param) {
  using ReturnType = remove_reference_t<T>&&;
  return static_cast<ReturnType>(param);
}

std::movestd::forward都不会生成任何可执行代码,连一个字节都不会生成,只是做了强制转换。
std::move是无条件强制将实参转换成右值,std::forward是在某个特定条件满足时才执行同一个强制转换。
std::move的形参是指涉到一个对象的万能引用,它的返回值是一个右值引用。右值引用返回后便是右值。

(2)如果想取得某个对象执行移动操作的能力,则不要将其声明为常量
std::move做的是强制类型转换,不是移动,而是告诉编译器该对象具备可移动的条件。
而且右值也只是在通常情况下可以移动,但有的时候只能拷贝,比如const右值,如下:

class Annotation {
public:
// 注意,此时text执行的是拷贝而非移动,std::move的结果是const std::string的右值!!
  explicit Annotation(const std::string text)
    : value(std::move(text)) {...}
private:
  std::string value;
};

上面例子的原因如下:

class string {
public:
  string(const string& rhs);
  string(string&& rhs);
};

由于std::move的结果是const std::string的右值,这个右值无法传递给移动构造函数,因为移动构造函数只能接收非常量的std::string型别的右值引用作为实参。可是这样一个右值却可以传递给复制构造函数,因为指涉到常量的左值引用允许绑定到一个常量右值型别的形参。
所以注意:
如果想取得某个对象执行移动操作的能力,则不要将其声明为常量,否则可能移动操作会被变换为复制操作。
std::move并不移动任何东西,甚至不保证经过其强制型别转换后的对象具备可移动的能力。

2.std::forward
std::forward一个典型的应用场景如下(用于转发型别):

void process(const Widget& lvalArg);
void process(Widget&& rvalArg);

template<typename T>
void logAndProcess(T&& param) {
  auto now = std::chrono::system_clock::now();
  makeLogEntry("Calling 'process'", now);
  process(std::forward<T>(param));
}
// 两种调用logAndProcess的场景
Widget w;
logAndProcess(w); // 传入左值
logAndProcess(std::move(w)); // 传入右值

因为所有函数形参均为左值(所以即使param是右值引用也是左值),所以需要std::forward来将param转换为右值。
因此std::forward可以用在param的实参是个右值的条件下,把param这个形参强制转换为右值型别。(区分param实参形参
注意:条款28指出原理:std::forward的任务是,当且仅当编码T中的信息表明传递给实参是个右值,即T的推导结果型别是个非引用型别时,对param这个左值实施到右值的强制转换!!
3.其他
所有引用都是左值,即使指向的可能是右值。所以传递给std::forward应当是一个非引用型别!!!
std::movestd::forward不能完全替代:
假设类中唯一非静态数据成员是一个随着移动构造操作而递增的静态计数器,移动构造函数可以以下面的方式实现:

// std::move可完美实现
class Widget {
public:
  Widget(Widget&& rhs)
    : s(std::move(rhs.s)) {
    ++moveCtorCalls;
  }
private:
  static std::size_t moveCtorCalls; // 移动的计数器
  std::string s;
};

// std::forward有局限性的实现
// 缺点:1.需要显示指明std::string.
//      2.传递给std::forward应当是一个非引用型别,否则可能会导致拷贝!!!(无法调用移动构造函数)
//      因为习惯上std::forward所传递的实参应该是个右值(引用是左值!)
//      而std::move消除了传递错误型别的可能性(比如传递std::string&)。
class Widget {
public:
  Widget(Widget&& rhs)
    : s(std::forward<std::string>(rhs.s)) {
    ++moveCtorCalls;
  }
private:
  static std::size_t moveCtorCalls; // 移动的计数器
  std::string s;
};

std::move所表达的意思是无条件的向右值型别的强制转换,而std::forward对绑定到右值的引用实施向右值型别类型的转换。
std::move为移动操作做铺垫,std::forward为转发做铺垫(不改变对象原始型别的左值性或右值性)。


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

1.万能引用和右值引用常见位置

void f(Widget&& param); // 右值引用
Widget&& var1 = Widget(); // 右值引用
auto&& var2 = var1; // 万能引用!!!
template<typename T>
void f(std::vector<T>&& param); // 右值引用
template<typename T>
void f(T&& param); // 万能引用

2.万能引用和右值引用的细分区别
(1)万能引用可以绑定左值,也可以绑定右值,还可以绑定const对象(可以保留const属性,其他一样)、volatile对象、const volatile对象(见条款1)
右值引用只能绑定右值,不能绑定左值。

// 万能引用
template<typename T>
void f(T&& param); // 万能引用
int x = 27;
const int cx = x;
const int& rx = x;
f(cx); // T的型别是const int&,param型别也是const int&
f(rx); // T的型别是const int&,param型别也是const int&
f(27); // T的型别是int,param型别是int&&

(2)要使一个引用成为万能引用,其涉及型别推导是必要条件,但不是充分条件。
形如“T&&”也是必要条件(包括auto和参数化模板)。
甚至在T&&上的const饰词都不能加,加了后也不是万能引用。

// 有型别推导,但是是右值引用
template<typename T>
void f(std::vector<T>&& param); // 右值引用
std::vector<int> v;
f(v); // 错误,右值引用不能绑定左值!!

// 加了cosnt饰词后是右值引用
template<typename T>
void f(const T&& param); // param是右值引用!!!!!

(3)但即使位于模板内的“T&&”,也有可能不是万能引用。

template<class T, class Allocator = allocator<T>>
class vector {
public:
  void push_back(T&& x); // 不是万能引用。
};
std::vector<Widget> v; // 因为声明vector这个class后,T就具象化了,所以上面是一个右值引用!!

(4)万能引用可以用于Args&&...args这种参数化模板

template<class T, class Allocator = allocator<T>>
class vector {
public:
  // Args是独立于vector的型别形参T的参数包。
  template<class... Args>
  void emplace_back(Args&&... x); // Args中每一个参数都是万能引用!!
};

(5)auto&&的都是万能引用,因为它自带型别推导。
除了1.中的例子以外,还有个只存在于C++14中的例子(C++14中,lambda表达式的形参可以用auto,详情见条款33,条款5.2也有用到)。

auto timeFuncInvocation = 
  [](auto&& func, auto&&... params) { // auto&&用于lambda的形参推导!!
  // forward+decltype的组合
  // 调用func,取用params。
  std::forward<decltype(func)>(func)(
    std::forward<decltype(params)>(params)); // 见条款33
};

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

1.尽量不要混用std::movestd::forward
当转发右值引用时,应当对其实施向右值的无条件强制型别转换(std::move),因为它们一定绑定到右值;而当转发万能引用时,应当对其实施向右值的有条件强制转换(std::forward)。
但条款23指出,即使对右值引用(形参)使用std::foward,也能硬弄出正确型别出来,但是代码啰嗦易错,比如实参传递右值引用,导致拷贝。
同时,也不要对万能引用使用std::move

// setName实现方式一:使用std::forward
class Widget {
public:
  Widget(Widget&& rhs)
  : name(std::move(rhs.name)),
  p(std::move(rhs.p)) {...}
  template<typename T>
  void setName(T&& newName) { // 使用万能引用
    name = std::forward<T>(newName);
  }
private:
  std::string name;
  std::shared_ptr<SomeDataStructure> p;
};

// setName实现方式二:使用std::move(无条件转换为右值)
class Widget {
public:
  template<typename T>
  void setName(T&& newName) { // 万能引用
    name = std::move(newName); // 可以编译,但糟糕
  }
private:
  std::string name;
  std::shared_ptr<SomeDataStructure> p;
};
std::string getWidgetName();
Widget w;
auto n = getWidgetName();
// 因为这默认是一个只读操作,而std::move后n是一个不确定的值。
w.setName(n);

// setName实现方式三:重载(避免在左值时对原值改动)
// 分别对常量左值和普通右值进行重载(此时常量左值才是使用的
class Widget {
public:
  void setName(const std::string& newName) {
    name = newName;
  }
  void setName(std::string&& newName) {
    name = std::move(newName);
  }
};

方式二的问题是:n是一个不确定的值。
方式三的问题是:需要编写和维护更多的代码;效率要打折扣。
效率打折扣的原因:
w.setName("Adela Novak");
这个情况下调用方式三,会重新构建std::string临时对象,随后该临时对象才会移入w的数据成员。
a.因此会多调用一次std::string的构造函数(以创建临时对象),一次std::string的移动赋值运算符(以移动newName到w.name),一次std::string的析构函数。
b.如果不是操作std::string,其他对象可能会更低效。
c.设计的可扩展性太差,当只要一个形参时,可能只需要两个重载就够了,但如果是n个参数,则需要2^n个重载函数。典型代表就是std::make_sharedstd::make_unique(C++14)因为它会使用可变参数模板:

template<class T, class... Args>
shared_ptr<T> make_shared(Args&&... args);
template<class T, class... Args>
unique_ptr<T> make_unique(Args&&... args);

2.如果你想要在单一函数内将某个对象不止一次地绑定到右值引用或万能引用,而且你想保证完成对该对象的其他所有操作之前,其值不被移走,那么就得仅在最后一次使用该引用时,对其实施std::move(右值引用),或std::forward(万能引用)
另外,注意条款14,某些情况需要用std::move_if_noexcept代替std::move(如vector空间不足时的移动)。
3.在按值返回的函数中,如果返回的是绑定到一个右值引用或一个万能引用的对象,则当你返回该引用时,应该使用(std::move、std::forward),这样可以使得空间复用

// 移动到函数的返回值存储位置
Matrix
operator+(Matrix&& lhs, const Matrix& rhs) {
  lhs += rhs;
  // 如果去掉std::move,会导致强制复制。
  return std::move(lhs);
}
// 原始对象如果是左值,那确实需要构造出实实在在的副本(复制)。
// 原始对象如果是右值,它的值应当被移动到返回值上(避免构造副本)
template<typename T>
Fraction
reduceAndCopy(T&& frac) {
  frac.reduce();
  return std::forward<T>(frac);
}

重要扩展:

(1)相同的std::move优化不要直接用在局部变量的返回上,因为编译器存在返回值优化RVO(return value optimization),直接在为函数返回值分配的内存上创建局部变量

// RVO返回值优化后,可以直接在为函数返回值分配的内存上(复制省略)
Widget makeWidget() {
  Widget w;
  return w;
}
// 反而导致没了RVO优化(因为这里返回的不是局部对象w,而是它的引用,不满足实施RVO的前提条件,限制了本来可用的编译器优化(可能导致多一次移动?)
Widget makeWidget() {
  Widget w;
  return std::move(w);
}

RVO优化的前提:1)局部对象型别和函数返回值型别相同。2)返回的就是局部对象本身。
(2)RVO的前提条件允许时,即使编译器最后不选择复制省略的策略,也会使用std::move隐式地实施于返回的局部对象上。(所以不用过多担心)
(3)对于具名的局部变量,会采用(2),对于不具名的局部变量(如函数参数),也可以直接按值返回,不用显示的写std::move

Widget makeWidget(Widget w) {
  return w;
}
// 编译器处理时,与下列代码等价
Widget makeWidget(Widget w) {
  return std::move(w);
}

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

emplace内部实现也用了forward传参。来看下面的emplace相关例子,分析调用的构造函数、复制、移动次数

std::multiset<std::string> names;
void logAndAdd(const std::string& name) {
  auto now = std::chrono::system_clock::now();
  log(now, "logAndAdd");
  names.emplace(name);
}

std::string petName("Darla");
logAndAdd(petName); // name实参和形参都是左值,调用了一次复制
logAndAdd(std::string("Persephone")); // name实参是右值,形参是左值,调用了一次构造,一次复制
logAndAdd("Patty Dog"); // name的实参是字符串常量右值,形参是左值,调用了一次构造,一次复制

借助完美转发实现后:

std::multiset<std::string> names;
template<typename T>
void logAndAdd(T&& name) {
  auto now = std::chrono::system_clock::now();
  log(now, "logAndAdd");
  names.emplace(std::forward<T>(name));
}

std::string petName("Darla");
logAndAdd(petName); // name实参和形参都是左值,左值完美转发,调用了一次复制
logAndAdd(std::string("Persephone")); // name实参是右值,形参是左值,右值完美转发,调用了一次构造,一次移动
logAndAdd("Patty Dog"); // name的实参是字符串常量右值,形参是左值,但emplace可以直接构造string,所以只调用了一次构造!!

下面进入这个条款的主题:避免依万能引用型别进行重载
情形一:万能引用精确匹配优先于类型提升后匹配
考虑如果通过整型的索引去访问name,新增一个int类型的重载:

std::string nameFromIdx(int idx);
void logAndAdd(int idx) {
  auto now = std::chrono::system_clock::now();
  log(now, "logAndAdd");
  names.emplace(nameFromIdx(idx));
}

此时选择那个重载函数会让人费解:

logAndAdd(22); // 正常,调用第二个重载
short nameIdx = 22;
logAndAdd(nameIdx); // 不正常,调用了第一个重载,万能引用的版本!!!

所以,(1)T&&万能引用几乎能和所有类型产生精确匹配(30条款描述了不属于此情况的实参),而精确匹配优先于类型提升后的匹配,所以一旦万能引用成为重载候选,他能吸引走大量的实参型别。
情形二:万能引用在构造函数中出现

class Person {
public:
  template<typename T>
  explicit Person(T&& n)
  : name(std::forward<T>(n)) {}
  explicit Person(int idx)
  : name(nameFromIdx(idx)) {}
  // 注意,下面两个函数为编译器自动生成
  Person(const Person& rhs); // 编译器生成的复制构造函数
  Person(Person&& rhs); // 编译器生成的移动构造函数
private:
  std::string name;
};

Person p("Nancy");
auto cloneOfP(p); // 错误!调用了万能引用的重载版本。
// 因为此时万能引用的函数被实例化为下列版本。
explicit Person(Person& n)
  : name(std::forward<Person&>(n)) {}
// 优先匹配了这个不加const的版本!!!!!

因此(2)A.万能引用精确匹配非const变量时,优于添加const饰词形参的函数!(注意和条款一中,模板匹配时,引用和const的忽略做区别)
如果此时使用const对象,则能正确调用普通的添加const饰词的版本函数了。

const Person p("Nancy");
auto cloneOfP(p); // 正常,这回调用的是万能引用的复制构造函数。

因此(2)B.若在函数调用时,一个模板实例化函数和一个非函数模板具备相等的匹配程度,则优先选用常规函数。

情形三:类似于情形一(万能引用精确匹配优先于,继承类向基类的类型转换)

class SpecialPerson: public Person {
public:
  SpecialPerson(const SpecialPerson& rhs)
  : Person(rhs) {}  // 调用的是基类的完美转发构造函数
  SpecialPerson(SpecialPersion&& rhs)
  : Person(std::move(rhs)) {}  // 调用的是基类的完美转发构造函数
};

因此(3)万能引用精确匹配优先于类型转换。


条款27 熟悉依万能引用型别进行重载的替代方案

解决条款26中的问题的方式:
1.舍弃重载
将两个函数命名为不同的名字,但不适用于第二个重载函数的例子。
2.传递const T&型别的形参
使用左值常量引用型别来代替传递万能引用型别,其实就是条款26中第一个实现,可能会损失效率。
3.传值
当你知道肯定需要复制形参时,考虑按值传递对象。

class Person {
public:
  template<typename T>
  explicit Person(string n) // 替换掉T&&型别的构造函数
  : name(std::move(n)) {}
  explicit Person(int idx)
  : name(nameFromIdx(idx)) {}
private:
  std::string name;
};

4.标签分派。
把它委托给两个函数,一个接收整形值,一个接收其他所有型别。
解决上一节的情形一:

template<typename T>
void logAndAdd(T&& name) {
  logAndAddImpl(
    std::forward<T>(name),
    std::is_integral<typename std::remove_reference<T>::type>()
    // 如果remove reference,则int&类型会判断为不是整形!!
  );
}

template<typename T>
void logAndAddImpl(T&& name, std::false_type) {  // 非整形实参
  auto now = std::chrono::system_clock::now();
  log(now, "logAndAdd");
  names.emplace(std::forward<T>(name));
}
std::string nameFromIdx(int idx);
void logAndAddImpl(int idx, std::true type) {  // 整形实参
  logAndAdd(nameFromIdx(idx)); // 这个代码很经典!! // 再次调用logAndAdd!!!
}

std::false_typestd::true_type就是标签,针对logAndAdd内的重载实现函数发起的调用把工作分派到正确的重载版本的手法就是创建适当的标签对象,这种设计叫标签分派,它在编译期起作用。
5.对接收万能引用的模板施加限制
有些针对构造函数的调用可能会由编译器生成的构造函数接手处理,导致绕过了标签系统。
此时可以用到std::enable_if施加了std::enable_if<condition>::type的模板只有再满足了condition所指定的条件才会启用。

// enable_if的使用格式
class Person {
public:
  template<typename T,
  typename = typename std::enable_if<condition>::type
  >
  explicit Person(T&& n);
};

这里的condition使用std::is_same<Person, T>::value来判断(is_same前不用加typename)
注意需要兼顾T的型别,
(1)考虑T是否是个引用,型别Person、Person&、Person&&都应该当做Person来处理。
(2)考虑T是否带有const或者volatile饰词,const Person、volatile Person、 const volatile Person都应该当做Person来处理。
标准库有处理(1)(2)两种情况的实现,使用std::decay<T>::type即可去掉引用和CV饰词(const或volatile),除此之外,std::decay<T>::type还可以将数组和函数强制转换为指针型别
因此型别判定就变成了下面的形式:
!std::is_same<Person, typename std::decay<T>::type>::value
解决上一节的情形二:

class Person {
public:
  template<typename T,
  typename = typename std::enable_if<
               !std::is_same<Person,
                             typename std::decay<T>::type
                             >::value
             >::type
  >
  explicit Person(T&& n);
};

另外,还需要最后解决的是上一节的情形三,派生的情形。
需要用到std::is_base_of<T1, T2>::value(is_base_of前不用加typename)
如果T2由T1派生而来,则std::is_base_of<T1, T2>::value是true,且std::is_base_of<T, T>::value也是true
解决上一节的情形三:

class Person {
public:
  template<typename T,
  typename = typename std::enable_if<
               !std::is_base_of<Person,
                             typename std::decay<T>::type
                             >::value
             >::type
  >
  explicit Person(T&& n);
};

使用C++14简化上面的代码:(is_base_of和is_same也都不用_t)

class Person {
public:
  template<typename T,
  typename = std::enable_if_t<
               !std::is_base_of<Person,
                             std::decay_t<T>
                             >::value
             >
  >
  explicit Person(T&& n);
};

6.综合第4点标签分配解决整形问题,和第5点解决Person自身类型的问题,最后可以得到下面的版本(C++14):

class Person {
public:
  template<typename T,
  typename = std::enable_if_t<
               !std::is_base_of<Person, std::decay_t<T>>::value
               &&
               !std::is_integral<std::remove_reference_t<T>>::value
             >
  >
  explicit Person(T&& n)
  : name(std::forward<T>(n)) {}
  explicit Person(int idx)
  : name(nameFromIdx(idx)) {}
};

7.权衡
前三种技术(舍弃重载,传递const&,传值)都需要对带调用函数参数注一指定型别,而后两种技术(5和6点)都用了完美转发,因此无需指定形参型别。
(1)完美转发效率更高,比如它允许将“Nancy”的字符串字面量转发给某个接受std::string的构造函数,而未使用完美转发的技术则必须要先从字符串字面量出发创建一个临时的std::string对象。
(2)但有些特定型别无法实施完美转发,如条例30。
(3)完美转发还有个缺点时,传递非法形参时,报错很难看(比如160行),暴露的位置很靠后。万能转发的次数越多,错误藏的越深。
解决(3)可以使用static_assert编译器的断言,以及std::is_constructible可以在编译器判断某个型别的对象是否从另一型别的对象出发完成构造。比如std::is_constructible<std::string, T>::value表示是否可以从T型别的对象构造一个std::string型别的对象。
类型错误匹配报错明确:

class Person {
public:
  template<typename T,
  typename = std::enable_if_t<
               !std::is_base_of<Person, std::decay_t<T>>::value
               &&
               !std::is_integral<std::remove_reference_t<T>>::value
             >
  >
  explicit Person(T&& n)
  : name(std::forward<T>(n)) {
    // 断言是否可以从T型别的对象构造一个std::string型别的对象。
    static_assert(std::is_constructible<std::string, T>::value,
                 "Parameter n can't be used to construct a std::string");
    ... // 构造函数要做的工作放在这里。
  }
  explicit Person(int idx)
  : name(nameFromIdx(idx)) {}
};

但还是有延迟,static_assert位于构造函数的函数体内,但转发代码属于成员初始化列表的一部分,所以还是会报之前一样的错,但在这之后会看到static_assert,所以错误更明确。


条款28 理解引用折叠、深入理解std::forward

对于万能引用来说:
如果传递的实参是个左值,T的推导结果就是个左值引用型别;如果传递的实参是个右值,T的推导结果就是个非引用型别(非对称性:左值的编码结果为左值引用型别,右值的编码结果为非引用型别

引用折叠:
对于两种引用组合的情况,如果任一引用是左值引用,则结果是左值引用。否则(即两个都为右值引用),结果为右值引用。
对于一个完美转发而言,如下:

template<typename T>
void f(T&& fParam) {
  someFunc(std::forward<T>(fParam));
}

std::forward的任务是,当且仅当编码T中的信息表明传递给实参右值,即T的推导结果型别是个非引用型别时,对fParam形参(左值)实施到右值的强制类型转换(详情见条款33,这也解释了24条最后一个代码case,为什么可以使用deltype,deltype左值引用就为左值引用,经过std::forward还是左值引用,deltype右值引用为右值引用,经过std::forward还是右值引用)!!!
std::forward的一种去掉其他细节的实现如下!!!

// C++11
template<typename T>  // 在命名空间std中
T&& forward(typename
              remove_reference<T>::type& param) {
  return static_cast<T&&>(param);
}
// C++14
template<typename T>  // 在命名空间std中
T&& forward(remove_reference_t<T>& param) {
  return static_cast<T&&>(param);
}

引用折叠的四种场景:
(1)最常见的模板实例化
(2)auto变量的型别生成

Widget widgetFactory();
Widget w;
auto&& w1 = w; // 初始化w1的是一个左值,因此auto的型别推导结果是Widget&,w1是左值引用!!
auto&& w2 = widgetFactory(); // auto的型别推导结果是Widget,w2仍然是右值引用

在万能引用中,T型别的左值推导结果为T&,T型别的右值推导结果为T。
(3)typedef和别名声明
注意:typedef和T&&结合后可能会名不副实。

template<typename T>
class Widget {
public:
  typedef T&& RvalueRefToT;
};
// 假设我们以左值引用来实例化该实参。
Widget<int&> w;
// 代入typedef
typedef int& && RvalueRefToT;
// 折叠后,T&&但RvalueRefToT却表示的左值引用。
typedef int& RvalueRefToT;

(4)decltype的运用
如果在分析一个涉及decltype的型别过程中出现了引用的引用,则引用折叠也会消灭他。


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

C++98升级C++11后,允许编译器使用相对低廉的移动操作来代替昂贵的复制操作(在满足某些条件的情况下)
但注意,在下面几个场景中,C++11的移动语义不会带来什么好处:
1.没有移动操作:待移动对象未能提供移动操作。因此,移动请求就变成了复制请求。
比如,声明了复制操作/移动操作/析构函数的类,不能够使用编译器自己默认生成的移动操作(条款17)。
2.移动未能更快:带移动的对象虽然有移动操作,但并不比其复制操作更快。
(1)对比std::vector的移动,它拥有一个指涉到存放容器内容的堆内存指针。(一般其实是几个,线性连续空间,它以两个迭代器_Myfirst和_Mylast分别指向配置得来的连续空间中目前已被使用的范围,并以迭代器_Myend指向整块连续内存空间的尾端。)
std::vector的移动仅需要常数时间。

std::vector<Widget> vw1;
// 完成移动操作仅需要常数时间,因为仅仅是包含在vw1和vw2中的指针被修改了。
auto vw2 = std::move(vw1);

(2)而对于std::array的移动。因为std::array仅仅是一个提供STL接口的内建数组,它的内存在栈上。所以对std::array进行移动操作,其实是对每个元素进行逐一的移动。
所以std::array的移动和复制都需要线性的计算复杂度。

std::array<Widget, 10000> aw1;
// 完成移动操作需要线性时间,需要将aw1中所有元素移入aw2.
auto aw2 = std::move(aw1);

(3)对于std::string来说,型别提供的移动是常数时间,复制是线性时间。
但注意!并不意味着移动比复制更快。
因为std::string的实现一般都采用了小字符串优化(small string optimization, SSO),采用SSO后,小型字符串(例如,容量不超过15个字符的字符串)会存储在std::string对象的某个缓冲区内,而不去使用在堆上的内存。
SSO发明的原因,主要是因为大部分字符串都是小字符串,使用内部缓冲区来存储这样的字符串,避免了动态内存分配,其实这是提高效率的表现。但同样也隐含这移动并不比复制的操作更快的缺点。
3.移动不可用:移动本可以发生的语境下,要求移动操作不可以发射异常,但该操作未加上noexcept声明,因此变成了复制操作。(条款14)
4.原对象是个左值,只有右值可以作为移动的来源。

所以选择的建议是:
在撰写模板时,如果不知道将要和哪些型别配合,或者代码中所涉及的特征不稳定会频繁修改,则需要保守的去复制对象。
如果已知代码内的型别,也可以肯定它们的特性(是否支持成本低廉的移动操作)不会发生改变,如果这些型别能够提供成本低廉的移动操作,则可以使用移动。


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

一般来说,完美转发都是想要目标函数能够处理原类型,所以转发一般都是在处理形参为引用型别的类型。
完美转发使用万能引用的原因是只有它才能对传入的实参是左值还是右值进行编码。
我们下列的讨论基于以下基础的完美转发形式:

// 一个参数的完美转发
template<typename T>
void fwd(T&& param) {
  f(std::forward<T>(param));
}
// 接收任意可变长型别参数的fwd函数
// 在std::make_shared和std::make_unique可以见到。
template<typename... Ts>
void fwd(Ts&& params...) {
  f(std::forward<T>(params)...);
}

完美转发失败:给定目标函数f和转发函数fwd,当以某特定实参调用f会执行某操作,而用同一实参调用fwd会执行不同的操作。
1.大括号初始化物
见参考条款7

void f(const std::vector<int>& v);
f({1, 2, 3});  // 没问题,{1, 2, 3}隐式转换为std::vector<int>
fwd({1, 2, 3}); // 错误,无法通过编译

// 解决此完美转发失败的方式:
auto il = {1, 2, 3}; // il的型别推导结果为std::initializer_list<int>
fwd(il);

完美转发会在下面两个条件中的任意一个成立时失败:
(1)编译器无法为一个或多个fwd的形参推导出型别结果。
(2)编译器为一个或多个fwd的形参推导出了“错误的”型别结果(fwd根据型别推导结果的实例化无法通过编译,或者fwd推导而得的型别调用f与直接以传递fwd的实参调用f行为不一致)
在本例中,fwd({1, 2, 3})的问题在于向未声明为std::initializer_list型别的函数模板形参传递了大括号初始化物。因为这样的语境是“非推导语境”,也就是fwd({1, 2, 3})的形参没有声明为std::initializer_list,编译器就会被禁止在fwd的调用过程中从表达式1, 2, 3出发来推导型别。
修改方式:先用auto声明一个局部变量,再把这个局部变量传给转发函数。
2.0和NULL用作空指针
0、NULL以空指针指明传递给模板,型别推导可能会出错(推导为整形)。
修改方式:使用nullptr传递指针。
3.仅有声明的整形的static const成员变量
大前提:
(1)从硬件角度来看,指针和引用在本质上是同一事物,引用不过是会提领的指针
(2)按照引用或者指针传递参数时,需要这个参数有定义,即能够取址。
(3)C++规定可以不需要给出类中的整形static const成员变量的定义,仅需要声明之,因为编译器会根据这些成员的值实施常量传播。(可以参考effective C++ 第二条)
所以以下代码是合理的:

class Widget {
public:
  static const std::size_t MinVals = 28; // 给出了MinVals的声明,但没给定义
};
std::vector<int> WidgetData;
widgetData.reserve(Widget::MinVals);  // 此处用到了MinVals
// 此处编译器直接将28赛道所有提及MinVals之处,未为MinVals的值保留存储。

void f(std::size_t val);
f(Widget::MinVals); // 没问题,当做28处理
fwd(Widget::MinVals); // 错误,应该无法链接,因为没有定义!!!

修改方式(提供定义):

// 在Widget的.cpp文件中定义
const std::size_t Widget::MinVals;

4.重载的函数名字和模板名字
如果参数是函数或者模板,且可以指代多个时,完美转发fwd并不知道需要哪一个 函数的重载版本 或 模板的实例。

// f正常传入函数参数可以使用下面两种声明:
void f(int (*pf) int);
void f(int pf(int));

// 多个函数重载
// 假设有下面两个重载函数:
int processVal(int value);
int processVal(int value, int priority);
f(processVal); // 没问题。
fwd(processVal); // 错误,不知道调用哪一个processVal的重载版本。

// 函数模板
template<typename T>
T workOnVal(T param) {
}
fwd(workOnVal);  // 错误,不知道调用workOnVal的哪个实例。

修改方式:指明是哪一个重载版本或者实例。

ProcessFuncType processValPtr = processVal;
fwd(processValPtr);
using ProcessFuncType = int(*)(int);
fwd(static_cast<ProcessFuncType>(workOnVal));

5.位域。
大前提:
位域里的元素不可以被直接取址,非const引用不能绑定到位域
位域的内存布局:https://www.sidney.wiki/cpp/713

struct IPv4Header {
  std::uint32_t version:4,
                IHL:4,
                DSCP:6,
                ECN:2,
                totalLength:16;
};
void f(std::size_t sz); // 待调用的函数
IPv4Header h;
f(h.totalLength); // 没问题
fwd(h.totalLength); // 错误!问题在于fwd的形参是个引用,而h.totalLength是个非cosnt的位域!!!

修改方式:
按值传递(借助副本)或者按照常量引用(reference-cosnt)传递(常量引用实际也是绑定到常规对象,其中复制了位域的值)

// 复制位域值
auto length = static_cast<std::uint16_t>(h.totalLength);
fwd(length); // 转发该副本

6 lambda表达式

需明确以下几个名字:

1.lamba表达式。它是表达式的一种,是源代码的组成部分,如下面第二排部分。

std::find_if(container.begin(), container.end(), 
             [](int val) { return 0 < val && val < 10; });

2.闭包,是lambda表达式的运行期对象,根据不同的捕获模式,闭包会持有数据的副本或引用。在上述代码中,闭包就作为第三个参数在运行期传递给了std::find_if对象。
不同语言的闭包一般都是指——一个函数捕捉到了它创建时候的上下文(也就是定义这个函数的时候可以访问的变量),当它被调用的时候,即使调用的地方已经离开了创建它的上下文,那些上下文的变量仍然可以继续使用
3.闭包类,是实例化闭包的类,每个lambda式都会触发编译器生成一个独一无二的闭包的类,而闭包中的语句会变成它的闭包类成员函数的可执行指令
一般而言,闭包可以复制。
以上概念中,lambda和闭包类一般是编译器的概念,闭包时运行期的概念


条款31 避免默认捕获模式

1.按引用的默认捕获会导致空悬指针的问题
按引用捕获会导致闭包包含指涉到局部对象的引用,或者指涉到定义lambda式的作用域内的形参的引用。一旦由lambda式所创建的闭包越过了该局部变量或形参的生命期,那么闭包内的引用就会空悬。

using FilterContainer = 
  std::vector<std::function<bool(int)>>;
FilterContainer filters;
void addDivisorFilter() {
  auto calc1 = computeSomeValue1();
  auto calc2 = computeSomeValue2();
  auto divisor = computeDivisor(calc1, calc2);
  filters.emplace_back(
    [&](int value) { return value % divisor == 0; }  // 危险,对divisor的指涉可能空悬。
  );
  // 虽然也有问题,但divisor至少提醒了我们注意生命周期的问题。
  filters.emplace_back(
    [&divisor](int value) { return value % divisor == 0; }  // 危险,对divisor的指涉仍然可能空悬。
  );
}

下面的使用可能没有问题,但仍然不好。

template<typename C>
void workWithContainer(const C& container) {
  auto calc1 = computeSomeValue1();
  auto calc2 = computeSomeValue2();
  auto divisor = computeDivisor(calc1, calc2);
  using ContElemT = typename C::value_type;
  using std::begin; // 暴露std实现用作兜底,effective C++条款25
  using std::end;
  if (std::all_of(
    begin(container), end(container),
    [&](const ContElemT& value) {
      return value % divisor == 0;
    })) {
    ...
  } else {
    ...
  }
}
// (顺便提一句)C++14提供了在lambda式的形参声明中使用auto的能力!!
  if (std::all_of(
    begin(container), end(container),
    [&](const auto& value) {
      return value % divisor == 0;
    }))

解决方式:
(1)显式的列出lambda式所依赖的局部变量或形参,防止有未知的声明期更短的变量存在。
(2)对divisor采用按值的默认捕获形式,但也不好(比如按值默认捕获了一个指针,但你不知道指针指向的对象何时释放!)

filters.emplace_back(
  [=](int value) { return value % divisor == 0; } 
);

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

class Widget {
public:
  void addFilter() const;
private:
  int divisor;
};

// 可以编译成功,但不好,也可能有生存期的问题
// lambda闭包的存活和它含有的this指针副本的Wiget对象的生命周期绑定!!!!(见后面的解释)
void Widget::addFilter() const {
  filters.emplace_back(
    [=](int value) { return value % divisor == 0; } 
  );
}

// 注意!!这里实际是捕获了this指针,编译器的视角看来代码类似于下面的形式:
void Widget::addFilter() const {
  auto currentObjectPtr = this;
  filters.emplace_back(
    [currentObjectPtr](int value) 
    { return value % currentObjectPtr->divisor == 0; } 
  );
}

捕获只能针对于在创建lambda式的作用域内可见的非静态局部变量(包括形参)!!!
因此,如果默认捕获模式被消除,或者显示的按值捕获divisor,代码都不会通过编译,如下所示。

// 不能通过编译,filters后续的调用会找不到divisor。
void Widget::addFilter() const {
  filters.emplace_back(
    [](int value) { return value % divisor == 0; } 
  );
}

// 不能通过编译,divisor既不是局部变量,也不是形参!!!!!!!!!
void Widget::addFilter() const {
  filters.emplace_back(
    [divisor](int value) { return value % divisor == 0; } 
  );
}

按值默认捕获,也可能会导致空悬指针。

// 在doSomeWork结束后,Widget对象被销毁,因此filters中就含有了一个带有空悬指针的元素!!!
void doSomeWork() {
  auto pw = 
    std::make_unique<Widget>();
  pw->addFileter(); // 调用第2点最开始的版本
}

解决方式:将想捕获的成员变量复制到局部变量中,再捕获该局部变量。

void Widget::addFilter() const {
  auto divisorCopy = divisor;
  filters.emplace_back(
    [divisorCopy](int value) 
    { return value % divisorCopy == 0; } 
  );
}

// C++14可以用广义的lambda捕获!!
void Widget::addFilter() const {
  auto divisorCopy = divisor;
  filters.emplace_back(
    [divisor = divisor](int value) // 因为divisor不是局部变量或形参,不能直接捕获,需要显式复制入闭包!!
    // 注意它与普通的按值捕获的区别。
    { return value % divisor == 0; } 
  );
}

// 下面也可以解决但不好,如果采用copy的方式,也可以按值默认捕获,但为何要冒着以外捕获this指针的风险呢?
void Widget::addFilter() const {
  auto divisorCopy = divisor;
  filters.emplace_back(
    [=](int value) 
    { return value % divisorCopy == 0; } 
  );
}

按值的默认捕获的另外一个缺点是,它似乎是表明闭包时自洽的,与外面的数据变化无缘。但lambda只能捕获依赖非静态局部变量和形参,不能捕获静态局部变量。(没能捕获–>不属于这个闭包)

void addDivisorFilter() {
  static auto calc1 = computeSomeValue1();
  static auto calc2 = computeSomeValue2();
  static auto divisor = computeDivisor(calc1, clac2);
  filters.emplace_back(
    [=](int value) {
      return value % divisor == 0;
    }
  );
  ++divisor;  // 注意,每次调用addDivisorFilter都会自增同一个divisor。所以每次调用时,添加到filters里的lambda式的行为都不一样(对应divisor新值),实现的效果变成了按引用捕获!!!
}

labmda按值捕获不了静态局部变量,甚至这种实现方式类似于按引用捕获了!!!


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

1.C++14新增初始化捕获(init capture)的特性
也即条款31提到的广义lambda捕获,它可以直接支持把一个只移对象(std::unique_ptrstd::future)放入闭包。
使用初始化捕获的优点:(1)获得了一个由lambda生成的闭包类中的成员变量(2)可以使用一个表达式,用以初始化这个成员变量。
下面两个都是C++14中的代码:

class Widget {
public:
  ...
  bool isValidated() const;
  bool isProcessed() const;
  bool isArchived() const;
private:
  ...
};
auto pw = std::make_unique<Widget>();
auto func = [ pw = std::move(pw) ] {
  return pw->isValidated() && pw->isArchived();
};

注意这个初始化lambda中,等号左边和等号右边分属于不同的作用域,左侧的作用域是闭包类的作用域,表示闭包类的成员变量pw,右侧的作用域是labmda定义之处的作用域相同。
除了移动一个对象外,还可以由std::make_unique实时初始化,原地构造

auto func = [ pw = std::make_unique<Widget>() ] {
  return pw->isValidated() && pw->isArchived();
};

在C++11中,也可以模拟移动捕获的行为。
2.C++11中用仿函数代替初始化捕获的lambda
注意:一个lambda表达式不过是生成一个类并创建一个该类的对象的手法。
所以我们可以用仿函数代替初始化捕获的lambda。

class IsValAndArch {
public:
  using DataType = std::unique_ptr<Widget>
  explicit IsValAndArch(DataType&& ptr)
  : pw(std::move(ptr)) {}
  bool operator()() const {
    return pw->isValidated() && pw->isArchived();
  }
private:
  DataType pw;
};
auto func = IsValAndArch(std::make_unique<Widget>());

上面也实现了对成员变量实施移动初始化的类。
3.C++11中使用std::bind+lambda模拟初始化捕获
std::bind的用法可以参考
https://www.sidney.wiki/cpp/725
需模拟的操作有两步(1)把需要捕获的对象移动到std::bind产生的函数对象中。(2)给到lambda式一个指涉到欲“捕获”的对象的引用。
比如对于一个vector对象,在C++14中移入闭包:

std::vector<double> data;
auto func = [data = std::move(data)] {
  ...  
};

auto func = [ pw = std::make_unique<Widget>()] {
  return pw->isValidated() && pw->isArchived();
};

在C++11中,使用std::bind去移入闭包:
注意,std::bind也生成一个函数对象,我们称为std::bind返回的函数为绑定对象(bind object)std::bind的第一个实参是一个可调用对象,接下来的所有实参表示传给改对象的值。

std::vector<double> data;
auto func = 
  std::bind(
    [](const std::vector<double>& data) { // 常量引用的原因见第4点
      ...
    }, std::move(data) // 这是bind的一个参数。
);

auto func = 
  std::bind(
    [](const std::unque_ptr<Widget>& pw) {
      return pw->isValidated() && pw->isArchived();
    }, std::make_unique<Widget>()。
);

上述代码中,当一个绑定对象被“调用”时,它所存储的实参会传递给 原先传递给std::bind的那个可调用对象(第一个参数),也就是func内经由移动构造出的data的副本会作为实参传递给那个 原先传递给std::bind的lambda表达式。
绑定对象存储着传递给std::bind所有实参的副本,闭包的生命期和绑定对象(也和绑定对象中的对象)是相同的,只要闭包存在,绑定对象内的伪捕获对象也存在。
std::bind模拟移动入闭包的操作有以下步骤,先移动构造一个对象入绑定对象,然后按引用把该移动构造的对象传递给lambda式。
4.lambda的const属性
默认情况下,lambda生成的闭包类中的operator()成员函数会带有const饰词,所以会导致闭包中的所有成员变量在lambda式的函数体内会带有const饰词,但是,绑定对象中移动构造得到的副本对象却不带有lambda饰词,为了防止该data被意外修改,lambda的形参就声明为了常量引用,比如上一个例子。
但如果想修改lambda内的成员变量,可以使用mutable饰词,这样lambda生成的闭包类中的operator()成员函数不会再带有const饰词此时,lambda的形参也可以省略const。

std::vector<double> data;
auto func = 
  std::bind(
    [](std::vector<double>& data) mutable { // lambda内的成员变量,以及data都可以被修改
      ...
    }, std::move(data) // 这是bind的一个参数。
);

条款33 对auto&&型别的形参使用decltype,以std::forward

1.C++14中可以使用泛型lambda式(generic lambda),即lambda可以在形参规格中使用auto

auto f = [](auto param) {
  return func(normalize(param));
};
// 等价于下面的闭包类函数调用运算符,auto转换见条款2
class SomeCompilerGeneratedClassName {
pubic:
  template<typename T>
  auto operator()(T param) const {
    return func(normalize(param));
  }
};

2.而如果想在lambda中实现对形参的完美转发,需要使用万能引用。

auto f = [](auto&& param) {
  return func(normalize(std::forward<delcltype(param)>param));
};

为什么是delcltype(param)呢
条款28解释过,如果把左值传递给万能引用的形参,则该形参的型别会为左值引用,如果传递的是右值,这形参是右值引用。
条款28还解释了,使用std::forward的惯例是,用型别形参为左值引用来表明要返回左值,用非引用型别来表明要返回右值
而条款3解释到,delcltype一个左值引用是左值引用型别,delctype一个右值引用还是右值引用型别,这里和非引用型别不一样,怎么处理呢?

先看std::forward在C++14的实现

template<typename T>  // 在命名空间std中
T&& forward(remove_reference_t<T>& param) {
  return static_cast<T&&>(param);
}

当客户代码想要完美转发Widget右值引用型别时,实例化代入到上述实现中。

Widget&& && forward(Widget& param) {
  return static_cast<Widget&& &&>(param);
}
// 引用折叠后变成了正确的代码,也是完美转发Widget非引用型别的代码!!
Widget&& forward(Widget& param) {
  return static_cast<Widget&&>(param);
}

所以实例化std::forward时,使用一个右值引用型别和一个非引用型别,都会产生右值引用的结果;使用一个左值引用型别,会产生左值引用的结果。
3.扩展多参数的完美转发lambda式版本

auto f =
  [](auto&&... params) {
  return 
  func(normalize(std::forward<decltype(params)>(params)...));
};

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

C++11,lambda几乎都是更好的选择,C++14中,lambda已经能全方面替代std::bind
举例,警报程序

// 表示时刻的型别typedef
using Time = std::chrono::steady_clock::time_point;
enum class Sound { Beep, Siren, Whistle };
// 表示时长
using Duration = std::chrono::steady_clock::duration;
// 在T时刻,发出声音s,持续时长d
void SetAlarm(Time t, Sound s, Duration d);

使用lambda,设置在一小时之后发出警报30s,但是具体声音不定。

// 接受一个声音,声音在1小时发出,并持续30s
// C++11实现
auto setSoundL = 
  [](Sound s) {
  using namespace std::chrono;
  setAlarm(steady_clock::now() + hours(1),
           s,
           seconds(30));
};
// C++14可提供秒(s)、毫秒(ms)和小时(h)等标准后缀!!!在std::literals名字空间里有实现!!!
auto setSoundL = 
  [](Sound s) {
  using namespace std::chrono;
  using namespace std::literals;
  setAlarm(steady_clock::now() + 1h,
           s,
           30s);
};

std::bind的缺点:
1.表达式评估求值的时刻在调用std::bind的时刻,并不是绑定对象的调用时刻,求得的结果值会保存在绑定对象中。

// 这个实现有问题!!!steady_clock::now()的计算时间是调用std::bind的时刻,并且求得的值保存在了绑定对象中。
using namespace std::chrono;
using namespace std::literals;
using namespace std::placeholders;
auto setSoundB = 
  std::bind(setAlarm,
            steady_clock::now() + 1h,
            _1,
            30s);

正确的std::bind实现如下,在原来的std::bind中再嵌套一层std::bindC++14之后,标准运算符模板的模板型别实参大多数情况下可以省略不写):

// C++14之前的实现,需要指明plus模板的具体型别。
auto setSoundB = 
  std::bind(setAlarm,
            std::bind(std::plus<steady_clock::time_point>(),
                      steady_clock::now(),
                      hours(1)),
            _1,
            seconds(30));
// C++14之后的实现,
auto setSoundB = 
  std::bind(setAlarm,
            std::bind(std::plus<>(),
                      steady_clock::now(),
                      hours(1)),
            _1,
            seconds(30));

2.std::bind还会遇到重载的问题,编译器不知道把那个重载函数版本传给std::bind,因为它拿到的就只有一个函数名,而仅函数名本身是多义的。

// 新增一个重载的setAlarm版本
enum class Volume { Noraml, Loud, LoudPlusPlus };
void setAlarm(Time t, Sound s, Duration d, Volume V);

有重载时,上述lambda形式会一如既往的运行重载协议(因为在lambda里面的函数就是常规的唤醒方式)
而在std::bind的调用,就无法通过编译了。
std::bind解决此问题的方式:

// 指明调用的是哪个重载版本
using SetAlarm3ParamType = void(*)(Time t, Sound s, Duration d);
auto setSoundB = 
  std::bind(static_cast<SetAlarm3ParamType>(setAlarm)),
            std::bind(std::plus<>(),
                      steady_clock::now(),
                      1h),
            _1,
            30s);

3.lambda的实现是简单的函数唤醒,setSoundL函数编译器内联的概率高。而上面std::bind的实现使用了函数指针发起调用,会降低setSoundB被内联的概率。
4.std::bind的方式可读性差
比如要求一个实参是否在极小值和极大值之间。

// C++11 lambda实现
auto betweenL =
  [lowVal, highVal](int val) {
  return lowVal <= val && val <= highVal;
};
// C++14 lambda实现(使用auto型别形参)
auto betweenL =
  [lowVal, highVal](const auto& val) {
  return lowVal <= val && val <= highVal;
};
// C++11 std::bind实现
auto betweenB = 
  std::bind(std::logical_and<bool>(),
              std::bind(std::less_equal<int>(), lowVal, _1),
              std::bind(std::less_equal<int>(), _1, highVal));
// C++14 std::bind实现
auto betweenB = 
  std::bind(std::logical_and<>(),
              std::bind(std::less_equal<>(), lowVal, _1),
              std::bind(std::less_equal<>(), _1, highVal));

5.std::bind函数参数按值还是按引用传递不清晰。

// 假设我们有一个函数用来制作Widget型别对象的压缩副本。
enum class CompLevel { Low, Normal, High };
Widget compress(const Widget& w,
                CompLevel lev);

lambda的函数参数传递方式很清晰:

// w和lev都是按值传递的
auto compressRateL = 
  [w](CompLevel lev) {
  return compress(w, lev);
}
// std::bind
Widget w;
using namespace std::placeholders;
auto compressRateB = std::bind(compress, w, _1);

w是按值还是按引用传递?区别很大,按引用传递的话,如果在绑定后调用前w改变了,存储的w的值也会随之改变。
std::bind本身的实参是按值传递,而std::bind绑定对象的函数的实参是按引用传递,并且调用了完美转发!!!
在这里,w是按值传递的,lev是按引用传递的!!!
如果w也想按引用传递,可以借助std::ref

auto compressRateB = std::bind(compress, std::ref(w), _1);

std::bind唯一在C++11中有两个优点:
(即使下面两个优点,在C++14中 lambda也可以做到)
1.参考条款32中的移动捕获。
2.实现静态多态,因为std::bind绑定对象的函数的实参是完美转发的,借助完成转发的特性,则可以实现静态多态。

class PolyWidget {
public:
  template<typename T>
  void operator()(const T& param);
};

PolyWideget pw;
auto boundPW = std::bind(pw, _1);

boundPW(1930); // int
boundPW(nullptr); // nullptr
boundPW("Rosebud"); // 字符串字面量char[]

C++11中lambda不好实现上面的多态,但在C++14中也可以用auto完美解决。

auto boundPW = [pw](const auto& pram) {
               pw(param); };

7 并发API

C++对并发支持的一大部分是以对编译器厂商实施约束的形式提供的,所以C++可以跨平台撰写具有标准行为的多线程程序。


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

// 基于线程创建
int doAsyncWork();
std::thread t(doAsyncWork);
// 基于任务创建
auto fut = std::async(doAsyncWork); // fut是future的缩写

async相对于thread的优点
1.fut可以接收doAsyncWork的返回值,可以在fut.get()中获取此返回值。
如果doAsyncWork发射了一个异常,get函数也可以访问到异常。否则如果使用std::thread,程序就会调用std::terminate直接退出了。
2.std::async可以自动管理线程耗尽、超订、负载均衡,且在各新平台适配,而std::thread则可能在这些点上表现不佳,需要自己实现。
(1)”线程“在带有并发的C++程序中的意义:
1)硬件线程是实际执行计算的线程,现在计算机体系结构会为每个CPU内核提供一个或多个硬件线程。
2)软件线程(又称操作系统线程或系统线程),是操作系统用以实施跨进程的管理,以及进行硬件线程调度的线程。通常能够创建的软件线程比硬件线程多,因为当一个软件线程阻塞(例如阻塞在I/O操作上,或者需要等待互斥量或条件变量),运行另外的非阻塞线程能够提供吞吐率。
3)std::thread是C++进程的对象,用作底层软件线程的句柄。有些std::thread对象表示为“null”句柄,对应于无软件线程。比如,当处于默认构造状态(没有待执行的函数),被移动了(作为移动的目的地的std::thread对象成为了底层线程的句柄,被联结了(函数已经运行结束),被分离了(std::thread对象与其底层软件线程的连接被切断了)
(1)线程耗尽
软件线程是一种有限的资源,如果试图创建的线程数量多于系统能够提供的数量,就会抛std::system_error的异常,即使待运行的函数是noexcept的。
解决方式:1)在当前线程中执行函数,但可能会导致负载不均衡。2)在已存在的软件线程完成工作后,在尝试创建一个新的std::thread,但会产生更多的等待。
(2)超订问题
即使没有用尽线程,但还是可能会发生超订(oversubscription)问题,也即当就绪状态(非阻塞)的软件线程超过了硬件线程的数量。
这个时候线程调度器会为软件线程在硬件线程上分配CPU时间片,当一个线程的时间片用完,另一个线程启动时,就会发生语境切换,增加线程管理开销。
尤其是一个软件线程的这一次和下一次被调度器切换到不同的CPU内核上的硬件线程时,会发生高昂的计算成本。因为1)那个线程通常不会命中CPU缓存,2)CPU内核运行的新软件线程还会污染CPU缓存上为旧线程所准备的数据。
解决方式:
合理的软件线程/硬件线程比例,但它是动态的,和I/O密集,CPU密集有有关系,也依赖于语境切换的成本,软件线程使用时的缓存命中率。而硬件线程的数量和CPU缓存的细节又取决于体系结构,所以要设计通用的有良好移植性的去解决超订问题较难。
std::async可以交给C++标准库的实现者负责统一的线程管理。
1)std::async,系统不保证会创建一个新的软件线程,他允许调度器把指定函数运行在请求这个函数的返回结果的线程中。(比如对fut调用了get或者wait的线程),即默认是std::launch::async | std::launch::defferred可选择的形式。如果想要确保在向另一个线程中,可以指定std::launch::async
2)std::async有可能会使用全范围的线程池来避免超订,而且会通过运用工作窃取算法来提高硬件内核间的负载均衡(看厂商是否会在它们的标准库中利用该技术)。

少数更适合使用std::thread的地方
1.你需要访问底层线程实现的API,比如pthread或者Windows线程库,它们提供的API比C++更丰富(比如C++没有线程优先级或者亲和性的概念),为了访问底层API,std::thread通常会提供native_handler成员函数,而这是std::future中没有的。
2.你需要且有能力为你的应用优化线程用法。(比如开发的程序执行时的性能剖析已知,运行在硬件特性固定的平台上)
3.你需要实现超越C++并发API的线程技术。比如在C++实现中未提供的线程池的平台上实现线程池。


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

async的默认启动策略是,异步或者推迟的方式均可,可根据实际情况选择。

// 下面两者是同一个意思
auto fut1 = std::async(f);
auto fut2 = std::async(std::launch::async | std::launch::deferred, f)

仅设置std::launch::async时,代表函数f必须以异步方式运行,也就是一定在另一个线程上。
仅设置std::launch::deferred时,代表函数f只会在std::async所返回的future的get或者wait调用时才运行(wait for不行)。
(这是一个简化描述,std::future虽然只支持移动,但它可以构造std::shared_future,而他支持复制的,所以有可能指涉到f传递给的那个std::async的调用所返回的共享状态future对象可能和std::async返回的future对象并非同一个,所以说只能在std::async所返回的futureget或者wait调用时才运行。

当不指定launch时,是无法预知f是否会和t(调用get/wait的线程)并发运行,也无法预知f和t是不是不在一个线程上,甚至不知道f在哪里被调用。
所以不指定launch时,可能会导致f读写线程级局部存储(thread-local storage,TLS)时,不知道是取的哪一个线程的局部存储。
而这带来的问题可能在测试时不太好发现,因为一般在负载较重时,或者硬件层面面临超订或者线程耗尽威胁时,才会以deferred方式运行。

不指定launch的问题:
了解wait_for:https://www.sidney.wiki/cpp/916

using namespace std::literals;
void f() {
  std::this_thread::sleep_for(1s)
}
auto fut = std::async(f);
// 当async方式调用时,没有问题,可以走出。
// 当deferred方式调用时,wait_for只会返回std::future_status::deferred,将陷入死循环!!!
while (fut.waif_for(100ms) !=
       std::future_status::ready) {
}

解决方法:

auto fut = std::async(f);
if (fut.wait_for(0s) == std::future_status::deferred) {
  ... // 使用fut的wait或者get调用f
} else {
  while (fut.wait_for(100ms) != std::future_status::ready) {
  ... // 任务既没有推迟,也没有就绪,则做并发工作,直到任务就绪。
  }
  ... // fut就绪
}

所以仅在满足下面的条件才使用默认策略调用async
(1)任务不需要与调用get或者wait的线程并发执行。
(2)读/写哪个线程的thread_local变量并无影响。
(3)可以给出保证在std::async返回的future之上调用get或wait,或者可以接受任务永远不执行。
(4)使用wait_for或wait_until的代码会将任务被推迟的可能性纳入考量。

书写一个真正异步的调用函数(等同于async第一个参数传std::launch::async)

// C++11
template<typename F, typename... Ts>
inline
std::future<typename std::result_of<F(Ts...)>::type> // 注意使用std::result_of获取F(Ts)调用的返回结果型别!!
reallyAsync(F&& f, Ts&&... params) {
  return std::async(std::launch::async,
                    std::forward<F>(f),
                    std::forward<Ts>(params)...);
}

// C++14使用auto来推导返回值类型
template<typename F, typename... Ts>
inline
auto
reallyAsync(F&& f, Ts&&... params) {
  return std::async(std::launch::async,
                    std::forward<F>(f),
                    std::forward<Ts>(params)...);
}

// 调用
auto fut = reallyAsync(f);

PS:std::future<T>,这个T是返回值类型,可以用get获取,当不需要返回值时,可以用std::future<void>


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

1.不可联结的情况有四种
每个std::thread要么处于可联结状态,要么处于不可联结状态。
std::thread对象对应的底层线程处于阻塞或等待调度或运行结束,则是可联结的。
不可联结的std::thread包括(和null句柄是一致的)
(1)默认构造的std::thread,没有可执行的函数,也就没有对应的底层执行线程。
(2)已移动的std::thread
(3)已联结的std::thread,联结后,std::thread型别对象不再对应到已结束运行的底层执行线程。
(4)已分离的std::thread,分离操作会把std::thread型别对象和它对应的底层执行线程之间的连接断开。
2.可联结的重要性:可联结的线程对象的析构函数被调用,则主程序的执行也就终止了!!!!!
例子:

constexpr auto tenMillion = 10000000;
// C++14
constexpr auto tenMillion = 10'000'000; // C++14支持数字用单引号增强可读性
bool doWork(std::function<bool(int)> filter,
            int maxVal = tenMillion) {
  std::vector<int> goodVals;
  std::thread t([&filter, maxVal, &goodVals] { // 需要调整优先级,所以使用std::thread
    for (auto i = 0; i <= maxVal; ++i) {
      if (filter(i)) goodVals.push_back(i);
    }
  });
  auto nh = t.native_handle(); // 使用t的低值句柄设定t的优先级。条款35
  // 更好的做法是以暂停状态启动线程t(在它开始执行计算之前调整其优先级)
  ...
  if (conditionsAreSatisfied()) {
    t.join(); // 等t结束执行
    performComputation(goodVals);
    return true; // 计算已实施
  }
  return false; // 计算未实施
}

注意:如果conditionsAreSatisfied()返回了false或者抛出了异常,那么dowork末尾调用std::thread型别对象t的析构函数时,由于它处于可联结状态joinable,所以会导致主程序执行终止。
之所以在析构时执行这个终止程序的策略原因是,另外两种选项都可能更糟糕:
(1)隐式join。
如果std::thread选择析构时隐式join,则析构函数会等待底层异步执行线程完成,但可能会导致性能异常。比如conditionsAreSatisfied早已返回false了,但doWork还在等待所有值上遍历筛选。
(2)隐式detach(最可怕的)。
在这种情况下,std::thread会分离std::thread对象和底层执行线程的链接。但可能会造成莫名其妙的内存问题!!比如,这里的thread的第三个参数goodVals是引用捕获的局部变量,在doWork返回后,goodVals被析构了,但lambda式子仍然在修改goodVals的那一部分内存,如果这一部分内存又在别的地方使用,则会导致内存莫名其妙被改变。
所以,通过规定可联结的线程的析构函数会导致程序终止,可以制止上面两种情况的发生。
3.自定义std::thread销毁时是调用join还是detach
借助RAII(Resource Acquisition Is Initialization)的思想。(RAII的例子:stl容器(析构函数会析构容器内容并释放内存,标准智能指针,std::fstream型别对象(析构函数会关闭对应的文件)。

class ThreadRAII {
public:
  enum class DtorAction { join, detach };
  ThreadRAII(std::thread&& t, DtorAction a) // 构造函数只支持右值型别的std::thread
  // 注意,初始化列表顺序最好和声明顺序一致(不一致的话以声明顺序为准)。
  // 最好是thread后构造,先释放。
  // 因为初始化可能会依赖另一个成员变量,后析构可能会导致使用了已经析构的成员。
  : action(a), t(std::move(t)) {} // std::move的原因是std::thread是不可复制的。
  ~ThreadRAII() {
    if(t.joinable()) { // 要先确保可联结!!!针对不可联结的线程调用join或detach会导致未定义行为。
      if(action == DtorAction::join) {
        t.join();
      } else {
        t.detach();
      }
    }
  }
  // 因为定义了析构函数,所以为了移动可用,需要定义移动构造函数/移动赋值函数!!
  ThreadRAII(ThreadRAII&&) = default;
  ThreadRAII& operator=(ThreadRAII&&) = default;
  std::thread& get() { return t; } // 类似智能指针,因此可以不用重复thread的接口
private:
  DtorAction action;
  std::thread t;
};

注意:要先确保可联结,针对不可联结的线程调用join或detach会导致未定义行为。
确实有可能会导致data race,在t.joinable()的执行和join和detach的调用之前,另一个线程可能让t不可联结。但这种情况不用担心,因为data race不是发生在析构函数内,可能会发生在试图同时调用两个成员函数(一个析构函数,一个其他成员函数)的用户代码内。
一般的,在一个对象之上同时调用多个成员函数,只有当所有这些成员函数都是const成员函数才安全。
用户代码(选择join,性能异常相比于detach的危害稍微小一点):

bool doWork(std::function<bool(int)> filter,
            int maxVal = tenMillion) {
  std::vector<int> goodVals;
  ThreadRAII t(
    std::thread t([&filter, maxVal, &goodVals] { // 需要调整优先级,所以使用std::thread
      for (auto i = 0; i <= maxVal; ++i) {
        if (filter(i)) goodVals.push_back(i);
      }
    }),
    ThreadRAII::DtorAction::join
  );
  auto nh = t.get().native_handle(); // 使用t的低值句柄设定t的优先级。条款35
  // 更好的做法是以暂停状态启动线程t(在它开始执行计算之前调整其优先级)
  ...
  if (conditionsAreSatisfied()) {
    t.get().join(); // 等t结束执行
    performComputation(goodVals);
    return true; // 计算已实施
  }
  return false; // 计算未实施
}

ps:不过join并不只是会导致性能异常,还可能会导致程序失去响应(条款39)。最合适的解决方式是和异步执行的lambda式通信,当我们已经不再需要它运行时,它应该提前返回。C++11并不支持这种可中断线程,但《C++ Concurrency in Action》的书里(9.2节)有提到一个实现。


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

std::thread的型别对象和future对象都可以视作系统线程的句柄。
std::thread的型别对象析构函数行为:针对可联结的std::thread调用析构函数会导致程序终止。
future对象的析构函数行为:有可能是隐式join、有可能是隐式detach或什么都没有执行。

1.共享状态的理解
调用方<–future<–<–std::promise<–被调方,std::promise
https://www.sidney.wiki/cpp/922
被调用方的结果存储在哪里呢?
首先它不会存储在被调方的std::promise,因为那个对象对于被掉方是个局部变量,会在被掉方结束后被析构。
然后它也不会存储在调用方的future中,因为std::future出发可以创建出std::shared::future型别对象(被调方的结果型别所有权被转移到std::shared::future,类似于unique_ptr构建shared_ptr),但被调方的结果型别不都是可复制的。
所以,被调方的结果只能存储在两者外部的某个位置,这个位置被叫做共享状态。
调用方<–future<–<–共享状态(被调用方结果)<–<–std::promise<–被调方

2.future对象的析构函数行为
future对象的析构函数行为由与其关联的共享状态决定。
(1)指涉到经由std::async启动的未推迟任务的共享状态的最后一个future会保持阻塞,直到该任务结束。对底层异步执行任务而言,相当于执行了一个隐式join。(主程序会被阻塞直到异步运行的任务结束)
(2)其他所有future对象的析构函数只仅仅将future对象析构就结束了(同步任务相当于什么都不做)。对底层异步执行任务而言,相当于是执行了一次隐式的detach(隐式detach后,底层程序异常不会导致程序结束不会导致主程序终止)。
注意析构后相当于对共享状态里的引用计数实行了一次自减该共享状态可以由指涉它的future和被调用方的std::promise共同操纵。
详细分析第(1)种情况,需要同时满足以下三种情况才会隐式join:
1)future所指涉的共享状态是由于调用了std::async创建的。
2)该任务的启动策略是std::launch::async
3)该future是指涉到该共享状态的最后一个期值(主要是考虑std::shared_future型别对象)。
但没有API判断其指涉的共享状态是否诞生于std::async的调用,因此可能共享状态的析构是否导致主程序阻塞是未知的。
推论:

// 容器的析构函数可能会在其析构函数中阻塞
std::vector<std::future<void>> futs;
// Wiget的析构函数可能会在其析构函数中阻塞
class Widget {
public:
private:
  std::shared_future<double> fut;
};

3.还可以使用std::packaged_task产生共享状态,但不会触发特殊的析构函数行为。
std::packaged_task用法见
https://www.sidney.wiki/cpp/928
std::packaged_task也不能复制,但可以借助std::thread异步运行,或者可以通过()直接调用(同步调用)。

int calcValue();
{
  std::packaged_task<int()> pt(calcValue);
  auto fut = pt.get_future();
  std::thread t(std::move(pt));
  ... 
}

在…的代码中,对t的操作决定了析构函数的调用行为(注意,这里是条款37,thread的析构函数行为!!)。
(1)未对t实施任何操作,则t是可联结的,对它进行析构会导致主程序终止。
(2)对t实施了join,fut无须在析构函数中阻塞,因为在调用的代码中已经有过join。
(3)对t实施了detach,fut无须在析构函数中detach,因为在调用的代码中已经有过detach。


条款39 考虑针对一次性事件通信使用以void为模板型别实参的期值

多线程的同步方式:
1.条件变量
https://www.sidney.wiki/cpp/936

std::condition_variable cv;
std::mutex m;

// 检测任务代码
... // 检测事件
cv.notify_one(); // 通知反应任务(随机通知一个)
// 当需要通知多个反应任务时,cv.notify_all()通知所有的反应任务。

// 反应任务代码
{ // 临界区开始
  std::unique_lock<std::mutex> lk(m); // 为互斥量加锁
  cv.wait(lk); // 等待通知到来(m被释放)
  ... // 针对事件做出反应(m被锁定)
} // 临界区结束,m解锁

这里可能会产生代码异味(code smell):即使代码能够一时运作,某些东西可能也会不太对劲。
(1)如果检测任务在反应任务调用wait前就通知了条件变量。会导致唤醒丢失。
(2)反应任务的wait语句无法应对虚假唤醒。
虚假唤醒和唤醒丢失可以看
https://www.sidney.wiki/cpp/948
2.共享的布尔标志位(轮询)

std::atomic<bool> flag(false); // 共享的布尔标志位
// 检测任务代码
...
flag = true; // 通知反应任务

// 反应任务代码
while(!flag); // 等待事件
... // 针对事件做出反应

优点:不需要互斥体,不存在虚假唤醒等问题。
缺点:反应任务的轮询可能成本高昂,会占用了另一个任务本该可以用到的硬件线程;在每次开始运行以及其时间片结束时,都会产生语境切换的成本;它可能让一颗硬件核心持续运行,而那颗核心本来可以关掉以节省电能。
3.使用条件变量+一个flag去测试是否已经发生

// wait的第二个参数为false时,会阻塞,为true时,不会阻塞;
cv.wait(lk, []{ return 事件是否已经发生; });

标记位不需要用std::atomic,平凡的布尔量即可。

std::condition_variable cv;
std::mutex m;
bool flag(false);

... // 检测事件
{
  std::lock_guard<std::mutex> g(m);
  flag = true;
}
cv.notify_one();

// 反应任务
{
  std::unique_lock<std::mutex> lk(m);
  cv.wait(lk, []{ return flag; }); // lambda式应对虚假唤醒。
  ... // 针对事件做出反应
}

优点:在检测任务通知之前相应任务就开始等待也没关系,存在虚假唤醒也不影响,且不需要轮询。
缺点:检测任务和反应任务沟通方式奇特,需要flag+条件变量的双重通知。
4.摆脱条件变量,使用std::promise
future存在于调用者和被调用者之间。
std::promise不仅可以存在于调用者和被调用者,可以将信息从一处传输到任意的另一处。
https://www.sidney.wiki/cpp/922
如果std::promise(检测任务)和std::future(单个反应任务)或者std::shared_future(多个反应任务)是表示一种没有数据要通过信道发送的型别(仅需要通知),则可以使用void型别作为共享状态。
使用方式:

std::promise<void> p;
... // 检测事件
p.set_value(); // 通知反应任务

// 反应任务
p.get_future().wait(); // 等待p的期值
... // 针对事件做出反应

优点:不需要互斥量,检测任务是否在相应任务等待之前设置std::promise都可以,对虚假唤醒免疫。
缺点:(1)共享状态是动态分配的,需要承担在堆上动态分配和回收的成本。
(2)条件变量可以多次通信,而std::promise不能重复使用,只能一次性通信。可解决方式是在一开始就把所有与创建线程需要的开销都申请好(申请多个),尔后一旦要在线程上执行某些操作时可避免常规的线程创建延迟了。

扩展:
(1)暂停的线程实现(在暂停状态实施配置动作,如获取native_handler去调用posix线程或者windows线程的API,设置优先级或内核亲和性等)

std::promise<void> p;
void react();
void detect() {
  std::thread t([]{
    p.get_future().wait();
    react();
  });
  ... // 在这里线程处于暂停状态!!可以设置线程相关配置。
  p.set_value(); // t取消暂停,开始执行。
  ... // 做其他动作
  t.join(); // 使t置于不可联结的状态。
}

借助ThreadRAII实现detect函数如下:

void detect() {
  ThreadRAII tr(
    std::thread([]{
      p.get_future().wait();
      react();
    }),
    ThreadRAII::DtorAction::join); // 这里有风险
  ... // 在这里线程处于暂停状态。但如果这里抛出异常,set_value永远不会被调用,线程不会被完成,函数失去反应,tr的析构函数永远不会被完成!!!
  p.set_value(); // t取消暂停,开始执行。
  ... // 做其他动作
}

解决方式,可以参考:
博客:The View From Aristeia, ThreadRAII + Thread Suspension = Trouble?
(2)多个反应线程实现先暂停后取消。
每个反应线程都需要借助std::shared_future副本

std::promise<void> p;
void react();
void detect() {
  auto sf = p.get_future().share(); // sf型别是std::shared_future<void>
  std::vector<std::thread> vt; // 反应任务的容器
  for (int i = 0; i < threadsToRun; ++i) {
    vt.emplace_back([sf]{ // 需要使用emplace_back,参考条款42
      sf.wait(); // sf副本上的wait
      react();
    })
  }
  ... // 在这里线程处于暂停状态!!可以设置线程相关配置。
  p.set_value(); // t取消暂停,开始执行。
  ... // 做其他动作
  for (auto &t :vt) { // 使所有线程置于不可联结的状态。
    t.join();
  }
}

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

1.atomic用于多线程访问的数据
(1)atomic的所有成员函数(包括那些包含RMW的函数)都保证被其他线程视为原子的。

std::aotmic<int> ai(0); // 原子
ai = 10; // 原子
// 注意这里只能保证ai的读取时原子的,在读取ai的值和调用operator<<之间,ai的值可能已经被修改了!!!!!
// 但是由于operator<<会使用按值传递的int形参来输出,因此输出的值还是从ai读取的值!!!(如果按引用则不一定了)
std::cout << ai;
++ai; // 原子
--ai; // 原子

atomic和volatile多线程保证的区别如下:

std::atomic<int> ac(0);
volatile int vc(0);
/*线程1*       /*线程2*/
++ac;         ++ac;
++vc;         ++vc;

以上,ac最后的结果一定为2,但vc可能为1,因为下列操作可能交替执行(线程1读取vc的值为0,线程2读取vc的值为0,线程1对vc自增为1,线程2对vc自增为1),因此volatile在多线程中产生了data race;
(2)atomic可以限制编译器对指令的重新排序
atomic与内存序可以看这个wiki:
https://www.sidney.wiki/cpp/952
看下面的例子:

std::atomic<bool> valAvailable(false); // 1
auto imptValue = computeImportantValue(); // 2
valAvailable = true; // 3

编译器可能会对不相关的赋值进行重新排序,由于不指明内存序的atomic具有最严格的内存序,所以3一定在2后面(也即其他线程看到valAvailable为true时imptValue一定进行了修改)。
但如果使用volatile:

volatile bool valAvailable(false); // 1
auto imptValue = computeImportantValue(); // 2
valAvailable = true; // 3,其他线程可能将这个赋值操作视作在2之前

此时编译器可能会先执行3,再执行2,所以volatile对并发编程不起作用。

2.volatile
(1)volatile可以禁用编译器的优化,使其适用于特种内存。

int x;
auto y = x;
y = x; // 常规内存可能会直接用这一条的x赋值y。
x = 10;
x = 20; // 常规内存可能会消除x=10的操作。

而volatile告诉了编译器不要在此特种内存上进行任何优化。最常见的特种内存是用于内存映射IO的内存,这种内存的位置实际上是用于与外部设备(如传感器、显示器、打印机、网络端口)通信。
比如读取温度时:

// 第一次和第二次读取温度时发生了温度改变。
volatile int x;
x = 10; // 写入x
x = 20; // 再次写入x

值10和20可能对于不同的指令,如果第一个赋值被优化掉,就改变了命令序列了。
但注意,volatile int x; auto y = x;这里的y是auto型别推导结果,会被省略掉const和volatile饰词,所以这里的y的型别是int!!!
另外注意,std::atomic<int> x; auto y = x;这里的操作是错误的,因为std::atomic的复制操作被删除了。
因为x的读取和y的写入操作在硬件层面不能合成一个原子操作,所以x这个atomic不支持复制构造,也不支持复制赋值。
(2)volatile总是从内存读取数据,不从缓存或寄存器中读取数据。
而atomic可能从寄存器中读取数据:

std::atomic<int> y(x.load());
y.store(x.load());
// 编译器可以将x的值处处在寄存器中,而不是两次读取,如下所示:
register = x.load();
std::atomic<int> y(register);
y.store(register);

上述优化在volatile中不被允许!
3.volatile和atomic可以一起使用。
比如用于多个线程同时访问的内存映射I/O位置:

volatile std::atomic<int> vai; // 针对vai的操作是原子的,且不能被优化掉。

其他扩展wiki:atomic、volatile、内存屏障(内存栅栏)、缓存一致性
https://www.sidney.wiki/cpp/954


8 微调


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

1.重载、万能引用、按值传递 三种方式的分析:

// 途径一:针对左值和右值重载
class Widget {
public:
  void addName(const std::string& newName) {
    names.push_back(newName);
  }
  void addName(std::string&& newName) {
    names.push_back(std::move(newName));
  }
private:
  std::vector<std::string> names;
};
// 途径二:使用万能引用
class Widget {
public:
  template<typename T>
  void addName(T&& newName) {
    names.push_back(std::forward<T>(newName));
  }
...
};
// 途径三:按值传递
class Widget {
public:
  void addName(std::string newName) {
    names.push_back(std::move(newName));
  }
...
};

Widget w;
std::string name("Bart");
w.addName(name); // 传入左值
w.addName(name+"Jenne"); // 插入右值

// 另一个例子
// 右值,成本一次移动
class Widget {
public:
  void SetPtr(std::unique_ptr<std::string>&& ptr) {
    p = std::move(ptr);
  }
private:
  std::unique_ptr<std::string> p;
};
// 按值传递,成本两次移动!!(若传入右值)
class Widget {
public:
  void SetPtr(std::unique_ptr<std::string> ptr) {
    p = std::move(ptr);
  }
private:
  std::unique_ptr<std::string> p;
};

Widget w;
w.setPtr(std::make_unique<std::string>("Modern C++")); // 这里是传入右值

(1)针对左值和右值重载
缺点:产生程序足迹问题(目标代码占用内存的尺寸),需要两份函数声明、两份函数实现、两份函数文档、两份函数维护工作量。(当函数没有内联时,目标代码膨胀为两个函数)
成本:对于左值是一次复制,对于右值是一次移动
(2)万能引用
缺点:1)作为模板,addName的实现需要在头文件中。2)针对std::string和可以转型为std::string的型别也会产生不同的实例化结果,如果传入的实参型别不正确,编译器错误会很多比较难看。
成本:对于左值是一次复制,对于右值是一次移动。(如果调用方传递的实参不是std::string,被转发到适当的std::string构造函数,这是甚至没有移动或复值)
(3)按值传递
成本:对于左值,是一次复制加一次移动,对于右值,是两次移动。(无论左值右值,都多一次移动操作)

2.不适用于按值传递的场景:
(1)除了复制到容器里之前,addName还需要检查名字长短。(有成立条件的传递)

// 这种按值传递的方式,若判断条件不成立,会多一次构造和析构的成本(相比引用而言)。
class Widget {
public:
  void addName(std::string newName) {
    if ((newName.length() >= minLen) && (newName.length() <= maxLen)) {
      names.push_back(std::move(newName));     
    }
  }
private:
  std::vector<std::string> names;
};

(2)移动成本高时,因为总是会多一次移动
(3)若用赋值来实施形参赋值时,可能引用方式不需要重新申请内存,直接复用老内存;但使用按值传递且在传入参数是左值时,可能带来额外的内存分配和回收的成本。

class Password {
public:
  explicit Password(std::string pwd)
  : text(std::move(pwd)) {}
  // 值方式实现
  void changeTo(std::string newPwd) {
    text = std::move(newPwd);
  }
  // 引用方式实现
  void changeTo(const std::string& newPwd) {
    text = newPwd; // 在text.capacity() >= newPwd.size()时,可以复用text的内存
  }
private:
  std::string text;
};

std::string initPwd("abcdefghijklmnopqrstunwxyz");
Password p(initPwd);
std::string newPassword = "abcdefghijklmnopqrstunw";
p.changeTo(newPassword);

上述代码中,按值传递的代价包括额外的内存分配和回收成本,这样的成本在传入左值时才会发生。
(对于string、vector都可能有额外成本,且string还要考虑有没有SSO小字符串优化(条款29),以及所赋值的能否放进SSO缓冲区)
(4)按值传递很容易遇到切片问题(子类转父类)(甚至比影响性能更不能让人接受)

class Widget {...};
class SpecialWidget: public Widget {...};
void processWidget(Widget w);
SpecialWidget sw;
processWidget(sw); // processWidget看到的只是一个Widget而非SpecialWidget型别的对象!!

3.总结
所以按值传递需要的分析很复杂,因此一般的策略是:
总是采用重载或万能引用的而非按值传递,除非已确凿证明按值传递能够为所需的形参型别生成可接受效率的代码。
针对可复制的、移动成本低廉的型别,并且传入的函数总是对其实施复制这种特殊情况,在切片问题也无须担心的前提下,按值传递可以提供一个易于实现的替代方法,它和按引用传递的效率相近,但是避免了他们的不足。


条款42 考虑置入而非插入

1.置入和插入的区别

std::vector<std::string> vs;
vs.push_back("xyzzy");

上述push_back代码发生了两次构造函数调用,一次析构函数调用。
(1)从"xyzzy"这个字符串常量创建temp临时对象,这里会调用一次普通构造函数
(2)temp临时对象被传递给push_back的右值重载版本,这里会调用一次移动构造函数
(3)push_back返回的那一时刻,temp被析构,这里会调用一次析构函数
而如果使用emplace_back,则只会有一次普通的构造函数调用,因为emplace_back使用的是完美转发

vs.emplace_back("xyzzy");
vs.emplace_back(50, 'x');

容器对emplace的支持情况
emplace_back可以用于任何支持push_back的容器;
emplace_front可以用于任何支持push_front的容器;
任何支持insert操作(forward_list和array不支持插入操作)的容器都支持emplace操作;
关联容器提供了empalce_hint来补充它们带有“hint”迭代器的insert函数;
std::forward_list也提供了emplace_after与其insert_after一唱一和;

2.什么使用置入而非插入
(1)欲添加的值是以构造而非赋值方式加入容器

vs.emplace_back("xyzzy"); // 构造而非赋值

注意emplace的特殊情况!!

std::vector<std::string> vs;
vs.emplace(vs.begin(), "xyzzy");

上面这个代码的emplace实现是使用 placement new 在容器提供的位置原位构造元素。然而若要求的位置已被既存的元素占据(一般都是这种情况),则首先在另一位置构造被插入的元素,然后再将他移动赋值到要求的位置中。
由于通常是移动赋值,所以需要一个源的移动对象,所以需要创建一个临时对象作为移动源,因此这个时候置入的优势消失了。
(2)传递的实参型别与容器持有之物不同。
实参型别和容器持有之物不同时,插入操作需要创建和析构临时对象。
但当container<T>持有之物型别和T相同时,置入的优势也消失了。
(3)容器不太可能由于出现重复情况而拒绝待添加的新值。
当容器不允许重复值时(如set、map、unordered_set、unordered_map),因为需要检测某值是否已经在容器中,置入的实现通常会使用该新值创建一个节点,以遍将该节点与容器现有的节点比较,如果该值不存在,则将节点链入容器中,如果该节点存在,则置入会终止,节点也会实施析构,这相当于置入也有构造和析构的成本
(4)带自定义删除器的智能指针,不能采用置入,应该采用插入。
自定义删除器的智能指针,不能使用make函数构造(条款21)

ptrs.push_back(std::shared_ptr<Widget>(new Widget, killWidget));
// 也可以写成这种形式!!
ptrs.push_back({new Widget, killWidget});

上述代码都会构造一个temp的智能指针临时对象,push_back会按照引用方式接收temp,如果push_back时抛出了内存不足的异常,此异常会被传播到push_back之外,这个时候temp被析构,内存正常被回收。

ptrs.emplace_back({new Widget, killWidget}); // 异常时可能会发生内存泄露

上诉代码从new Widget的裸指针进行完美转发,一旦抛出内存不足的异常,该因此常传播到emplace_back之外后,裸指针丢失了,会发生内存泄漏。
裸指针初始化智能指针时,需要立即传递给资源管理对象的构造函数,make函数可以自动化这一点,但带自定义删除器的又无法使用make函数。上面的emplace_back完美转发推迟了资源管理对象的创建。
改进方案:

std::shared_ptr<Widget> spw(new Widget, killWidget);
ptrs.push_back(std::move(spw));
// 此时的置入不再有性能优势。
std::shared_ptr<Widget> spw(new Widget, killWidget);
ptrs.emplace_back(std::move(spw));

(5)置入还可能会导致一个特殊问题,置入函数可能会执行在插入函数中被explicit拒绝的型别转换

std::regex upperCaseWord("[A-Z]+"); // 正确,正确调用const char*指针的std::regex构造函数。
std::regex r(nullptr); // 可以通过编译,调用了const char*指针的std::regex构造函数,这里是直接初始化!!
std::regex r = nullptr; // 无法通过编译,const char*指针的std::regex构造函数以explicit声明,这里是复制初始化!!
std::vector<std::regex> regexes;
regexes.emplace_back(nullptr); // 竟然能通过编译,但含义是错误的!
regexes.push_back(nullptr); // 错误,无法通过编译。

因为emplace_back调用的是类似括号初始化的那种直接初始化的方式,所以能够调用const char*指针的std::regex构造函数。
push_back调用的是类似等号初始化的那种复制初始化的方式,不能调用explicit禁止的构造函数。
注意:std::regex r(nullptr); 直接初始化和std::regex r = nullptr;复制初始化的区别(条款7)。
等号是复制初始化,括号是直接初始化。括号的直接初始化一般会调用普通构造函数,而等号的复制初始化也可以调用普通的构造函数,只是不能调用带explicit的普通构造函数,且在不能复制的对象中也不能调用复制初始化。


1人评论了“Effective Modern C++ 笔记”

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

目录

Contents
滚动至顶部