文章目录
最近读完了Effective Modern C++ ,对move,forward,万能引用有了更加深刻的理解,相见恨晚的好书!
第一章 型别推导
条款1:理解模板类型推导
template<typename T>
void f(ParamType param);
f(expr); // 根据exper 推导 T and ParamType
- 1 ParamType 具有指针或引用类型,不是万能引用
- 2 ParamType是一个万能引用
- 3 ParamType非指针也非引用
1 ParamType 具有指针或引用类型,不是万能引用
template<typename T>
void f(T& param); // param is a reference
int x = 27; // x is an int
const int cx = x; // cx is a const int
const int& rx = x; // rx is a reference to x as a const int
f(x); //T的型别是int, param的型别是int&
f(cx); //T的型别是const int(常量性保留), param的型别是const int&
f(rx); //T的型别是const int(引用性忽略), param的型别是const int&
当人们向引用型别的形参传入const对象时,他们期望该对象保持其不可修改的属性,
也就是说,期望该形参成为const的引用型别。这也是为何向持有T&型别的模板传入const对象是安全的:该对象的常量性(constness)会成为T的型别推导结果的组成部分。
template<typename T>
void f(const T& param); // param is now a ref-to-const
int x = 27; // as before
const int cx = x; // as before
const int& rx = x; // as before
f(x); // T is int, param's type is const int&
f(cx); // T is int, param's type is const int&
f(rx); // T is int, param's type is const int&
如果传入指针
template<typename T>
void f(T* param); // param is now a pointer
int x = 27; // as before
const int *px = &x; // px is a ptr to x as a const int
f(&x); // T is int, param's type is int*
f(px); // T is const int, // param's type is const int*
2 ParamType 是万能引用
万能引用形参的声明方式类似右值引用(即在函数模板中持有型别形参T时,万能引用的声明型别写作T&&),但是当
1 实参传入左值,其表现会有所不同,进来的都搞成引用。
2 实参传入右值,T就是右值对应类型,不是引用。
template<typename T>
void f(T&¶m);//param现在是个万能引用(进来的都搞成引用)
int x = 27;
const int cx = x;
const int&rx = x;
f(x); //x是个左值,所以T的型别是int&,param的型别也是int&
f(cx); //cx是个左值,所以T的型别是const int& , //param的型别也是const int&
f(rx); //rx是个左值,所以T的型别是const int& , //param的型别也是const int&
* f(27); //27是个右值,所以T的型别是int, //这么一来,param的型别就成了int&8
关键之处在于,万能引用形参的型别推导规则不同于左值引用和右值引用形参。具体地,当遇到万能引用时,型别推导规则会区分实参是左值还是右值。而非万能引用是从来不会作这样的区分的。
3 ParamType非指针也非引用
忽略引用,忽略常量性,忽略volatile
template<typename T>
void f(T param); // param is now passed by value
int x = 27; // as before
const int cx = x; // as before
const int& rx = x; // as before
f(x); // T's and param's types are both int
f(cx); // T's and param's types are again both int
f(rx); // T's and param's types are still both int
const char* const ptr = // ptr 指向const对象的const指针
f(ptr) // T's and param's被推导为 const char*。 指向对象的常量性保留,自身的常量性忽略
数组实参
const char name[] = "J. P. Briggs"; // name's type is const char[13]
const char * ptrToName = name; // array decays to pointer
//情况3
template<typename T>
void f(T param);
f(name); //T: const char *
//情况2
template<typename T>
void f(T& param);
f(name); //T: const char [13] param: 推导为该数组的一个引用 const char (&)[13]
以编译期常量形式返回数组尺寸
//(该数组形参未起名字,因为我们只关心其含有的元素个数)
template<typename T,std:size_t N> //关于constexpr
constexpr std:size t arraySize(T (&)[N])noexcept //和noexcept
{ //的说明,请参考下文
return N;
}
int keyVals[] = { 1, 3, 7, 9, 11, 22, 35 };
现代c++中的数组,推荐使用 std::array<int, arraySize(keyVals)> mappedVals;
函数实参
void someFunc(int, double); // someFunc is a function;
// type is void(int, double)
template<typename T>
void f1(T param); // in f1, param passed by value
template<typename T>
void f2(T& param); // in f2, param passed by ref
f1(someFunc); // param deduced as ptr-to-func; // type is void (*)(int, double)
f2(someFunc); // param deduced as ref-to-func; // type is void (&)(int, double)
- 在模板型别推导过程中,具有引用型别的实参会被当成非引用型别来处理。换言之,其引用性会被忽略。
- 对万能引用形参进行推导时,左值实参会进行特殊处理。
- 对按值传递的形参进行推导时,若实参型别中带有const或volatile饰词,则它们还是会被当作不带const或volatile饰词的型别来处理。
- 在模板型别推导过程中,数组或函数型别的实参会退化成对应的指针,除非它们被用来初始化引用。
条款2 理解auto型别推导
auto x = 27; //情况3(x既非指针也非引用)
const auto cx = x; //情况3(cx同样既非指针也非引用)
const auto& rx = x; //情况1(rx是个引用,但不是万能引用)
auto&& uref1 = x; // x is int and lvalue, // so uref1's type is int&
auto&& uref2 = cx; // cx is const int and lvalue, // so uref2's type is const int&
auto&& uref3 = 27; // 27 is int and rvalue, // so uref3's type is int&&
数组和函数
const char name[] = "R. N. Briggs"; // name's type is const char[13]
auto arr1 = name; // arr1's type is const char*
auto& arr2 = name; // arr2's type is // const char (&)[13]
void someFunc(int, double); // someFunc is a function; // type is void(int, double)
auto func1 = someFunc; // func1's type is // void (*)(int, double)
auto& func2 = someFunc; // func2's type is // void (&)(int, double)
auto和模板型别推导真正的唯一区别在于,auto会假定用大括号括起的初始化表达式代表一个std:initializer list,但模板型别推导却不会。
auto x1 = 27; // type is int, value is 27
auto x2(27); // ditto
auto x3 = { 27 }; // type is std::initializer_list<int>, // value is { 27 }
auto x4{ 27 }; // ditto
C++14允许使用auto来说明函数返回值需要推导(参见条款3),而且C++14中的lambda式也会在形参声明中用到auto。然而,这些auto用法是在使用模板型别推导而非auto型别推导。所以,带有uto返回值的函数若要返回一个大括号括起的初始化表达式,是通不过编译的:
auto createInitList()
{
return { 1, 2, 3 }; // error: can't deduce type
} // for { 1, 2, 3 }
std::vector<int> v;
…
auto resetV = [&v](const auto& newValue) { v = newValue; }; // C++14
…
resetV({ 1, 2, 3 }); // error! can't deduce type // for { 1, 2, 3 }
- 在一般情况下,aut0型别推导和模板型别推导是一模一样的,但是auto型别推导会假定用大括号括起的初始化表达式代表一个std::initializer list,但模板型别推导却不会。
- 在函数返回值或lambda式的形参中使用auto,意思是使用模板型别推导而非aut0型别推导。
条款3 理解decltype
vector<int> v; // decltype(v) is vector<int>
if (v[0] == 0) … // decltype(v[0]) is int&
C++I1中,decltype的主要用途在于声明那些返回值型别依赖于形参型别的函数模板。举个例子,假设我们想要撰写一个函数,其形参中包括一个容器,支持[]语法。
template<typename Container, typename Index> // works, but requires refinement
auto authAndAccess(Container& c, Index i) -> decltype(c[i])
{
authenticateUser();
return c[i];
}
一般来说,含有型别T的对象的容器,其operator[]会返回T&。std::deque就属于这种情况,而std::vector也几乎总是属于这种情况。只有std::vector<bool>对应的operator[]并不返回bool&,而返回一个全新对象。至于这样处理的原因和具体处
在函数名字之前使用的那个auto和型别推导没有任何关系。它只为说明这里使用了C++11中的返回值型别尾序语法(trailing return type syntax),即该函数的返回值型别将在形参列表之后(在“->”之后)。尾序返回值的好处在于,在指定返回值型别时可以使用函数形参。比如,在authAndAccess中,我们在指定返回值型别时就可以使用c和i。如果我们还是使用传统的返回值型别先序语法,那c和i会由于还未声明,从而无法使用。
c++14中可以去掉返回值型别尾序语法
template<typename Container, typename Index> // works, but requires refinement
auto authAndAccess(Container& c, Index i) -> decltype(c[i])
{
authenticateUser();
return c[i]; //引用性会被忽略,返回一个int值,右值
}
template<typename Container, typename Index>
decltype(auto) authAndAccess(Container& c, Index i)
{
authenticateUser();
return c[i];
}
声明变量同样可以
Widget w;
const Widget& cw = w;
auto myWidget1 = cw; //myWidget1 Widget
decltype(auto) myWidget2 = cw; //myWidget2 const Widget&
满足实参传入右值,要用万能引用,对应的返回值就要std::forward
template<typename Container, typename Index> // c++14 最终版本
decltype(auto)
authAndAccess(Container&& c, Index i)
{
authenticateUser();
return std::forward<Container>(c)[i];
}
template<typename Container, typename Index> // c++11 最终版本
auto
authAndAccess(Container&& c, Index i) -> decltype(std::forward<Container>(c)[i])
{
authenticateUser();
return std::forward<Container>(c)[i];
}
×是一个变量名字,所以decltype(x)的结果是int。但是如果把名字x放入一对小括号中,就得到了一个比仅有名字更复杂的表达式“(x)”。作为一个名字,×是个左值,而在C+的定义中,表达式(x)也是一个左值,所以decltype((x)的结果就成了int&。仅仅是把一个名字放入一对小括号,就改变了decltype的推导结果!
decltype(auto) f1()
{
int x = 0;
…
return x; // decltype(x) is int, so f1 returns int
}
decltype(auto) f2()
{
int x = 0;
…
return (x); // decltype((x)) is int&, so f2 returns int&
}
第二章 auto
条款5 优先选用auto
哈希表中的std::pair(也就是std::unordered_map本身)的型别并不是std::pair<std::string,int>,而是std:pair<const std:string,.int>。
条款6:当auto推导的型别不符合要求时,使用带显式型别的初始化物习惯用法
auto index = static_cast<int>(d * c.size()); //std::vector<int>::size_type
第三章 转向现代c++
条款7 创建对象时的()和{}
如果你想以任意数量的实参来创建一个任意型别的对象,对于传入的参数无法断定是{}还是()初始化。
template<typename T, // type of object to create
typename... Ts> // types of arguments to use
void doSomeWork(Ts&&... params)
{
create local T object from params...
…
}
T localObject(std::forward<Ts>(params)...); // using parens
T localObject{std::forward<Ts>(params)...}; // using braces
这正是标准库函数std:make unique和std::make shared(参见条款2l)所面临的问题。而这些函数解决问题的办法是在内部使用了小括号,并把这个决定以文档的形式广而告之,作为其接口的组成部分。(提前说明)
- 大括号初始化可以应用的语境最为宽泛,可以阻止隐式窄化型别转换,还对最令人苦恼之解析语法免疫。
- 在构造函数重载决议期间,只要有任何可能,大括号初始化物就会与带有std::initializer list型别的形参相匹配,即使其他重载版本有着貌似更加匹配的形参表。
- 使用小括号还是大括号,会造成结果大相径庭的一个例子是:使用两个实参来创建一个std:vector<数值型别>对象。
- 在模板内容进行对象创建时,到底应该使用小括号还是大括号会成为一个棘手问题。
条款8:优先选用nullptr,而非O或NULL
- 相对于0或NULL,优先选用nullptr.。
- 避免在整型和指针型别之间重载。
template<typename FuncType,
typename MuxType,
typename PtrType>
auto lockAndCall(FuncType func, MuxType& mutex, PtrType ptr) -> decltype(func(ptr))
{
MuxGuard g(mutex);
return func(ptr);
}
条款9 优先选用别名声明using a = b,而非typedef
typedef
typedef std::unique_ptr<std::unordered_map<std::string, std::string>> UPtrMapSS;
typedef void (*FP)(int, const std::string&);
//template
template<typename T>
struct MyAllocList {
typedef std::list<T, MyAlloc<T>> type;
};
MyAllocList<Widget>::type lw; //业务代码
//在模板内部使用typedef创建一个链表,容纳的对象型别由模板形参指定
template<typename T>
class Widget {
private:
typename MyAllocList<T>::type list;
};
//这里,MyAllocList.<T>::type代表一个依赖于模板型别形参(T)的型别,所以MyAllocList<T>:type称为带依赖型别,而C++中众多讨喜的规则之一就是带依赖型别必须前面加个typename。
using
using UPtrMapSS = std::unique_ptr<std::unordered_map<std::string, std::string>>;
using FP = void (*)(int, const std::string&);
//template
template<typename T>
using MyAllocList = std::list<T, MyAlloc<T>>;
MyAllocList<Widget> lw; //业务代码
//在模板内部使用
template<typename T>
class Widget {
private:
MyAllocList<T> list; // no "typename", no "::type"
};
//当编译器处理到Widget模板并遇见了MyAllocList<T>(即别名模板的运用)时,它们知道MyAllocList<T>是一个型别的名字,因为MyA11 ocList是个别名模板:它必然命名了一个型别。综上,MyAllocList<T>是个非依赖性型别,所以typename饰词既不要求也不允许。
C++11以**型别特征(type trait)**的形式给了程序员以执行此类变换的工具。型别特征是在头文件<type traits>给出的一整套模板。由typedef实现,使用的时候需要加typename。c++14不需要。
std:remove_const<T>:type //C++11:const T→T
std:remove_const_t<T>//C+14中的等价物
std:remove_reference<T>:type//C++11:T&/T&&→T
std:remove_reference_t<T>//C++14中的等价物
std:add_lvalue_reference<T>:type//C++11:T→T&
std:add_lvalue_reference_t<T>//C+14中的等价物
//c++11中模拟c++14
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;
- typedef不支持模板化,但别名声明支持。
- 别名模板可以让人免写“:type”后缀,并且在模板内,对于内嵌typedef的引用经常要求加上typename前缀。
条款10 优先选用限定作用域的枚举型别
不限作用域
// unscoped enum
enum Color { black, white, red }; // unscoped enum
std::vector<std::size_t> // func. returning
primeFactors(std::size_t x); // prime factors of x
Color c = red;
…
if (c < 14.5) { // compare Color to double (!)
auto factors = // compute prime factors
primeFactors(c); // of a Color (!)
…
}
enum Color; //错误
限定作用域
//scoped
enum class Color { black, white, red }; // enum is now scoped
Color c = Color::red; // as before, but
… // with scope qualifier
if (c < 14.5) { // error! can't compare
// Color and double
auto factors = // error! can't pass Color to
primeFactors(c); // function expecting std::size_t
…
}
//使用时需要强转
if (static_cast<double>(c) < 14.5) { // odd code, but
// it's valid
auto factors = // suspect, but
primeFactors(static_cast<std::size_t>(c)); // it compiles
…
}
enum class Color; //正确
条款11 优先选用删除函数,而非private未定义函数
template <class charT, class traits = char_traits<charT> >
class basic_ios : public ios_base
{
public:
…
private:
basic_ios(const basic_ios& ); // not defined
basic_ios& operator=(const basic_ios&); // not defined
};
//删除函数
template <class charT, class traits = char_traits<charT> >
class basic_ios : public ios_base {
public:
…
basic_ios(const basic_ios& ) = delete;
basic_ios& operator=(const basic_ios&) = delete;
…
};
删除函数的一个重要优点在于,任何函数都能成为删除函数,但只有成员函数能声明成private。
//为想要滤掉的型别创建删除版本
bool isLucky(int number); // original function
bool isLucky(char) = delete; // reject chars
bool isLucky(bool) = delete; // reject bools
bool isLucky(double) = delete;
很有意思的是,如果是类内部的函数模板,并且你想通过private声明来禁用某些具现(C++98中的老生常谈),这是做不到的,因为你不可能给予成员函数模板的某个特化以不同于主模板的访问层级。删除函数可以做到。
template<typename T>
void processPointer(T* ptr);
template<>
void processPointer<void>(void*) = delete;
template<>
void processPointer<char>(char*) = delete;
条款12:为意在改写的函数添加override声明
成员函数引用饰词使得对于左值和右值对象(*this)的处理能够区分开来。
class Widget
{
public:
…
void doWork() &; // this version of doWork applies
// only when *this is an lvalue
void doWork() &&; // this version of doWork applies
}; // only when *this is an rvalue
…
Widget makeWidget(); // factory function (returns rvalue)
Widget w; // normal object (an lvalue)
…
w.doWork(); // calls Widget::doWork for lvalues (i.e., Widget::doWork &)
makeWidget().doWork(); // calls Widget::doWork for rvalues (i.e., Widget::doWork &&)
条款13:优先选用constjterator,而非iterator
std::vector<int> values;
auto it = std::find(values.cbegin(),values.cend(), 1983); // and cend
values.insert(it, 1998);
对于没有cbegin的容器,通用模板
template<typename C, typename V>
void findAndInsert(C& container, // in container, find
const V& targetVal, /* first occurrence */
const V& insertVal) /* of targetVal, then */
{ // insert insertVal
using std::cbegin; // there
using std::cend;
auto it = std::find(cbegin(container), // non-member cbegin
cend(container), // non-member cend
targetVal);
container.insert(it, insertVal);
}
非成员函数版本的cbegin的实现
template <class C>
auto cbegin(const C& container)->decltype(std::begin(container))
{
return std::begin(container); // see explanation below
}
条款14:只要函数不会发射异常,就为其加上noexcept声明
能移动则移动,必须复制才复制
- noexcept声明是函数接口的组成部分,这意味着调用方可能会对它有依赖。
- 相对于不带noexcept声明的函数,带有noexcept声明的函数有更多机会得到优化。
- noexcept性质对于移动操作、swap、函数释放函数和析构函数最有价值。
- 大多数函数都是异常中立的,不具备noexcept性质。
条款15:只要有可能使用constexpr,就使用它
constexpr对象
所有constexpr对象都是const对象,而并非所有的const对象都是constexpr对象。可以毫无顾虑地认为constexpr对象是在编译期间决定的)。在编译阶段就已知的值拥有种种特权。比如,它们可能被放置在只读内存里.
constexpr函数
constexpr // pow's a constexpr func
int pow(int base, int exp) noexcept // that never throws
{
… // impl is below
}
constexpr auto numConds = 5; // # of conditions
std::array<int, pow(3, numConds)> results; // results has 3^numConds elements
- **constexpr函数可以用在要求编译期常量的语境中。**在这样的语境中,若你传给一个constexpr函数的实参值是在编译期已知的,则结果也会在编译期间计算出来。如果任何一个实参值在编译期未知,则你的代码将无法通过编译。
- 在调用constexpr函数时,若传入的值有一个或多个在编译期未知,则它的运作方式和普通函数无异,亦即它也是在运行期执行结果的计算。这意味着,如果函数执行的是同样的操作,仅仅应用的语境一个是要求编译期常量的,一个是用于所有其他值的话,那就不必写两个函数。constexpr函数就可以同时满足所有需求。
在C++11中,constexpr函数不得包含多于一个可执行语句,即一条return语句。这个限制听上去限制极大,但其实没有那么大,因为我们还有两条技巧可以用来拓展constexpr函数的表达力。首先,条件运算符?:可以用于需要使用if-else语句;其次,用到循环的地方可以用递归代替。所以,pow可以像下面这样实现:
constexpr int pow(int base, int exp) noexcept
{
return (exp == 0 ? 1 : base * pow(base, exp - 1));
}
constexpr int pow(int base, int exp) noexcept // C++14
{
auto result = 1;
for (int i = 0; i < exp; ++i) result *= base;
return result;
}
constexpr函数仅限于传入和返回字面型别(literal type),意思就是这样的型别能够持有编译期可以决议的值。在C++11中,所有的内建型别,除了void,都符合这个条件。但是用户自定义型别同样可能也是字面型别,因为它的构造函数和其他成员函数可能也是constexpr函数。在c++11中,constexpr函数都隐式的声明为const了(成员函数的const属性,不能修改其对象)。
//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; }
void setX(double newX) noexcept { x = newX; }
void setY(double newY) noexcept { y = newY; }
private:
double x, y;
};
//c++14解除限制
constexpr Point reflection(const Point& p) noexcept
{
Point result; // create non-const Point
result.setX(-p.xValue()); // set its x and y values
result.setY(-p.yValue());
return result; // return copy of it
}
- constexpr对象都具备const属性,并由编译期已知的值完成初始化。
- constexpr函数在调用时若传入的实参值是编译期已知的,则会产出编译期结果。
- 比起非constexpr对象或constexpr函数而言,constexpr对象或是constexpr函数可以用在一个作用域更广的语境中。
除非保证函数的constexpr永远不会去掉,否则不要加constexpr修饰函数
条款16:保证const成员函数的线程安全性
-
保证const成员函数的线程安全性,除非可以确信它们不会用在并发语境中。
-
运用std::atomic型别的变量会比运用互斥量提供更好的性能,但前者仅适用对单个变量或内存区域的操作。
* 条款17:理解特种成员函数的生成机制
两种复制操作(拷贝构造和拷贝赋值)是彼此独立的:声明了其中一个,并不会阻止编译器生成另一个。所以,如果你声明了一个复制构造函数,同时未声明复制赋值运算符,并撰写了要求复制赋值的代码,则编译器会为你生成复制赋值运算符。类似地,如果你声明了一个复制赋值运算符,同时未声明复制构造函数,而撰写了要求复制构造的代码,则编译器会为你生成复制构造函数。这在C++98中成立,在C+11中仍成立。
两种移动操作(移动构造和移动赋值)并不彼此独立:声明了其中一个,就会阻止编译器生成另一个。这种机制的理由在于,假设你声明了一个移动构造函数,你实际上表明移动操作的实现方式将会与编译器生成的默认按成员移动的移动构造函数多少有些不同。而若是按成员进行的移动构造操作有不合用之处的话,那么按成员进行的移动赋值运算符极有可能也会有不合用之处。综上,声明一个移动构造函数会阻止编译器去生成移动赋值运算符,而声明一个移动赋值运算符也会阻止编译器去生成移动构造函数。
一旦显式声明了复制操作,这个类也就不再会生成移动操作了。这样的判断的依据在于,声明复制操作(无论是复制构造还是复制赋值)的行为表明了对象的常规复制途径(按成员复制)对于该类并不适用。编译器从而判定,既然按成员复制不适用于复制操作,则按成员移动极有可能也不适用于移动操作。反之亦然。一旦声明了移动操作(无论是移动构造还是移动赋值),编译器就会废除复制操作(废除的方式是删除它们,参见条款11)。
只要用户声明了析构函数,就不会生成移动操作。
移动操作的生成条件,仅当一下三者同时成立。
该类未声明任何复制操作。
该类未声明任何移动操作。
该类未声明任何析构函数。
通过把析构函数声明成虚的来达到拥有虚析构函数的目的。通常情况下,虚析构函数的默认实现就是正确的,而“=default”则是表达这一点的很好方式。不过,一旦用户声明了析构函数,移动操作的生成就被抑制了,而如果可移动性是能够支持的,加上“=defau1t”就能够再次给予编译器以生成移动操作的机会。声明移动操作又会废除复制操作,所以如果还要可复制性,就再加一轮“=default”来实现这个愿望吧:
class Base
{
public:
virtual ~Base() = default; // make dtor virtual
Base(Base&&) = default; // support moving
Base& operator=(Base&&) = default;
Base(const Base&) = default; // support copying
Base& operator=(const Base&) = default;
…
};
总而言之,C++11中,支配特种成员函数的机制如下:
- 默认构造函数:与C++98的机制相同。仅当类中不包含用户声明的构造函数时才生成。
- 析构函数:与C++98的机制基本相同,唯一的区别在于析构函数默认为noexcept(参见条款14)。与C++98的机制相同,仅当基类的析构函数为虚的,派生类的析构函数才是虚的。
- 复制构造函数:运行期行为与C++98相同:按成员进行非静态数据成员的复制构造。仅当类中不包含用户声明的复制构造函数时才生成。如果该类声明了移动操作,则复制构造函数将被删除。在已经存在复制赋值运算符或析构函数的条件下,仍然生成复制构造函数已经成为了被废弃的行为。
- 复制赋值运算符:运行期行为与C++98相同:按成员进行非静态数据成员的复制赋值。仅当类中不包含用户声明的复制赋值运算符时才生成。如果该类声明了移动操作,则复制构造函数将被删除。在已经存在复制构造函数或析构函数的条件下,仍然生成复制赋值运算符已经成为了被废弃的行为。
- 移动构造函数和移动赋值运算符:都按成员进行非静态数据成员的移动操作。仅当类中不包含用户声明的复制操作、移动操作和析构函数时才生成。
- 特种成员函数是指那些C++会自行生成的成员函数:默认构造函数、析构函数、复制操作,以及移动操作。
- 移动操作仅当类中未包含用户显式声明的复制操作、移动操作和析构函数时才生成。
- 复制构造函数仅当类中不包含用户显式声明的复制构造函数时才生成,如果该类声明了移动操作则复制构造函数将被删除。复制赋值运算符仅当类中不包含用户显式声明的复制赋值运算符才生成,如果该类声明了移动操作则复制赋值运算符将被删除。在已经存在显式声明的析构函数的条件下,生成复制操作已经成为了被废弃的行为。
- 成员函数模板在任何情况下都不会抑制特种成员函数的生成。
第四章 智能指针
条款18:使用std::unique_ptr管理具备专属所有权的资源
class Investment { … };
class Stock: public Investment { … };
class Bond: public Investment { … };
class RealEstate: public Investment { … };
template<typename... Ts> // return std::unique_ptr
std::unique_ptr<Investment> // to an object created
makeInvestment(Ts&&... params); // from the given args
c++11传入deleter
auto delInvmt = [](Investment* pInvestment) // custom
{ // deleter
makeLogEntry(pInvestment); // (a lambda
delete pInvestment; // expression)
};
template<typename... Ts> // revised
std::unique_ptr<Investment, decltype(delInvmt)> // return type
makeInvestment(Ts&&... params)
{
std::unique_ptr<Investment, decltype(delInvmt)> pInv(nullptr, delInvmt); // ptr to be returned
if ( /* a Stock object should be created */ )
{
pInv.reset(new Stock(std::forward<Ts>(params)...));
}
else if ( /* a Bond object should be created */ )
{
pInv.reset(new Bond(std::forward<Ts>(params)...));
}
else if ( /* a RealEstate object should be created */ )
{
pInv.reset(new RealEstate(std::forward<Ts>(params)...));
}
return pInv;
}
c++14传入deleter
template<typename... Ts>
auto makeInvestment(Ts&&... params) // C++14
{
auto delInvmt = [](Investment* pInvestment) // this is now
{ // inside
makeLogEntry(pInvestment); // makedelete pInvestment; // Investment
};
std::unique_ptr<Investment, decltype(delInvmt)> pInv(nullptr, delInvmt); // as before
if ( … ) // as before
{
pInv.reset(new Stock(std::forward<Ts>(params)...));
}
else if ( … ) // as before
{
pInv.reset(new Bond(std::forward<Ts>(params)...));
}
else if ( … ) // as before
{
pInv.reset(new RealEstate(std::forward<Ts>(params)...));
}
return pInv; // as before
}
lambda形式的deleter比函数形式的更节约存储尺寸
auto delInvmt1 = [](Investment* pInvestment) // custom deleter as stateless lambda
{
makeLogEntry(pInvestment);
delete pInvestment;
};
template<typename... Ts> // return type
std::unique_ptr<Investment, decltype(delInvmt1)> makeInvestment(Ts&&... args); // has size of Investment*
//---------------------------------------------------------------------------------------------------
void delInvmt2(Investment* pInvestment) // custom deleter as function
{
makeLogEntry(pInvestment);
delete pInvestment;
}
template<typename... Ts> // return type has size of Investment* plus at least size of function pointer!
std::unique_ptr<Investment, void (*)(Investment*)> makeInvestment(Ts&&... params);
- std::unique ptr是小巧、高速的、具备只移型别的智能指针,对托管资源实施专属所有权语义。
- 默认地,资源析构采用delete运算符来实现,但可以指定自定义删除器。有状态的删除器和采用函数指针实现的删除器会增加std::unique_ptr型别的对象尺寸。
- 将std:unique_ptr转换成std::shared_ptr是容易实现的。
https://learn.microsoft.com/zh-cn/cpp/cpp/how-to-create-and-use-unique-ptr-instances?view=msvc-170
https://www.cnblogs.com/DswCnblog/p/5628195.html
条款19:使用std::shared_ptr管理具备共享所有权的资源
引用计数
-
std::shared_ptr的尺寸是裸指针的两倍。因为它们内部既包含一个指涉到该资源的裸指针,也包含一个指涉到该资源的引用计数的裸指针
-
引用计数的内存必须动态分配。从概念上来说,引用计数与被指涉到的对象相关联,然而被指涉到的对象对此却一无所知。因此它们没有存储引用计数的位置[令人愉快的是,这么一来任何型别的对象(甚至内建型别)都可以由std:sharedptr托管]。条款21会解释,std:shared _ptr若是由std:make_ptr创建,可以避免动态分配的成本。然而仍有一些场景下,不可以使用std:make_ptr。但无论是不是使用std:make_ptr,引用计数都会作为动态分配的数据来存储。
-
引用计数的递增和递减必须是原子操作,因为在不同的线程中可能存在并发的读写器。例如,在某个线程中指涉到某资源的某个std:shared_ptr可能正在执行其析构函数(因此其所指资源的引用计数会递减),而在另一个线程中,一个指涉到同一对象的std:shared ptr有可能同时正在被复制(因此该引用计数又会递增)。原子操作一般都比非原子操作慢,所以即使引用计数通常只有一个字长,也应当假设读写它们成本比较高昂的。
为什么不总是递增引用计数。移动构造函数,这就是原因所在。从一个已有std:shared ptr移动构造一个新的std:shared ptr会将源std::shared _ptr置空,这意味着一旦新的std:shared ptr产生后,原有的std::shared_ptr将不再指涉到其资源,结果是不需要进行任何引用计数操作。因此,移动std:shared _ptr比复制它们要快:复制要求递增引用计数,而移动则不需要。这一点对于构造和赋值操作同样成立,所以,移动构造函数比复制构造函数快,移动赋值比复制赋值快。
auto loggingDel = [](Widget *pw) // custom deleter
{ // (as in Item 18)
makeLogEntry(pw);
delete pw;
};
std::unique_ptr< Widget, decltype(loggingDel)> upw(new Widget, loggingDel);
// deleter type is part of ptr type
std::shared_ptr<Widget> spw(new Widget, loggingDel);
// deleter type is not part of ptr type
shared_ptr和unique_ptr的区别
自定义析构器的型别会影响unique_ptr的型别,不会影响shared_ptr的型别。
auto customDeleter1 = [](Widget *pw) { … }; // custom deleters,
auto customDeleter2 = [](Widget *pw) { … }; // each with a different type
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 };
与std:unique ptr的另一点不同,是自定义析构器不会改变std::shared_ptr的尺寸。无论析构器是怎样的型别,std::shared ptr对象的尺寸都相当于裸指针的两倍。这是个很棒的消息,但也可能会令你隐隐不安。自定义析构器有可能是函数对象,而函数对象可以包含任意数量的数据,这意味着它们的尺寸可能是任意大小。std::shared_ptr如何能够在不使用更多内存的前提下,指涉到任意尺寸的析构器?
智能指针的内存状态
std::shared_ptr<T>
| 指向T型别对象的指针| ---> |T型别的对象|
| 指向控制块的指针 | ---> | 引用计数 | 弱计数 | 其他数据(自定义删除器,分配器) |
控制块规则如下
- std:make shared(参见条款21)总是创建一个控制块。std::make shared会生产出一个用以指涉到的新对象,因此在调用std::make shared的时刻,显然不会有针对该对象的控制块存在。
- 从具备专属所有权的指针(即std:unique_ptr或std:auto_ptr指针)出发构造一个std::shared_ptr时,会创建一个控制块。专属所有权指针不使用控制块,因此不应该存在所指涉到的对象来说不应存在控制块(作为构造过程的一部分,std::shared_ptr被指定了其所指涉到的对象的所有权,因此那个专属所有权的智能指针会被置空)
- 当std:shared_ptr构造函数使用裸指针作为实参来调用时,它会创建一个控制块。如果想从一个已经拥有控制块的对象出发来创建一个std:shared_ptr,你大概会传递一个std:shared_ptr或std:weak_ptr(参见条款20)而非裸指针作为构造函数的实参。如果给std:shared_ptr的构造函数传递std::sharedptr或std:weak_ptr作为实参,则不会创建新的控制块,因为它们可以依赖传入的智能指针以指涉到任意所需的控制块。
so,禁止从一个裸指针来构造多个std::shared_ptr。
当裸指针为this时,它会多少个创建一个控制块? 即当A* a被shared_ptr托管的时候,如何在func获取自身的shared_ptr成了问题.
使用 : public std::enable_shared_from_this<Widget>
std::vector<std::shared_ptr<Widget>> processedWidgets;
class Widget {
public:
void process()
{
processedWidgets.emplace_back(this); //this裸指针传进入了,规则3 会创建一个控制块
}
};
std:enable_shared_from_this定义了一个的成员函数,它会创建一个std:shared_ptr指涉到当前对象,但同时不会重复创建控制块。这个成员函数的名字是enable_shared_from_this, 每当你需要一个和this指针指涉到相同对象的std:shared_ptr时,都可以在成员函数中使用它。Widget:process的一个安全实现如下:
void process()
{
processedWidgets.emplace_back(shared_from_this()); //this裸指针传进入了,规则3 会创建一个控制块
}
本质通过继承一个类,这个类本质上会给被管理的object上加一个指向计数器的weak ptr,于是就可以正确地增加引用计数而不是搞出2个独立的计数器
class Widget: public std::enable_shared_from_this<Widget> {
public:
// 将实参完美转发给private构造函数的工厂函数
template<typename... Ts>
static std::shared_ptr<Widget> create(Ts&&... params);
…
void process(); // as before
…
private:
… // ctors
};
- std:shared ptr提供方便的手段,实现了任意资源在共享所有权语义下进行生命周期管理的垃圾回收。
- 与std:unique ptr相比,std::shared ptr的尺寸通常是裸指针尺寸的两倍,它还会带来控制块的开销,并要求原子化的引用计数操作。
- 默认的资源析构通过delete运算符进行,但同时也支持定制删除器。删除器的型别对std::shared ptr的型别没有影响。
- 避免使用裸指针型别的变量来创建std::shared_ptr指针。
条款20:对于类似std::shared_ptr但有可能空悬的指针使用std::weak_ptr
如果loadWidget是成本高昂(例如,因为其执行了文件或数据库的I/O操作),并且ID会被频繁地重复使用的话,一个合理优化是撰写一个能够完成loadWwidget的工作,但又能缓存结果的函数。然而用缓存所有用过的Widget造成缓存拥塞,可能本身就会引起性能问题,因此另一个合理的优化就是在缓存的Widget不再有用时将其删除。
std::shared_ptr<const Widget> fastLoadWidget(WidgetID id)
{
static std::unordered_map<WidgetID, std::weak_ptr<const Widget>> cache;
auto objPtr = cache[id].lock(); // objPtr is std::shared_ptr
// to cached object (or null
// if object's not in cache)
if (!objPtr) { // if not in cache, load it cache it
objPtr = loadWidget(id);
cache[id] = objPtr;
}
return objPtr; //当外边不用时,里边也就失效了。适合于频繁变化的场景做缓存。
}
从效率的角度来看,std::weak_ptr和std:shared_ptr从本质上来说是一致的。std:weak_ptr的对象和std:shared_ptr的对象尺寸相同,它们和std::shared_ptr使用同样的控制块(参见条款19),其构造,析构,赋值操作都包含了对引用计数的原子操作。这种说法可能会令你惊讶,因为在本条款开头我曾提过,std::weak_ptr不参与引用计数操作。其实我的说法与此有别,我说的是std:weak_ptr不干涉对象的共享所有权,因此不会影响所指涉到的对象的引用计数。实际上控制块里还有第二个引用计数,std:weak_ptr操作的就是这第二个引用计数。更多细节请参考条款21。
- 使用std::weak ptr来代替可能空悬的std:shared ptr。
- std::weak_ptr可能的用武之地包括缓存,观察者列表,,以及避免std:shared_ptr指针环路。
条款21:优先选用std::make_unique和std::make_shared,而非直接使用new
- 避免内存泄漏
processWidget(std::shared_ptr<Widget>(new Widget), computePriority()); //潜在的资源泄露
编译器优化顺序为
实施“new Widget”。
执行computePriority。
运行std:shared ptr构造函数。
如果生成了这样的代码,并且在运行期computePriority产生了异常,那么由第一步动态分配的Widget会被泄漏,因为它将永远不会被存储到在第三步才接管的std:shared_ptr中去。make_shared一步完成。
- 内存分配次数减少
条款19解释过,每个std:shared_ptr会指涉到一个控制块,除了其他东西之外,这个控制块包含了与所指涉到的对象相关联的引用计数。控制块的内存是std::shared_ptr的构造函数进行分配的。因此,直接使用new表达式的话,除了要为Widget进行一次内存分配,还要为与其相关联的控制块再进行一次内存分配。
限制:
1 make_shared 默认小括号,不支持初始化列表
2 make不支持指定deleter
条款22:使用Pimpl习惯用法时,将特殊成员函数的定义放到实现文件中
有场景如下
class Widget { // in header "widget.h"
public:
Widget();
…
private:
std::string name;
std::vector<double> data;
Gadget g1, g2, g3; // Gadget is some user-
}; // defined type
因为Widget的数据成员属于std:string、std:vector和Gadget等多种型别,这些型别所对应的头文件必须存在,Widget才能通过编译,这就说明Widget的客户必须include<string>、<vector>,以及gadget.h。这些头文件增加了Widget的客户的编译时间,此外,它们也使得这些客户依赖于这些头文件的内容。假如某个头文件的内容发生了改变,则Widget的客户必须重新编译。标准头文件<string>和<vector>不会经常改变,但gadget,h却有可能会经常修订。
c++98 减少依赖
头文件
class Widget { // still in header "widget.h"
public:
Widget();
~Widget(); // dtor is needed—see below 必须实现了
…
private:
struct Impl; // declare implementation struct
Impl *pImpl; // and pointer to it
};
源文件
#include "widget.h" // in impl. file "widget.cpp"
#include "gadget.h"
#include <string>
#include <vector>
struct Widget::Impl
{ // definition of Widget::Impl
std::string name; // with data members formerly
std::vector<double> data; // in Widget
Gadget g1, g2, g3;
};
Widget::Widget() // allocate data members for
: pImpl(new Impl) // this Widget object
{}
Widget::~Widget() // destroy data members for
{ delete pImpl; } // this object //必须实现,因为在这里需要delete释放资源
这里,我把#include指令展示出来,是为了说明对std::string、std::vector和Gadget所对应的头文件的总体依赖仍然存在。然而,这些依赖已经从widget.h(对Widget客户可见并由他们使用)转移到了widget.cpp中(因而只对Widget实现者可见并被使用)。我也已经高亮标注了动态分配和回收Impl对象的代码。由于当widget被析构时需要回收该对象,所以就要求Widget具备一个析构函数。
但是我展示的是C++98的代码,它们散发着上一个千年的腐朽气息。其中使用了裸指针、裸new运算符和裸delete运算符,一切都是如此地…赤裸裸。本章的主旨在于优先选用智能指针,而非裸指针。如果我们需要的是在Widget构造函数里动态分配一个Widget::Impl对象,同时在Widget析构时自动释放该对象,那么std:uniqueptr(参见条款18)正是我们所需要的工具。
c++11/14
用std:unique_ptr替代指涉到Impl的裸指针以后,头文件的代码就会长成这样:
头文件
class Widget { // in "widget.h"
public:
Widget();
…
private:
struct Impl;
std::unique_ptr<Impl> pImpl; // use smart pointer
}; // instead of raw pointer
源文件
#include "widget.h" // in "widget.cpp"
#include "gadget.h"
#include <string>
#include <vector>
struct Widget::Impl
{ // as before
std::string name;
std::vector<double> data;
Gadget g1, g2, g3;
};
Widget::Widget() // per Item 21, create
: pImpl(std::make_unique<Impl>()) // std::unique_ptr
{} // via std::make_unique
想要编译通过需要额外加代码
该问题是由w被析构时(例如,离开作用域时)所生成的代码引起的。在那一时刻,析构函数被调用,在使用了std::unique ptr的类定义里,我们未声明析构函数,因为无须为其撰写代码。根据编译器生成特种成员函数的基本规则(参见条款17),编译器为我们生成了一个析构函数。在该析构函数内,编译器会插入代码来调用Widget的数据成员pImpl。pImpl是个std:unique_ptrWidget:Impl型别的对象,即一个使用了默认析构器的std::unique_ptr。默认析构器是在std:unique_ptr内部使用delete运算符来针对裸指针实施析构的函数。然而,在实施delete运算符之前,典型的实现会使用C++11中的static assert去确保裸指针未指涉到非完整型别。这么一来,当编译器为Widget w的析构函数产生代码时,通常就会遇到一个失败的static assert,从而导致了错误信息的产生。这个错误信息和w被析构的位置有关,因为Widget的析构函数与其他编译器产生的特种成员函数一样,基本上隐式inline的。这个编译错误通常会标示在w生成的那一代码行,因为正是这行源代码的显式创建对象导致了后来的隐式析构。
为解决这一问题,只需保证在生成析构std:unique<Widget::Impl>代码处,Widget::Impl是个完整型别即可。只要型别的定义可以被看到,它就是完整的。而Widget:Impl的定义位于widget.cpp中的。因此,成功编译的关键在于让编译器看到Widget的析构函数的函数体(即,编译器将要生成代码来析构std:unique_ptr型别的数据成员之处)的位置在widget…cpp内部的Widget:Impl定义之后。
class Widget { // as before, in "widget.h"
public:
Widget();
~Widget(); // declaration only
…
private: // as before
struct Impl;
std::unique_ptr<Impl> pImpl;
};
析构函数最后再定义
#include "widget.h" // as before, in "widget.cpp"
#include "gadget.h"
#include <string>
#include <vector>
struct Widget::Impl { // as before, definition of
std::string name; // Widget::Impl
std::vector<double> data;
Gadget g1, g2, g3;
};
Widget::Widget() // as before
: pImpl(std::make_unique<Impl>())
{}
Widget::~Widget() // ~Widget definition
{}
//或者
Widget::~Widget() = default; // same effect as above
此时定义了析构函数,但是如果需要支持移动操作,就必须自己声明该函数。如果函数实现写在了头文件中。
这种手法会导致和类中没有声明析构函数一样的问题,产生该问题的基本原因也相同。编译器生成的移动赋值操作需要在重新赋值前析构pImpl指涉到的对象,但在Widget的头文件里pImpl指涉到的是个非完整型别。move构造函数出问题的原因有所不同,这里的问题在于,编译器会在move构造函数内抛出异常的事件中生成析构pImpl的代码,而对pImpl析构要求Impl具备完整型别。
//头文件
Widget(Widget&& rhs); // declarations
Widget& operator=(Widget&& rhs); // only
//源文件
Widget::Widget(Widget&& rhs) = default; // defini
Widget& Widget::operator=(Widget&& rhs) = default; // tions
如果想要支持深拷贝
Widget::Widget(const Widget& rhs) // copy ctor
: pImpl(std::make_unique<Impl>(*rhs.pImpl))
{}
Widget& Widget::operator=(const Widget& rhs) // copy operator=
{
*pImpl = *rhs.pImpl;
return *this;
}
PS: https://github.com/ShiqiYu/CPP/issues/32 可以借助解引用操作变通地对 std::unique_ptr 进行深拷贝 不一定是安全的操作
#include <iostream>
#include <memory>
// You shouldn't get an instance of it!
class UnsafeClass {
public:
virtual void do_nothing() const {
printf ("[[UNSAFE OP!!!]]\n");
}
};
class Derived : public UnsafeClass {
public:
void do_nothing() const override {
printf ("Derived: do nothing. \n");
}
};
int main() {
using namespace std;
unique_ptr < UnsafeClass > ptr = make_unique < Derived > ();
// tell me, what would happens when you call it!
ptr->do_nothing();
// do a copy!
unique_ptr < UnsafeClass > ptr2 = make_unique < decltype(ptr) :: element_type > ( *ptr );
// successfully? or not?
ptr2->do_nothing();
}
原理
为达到实现Pimpl习惯用法的目的,应该选用的是std::unique_ptr智能指针,因为在对象内部(例如,在Widget内部)的pImpl指针拥有相应的实现对象(例如,Widget:Impl对象)的专属所有权。值得指出的是,如果我们在这里使用std:share_ptr而非std:unique_ptr来实现pImpl,则本条款的建议不再适用。在这个前提下,无须在Widget中声明析构函数。而由于没有用户自定义的析构函数,编译器会很乐意生成移动操作并精确地按我们想要的方式运作。给定widget.h中的代码如下:
std:unique_ptr和std:shared_ptr这两种智能指针在实现pImpl指针行为时的不同,源自它们对于自定义析构器的支持的不同。对于std::unique_ptr而言,析构器型别是智能指针型别的一部分,这使得编译器会产生更小尺寸的运行期数据结构以及更快速的运行期代码。如此高效带来的后果是,欲使用编译器生成的特种函数(例如,析构函数或移动操作),就要求其指涉到的型别必须是完整型别。而对于std::shared_ptr而言,析构器的型别并非智能指针型别的一部分,这就需要更大尺寸的运行时期数据结构以及更慢一些的目标代码,但在使用编译器生成的特种函数时,其指涉到的型别却并不要求是完整型别。
- Pipl惯用法通过降低类的客户和类实现者之间的依赖性,减少了构建遍数。
- 对于采用std::unique _ptr来实现的pImpl指针,须在类的头文件中声明特种成员函数,但在实现文件中实现它们。即使默认函数实现有着正确行为,也必须这样做。
- 上述建议仅适用于std:unique_ptr,但并不适用std:shared_ptr。
第五章 右值引用、移动语义和完美转发
移动语义使得编译器得以使用不那么昂贵的移动操作,来替换昂贵的复制操作。同复制构造函数、复制赋值运算符给予人们控制对象复制的具体意义的能力一样,移动构造函数和移动赋值运算符也给予人们控制对象移动语义的能力。移动语义也使得创建只移型别对象成为可能,这些型别包括std::unique_ptr、std::future和std:thread等。
完美转发使得人们可以撰写接受任意实参的函数模板,并将其转发到其他函数,目标函数会接受到与转发函数所接受的完全相同的实参。
右值引用是将这两个风马牛不相及的语言特性胶合起来的底层语言机制,正是它使得移动语义和完美转发成为了可能
在阅读本章中的条款时,一定要把这一点铭记在心:形参总是左值,即使其型别是右值引用。即,给定函数形如:void f(Widget&&w); 它的形参也是个左值。
* 条款23:理解std::move和std::forward
std:move并不进行任何移动,std:forward也不进行任何转发。这两者在运行期都无所作为。它们不会生成任何可执行代码,连一个字节都不会生成。
std::move和std::forward都是仅仅执行强制型别转换的函数(其实是函数模板)。
std:move无条件地将实参强制转换成右值
std:forward则仅在某个特定条件满足时才执行同一个强制转换
* move 原理与实现
c++11中std::move的示例非标准实现,remove_reference<T>::type&&
底层是个形参为万能引用的模板,返回值是T去掉引用属性后的&&,remove_reference<T>::type&&
std::move不仅不实际移动任何东西,甚至不保证经过其强制型别转换后的对象具备可移动的能力(必要条件非const)。关于针对任意对象实施过std::move的结果,唯一可以确定的是,该结果会是个右值。
template<typename T> // in namespace std
typename remove_reference<T>::type&&
move(T&& param)
{
using ReturnType = typename remove_reference<T>::type&&; // alias declaration;see Item 9
return static_cast<ReturnType>(param);
}
函数返回值的“&&”部分,暗示着std::move返回的是个右值引用。但是,如条款28所言,如果T碰巧是个左值引用的话,那么T&&就成了左值引用。为了避免这种情况发生,它将型别特征(参见条款9) std::remove reference应用于T,从而保证“&应用在一个非引用型别之上。这么一来,就可以确保std::move返回的是右值引用,而这一点十分重要,因为从该函数返回的右值引用肯定是右值。综上所述,std::move将实参强制转换成了右值,而这就是该函数全部的所作所为。
c++14中的实现,std::move更好的名称是rvalue_cast,做的是强制型别转换,不做移动
template<typename T> // C++14; still in
decltype(auto) move(T&& param) // namespace std
{
using ReturnType = remove_reference_t<T>&&;
return static_cast<ReturnType>(param);
}
特例
class Annotation {
public:
explicit Annotation(const std::string text)
…
};
假如其有一个成员变量,企图再初始化列表里使用move来移动构造
class Annotation {
public:
explicit Annotation(const std::string text) : value(std::move(text)) // "move" text into value; this code
{ … } // doesn't do what it seems to!
…
private:
std::string value;
};
实际上,text并非被移动,还是被复制入value的。text已经被std:move强制转换成为一个右值。但是,text是被声明为const std:string的,所以在强制转换之前,是个左值const std:string, 而强制转换的结果是个右值const std:string&& , 经过这番折腾以后常量性保留了下来。
class string { // std::string is actually a
public: // typedef for std::basic_string<char>
…
string(const string& rhs); // copy ctor
string(string&& rhs); // move ctor
…
};
在Annotation的构造函数的成员初始化列表中,std:move(text)的结果是个const std:string型别的右值。这个右值无法传递给std::string的移动构造函数,因为移动构造函数只能接受非常量std::string型别的右值引用作为形参。可是,这样一个右值却可以传递给复制构造函数,因为指涉到常量的左值引用允许绑定到一个常量右值型别的形参。因此,成员初始化最终会调用的是std::string的复制构造函数,即使text已经被转换成了一个右值!这种行为对于维持常量正确性至关重要,因为将值移出对象通常会改动该对象,所以语言不应该允许常量对象传递到有可能改动它们的函数(例如移动构造函数)
从本例中,可以习得两点经验:
- 首先,如果想取得对某个对象执行移动操作的能力,则不要将其声明为常量,因为针对常量对象执行的移动操作将一声不响地变换成复制操作;
- 其次,std::move不仅不实际移动任何东西,甚至不保证经过其强制型别转换后的对象具备可移动的能力(必要条件非const)。关于针对任意对象实施过std::move的结果,唯一可以确定的是,该结果会是个右值。
forward
std::forward的身世与std::move类似,只是std::move会无条件地将其实参强制转换为右值型别不同,std::forward仅在特定条件下才实施这样的强制型别转换。换言之,std::forward是一个有条件强制型别转换。
void process(const Widget& lvalArg); // process lvalues
void process(Widget&& rvalArg); // process rvalues
template<typename T> // template that passes
void logAndProcess(T&& param) // param to process
{
auto now = // get current time
std::chrono::system_clock::now();
makeLogEntry("Calling 'process'", now);
process(std::forward<T>(param));
}
Widget w;
logAndProcess(w); // call with lvalue
logAndProcess(std::move(w)); // call with rvalue
在logAndProcess内,形参param被传递给函数process。而process依据形参是左值还是右值型别进行了重载,所以我们很自然地会期望,当调用logAndProcess时若传人的是个左值,则该左值可以在被传递给process函数仍被当作一个左值;而当调用logAndProcess时若传入的是个右值,则调用是process取用右值型别的那个重载版本。
但是,所有函数形参皆为左值,param亦不例外。是故,所有logAndProcess内对process的调用都会是取用了左值型别的那个重载版本。为了避免这种结果,就需要一种机制,当且仅当用来初始化param的实参(即传递给logAndProcess的实参)是个右值的条件下,把param强制转换成右值型别。这恰恰就是std:forward所做的一切。这就是为何说std::forward是有条件强制型别转换:仅当其实参是使用右值完成初始化时,它才会执行向右值型别的强制型别转换。
上述代码中,std::forward是如何分辨param是通过左值还是右值完成了初始化的呢?一句话:该信息是被编码到logAndProcess的模板形参T中的。该形参被传递给std::forward后,随即由后者将编码了信息恢复出来。具体的原理细节参见条款28。
总结
更为重要的是,使用std::move所要传达的意思是无条件地向右值型别的强制型别转换,而使用std::forward则想说明仅仅对绑定到右值的引用实施向右值型别的强制型别转换。这是两个非常不同的行为。前者是典型地为移动操作做铺垫,而后者仅仅是传递(转发)一个对象到另一个函数,而在此过程中无论该对象原始型别具备左值性(lvalueness)和右值性(rvalueness.),都保持原样。这两个行为是如此不同,因而最好使用两个不同函数(以及函数名字)来区分这两者。
- std::moVe实施的是无条件的向右值型别的强制型别转换。就其本身而言,它不会执行移动操作。
- 仅当传入的实参被绑定到右值时,std::forward才针对该实参实施向右值型别的强制型别转换。
- 在运行期,std::move和std:forward都不会做任何操作。
条款24 区分万能引用和右值引用
实际上,“T&&”有两种不同的含义。其中一种含义,理所当然,是右值引用。正如期望,它们仅仅会绑定到右值,主要在于识别出可移对象。
“T&&”的另一种含义,则表示其既可以是右值引用,亦可以是左值引用,二者居一。带有这种含义的引用在代码中形如右值引用(即T&&),但它们可以像左值引用一样运作(即T&)。这种双重特性使之既可以绑定到右值(如右值引用),也可以绑定到左值(如左值引用)。犹有进者,它们也可以绑定到const对象或非const对象,以及volatile对象或非volatile对象,甚至绑定到那些既带有const又带有volatile饰词的对象。它们几乎可以绑定到万事万物。这种拥有史无前例的灵活性的引用值得拥有一个独特的名字。我称之为万能引用(universal reference)。
万能引用场景,都涉及到了型别推导
template<typename T>
void f(T&& param); //通过推导得到param型别
auto&& var2 = var1; //通过推导得到var2型别
非万能引用型别
void f(Widget&& param);
Widget&& var1 = Widget();//右值引用
- 万能引用
因为万能引用首先是个引用,所以初始化是必需的。万能引用的初始化物会决定它代表的是个左值还是右值引用:如果初始化物是右值,万能引用就会对应到一个右值引用;如果初始化物是左值,万能引用就会对应到一个左值引用。对于作为函数形参的万能引用而言,初始化物在调用处提供:
template<typename T>
void f(T&& param); // param is a universal reference
Widget w;
f(w); // lvalue passed to f; param's type is Widget& (i.e., an lvalue reference)
f(std::move(w)); // rvalue passed to f; param's type is Widget&& (i.e., an rvalue reference)
- 像但不是万能引用
param的型别声明的形式不是“T&&”,而是“std:vector<T>&&”,这就排除了param是个万能引用的可能性。
template<typename T>
void f(std::vector<T>&& param); // param is an rvalue reference
有const也不再符合万能引用的格式
template<typename T>
void f(const T&& param); // param is an rvalue reference
位于模板内,不一定涉及到型别推导,非万能引用。push back作为vector的一部分,如果不存在特定的vector实例,则它也无从存在。
该实例的具现型别完全决定了push back的声明型别。
template<class T, class Allocator = allocator<T>> // from C++
class vector { // Standards
public:
void push_back(T&& x);
…
};
//若有
std::vector<Widget> v;
//则模板被具现为如下,直接右值引用
class vector<Widget, allocator<Widget>> {
public:
void push_back(Widget&& x); // rvalue reference
…
};
- 万能引用
template<class T, class Allocator = allocator<T>> // still from
class vector { // C++
public: // Standards
template <class... Args>
void emplace_back(Args&&... args);
…
};
其中的型别形参Args独立于vector的型别形参T,所以,Args必须在每次emplace_back被调用时进行推导(好吧,实际上Args是个形参包,而不是单个的型别形参,但是它符合我们讨论的目的,仍然可以把它当作单个的型别形参处理)
即万能引用的形式必须是“T&&”,但没必要一定要取“T”这个名字。
更准确地说,声明为auto&&型别的变量都是万能引用,因为它们肯定涉及型别推导并且肯定有正确的形式(“T&&”)。
auto timeFuncInvocation =
[](auto&& func, auto&&... params) // C++14
{
//start timer;
std::forward<decltype(func)>(func)( // invoke func
std::forward<decltype(params)>(params)... // on params
);
//stop timer and record elapsed time;
};
不必担心,本条款的重点在于该lambda表达式所声明的auto&&型别的形参。func是一个可以被绑定到任何可调用对象的万能引用,可以是左值或右值。params是0个或更多的万能引用(亦即一个万能引用形参包),可以被绑定任何数量的任意型别的对象。多亏有了auto万能引用,才取得了“timeFuncInvocation可以计算出大多数任何函数的执行时间”这样的成果(欲知“任何”和“大多数任何”的区别何在,参见条款30)
总结
- 如果函数模板形参具备T&&型别,并且T的型别系推导而来,或如果对象使用auto&&声明其型别,则该形参或对象就是个万能引用。
- 如果型别声明并不精确地具备type&&的形式,或者型别推导并未发生,则type&&就代表右值引用。
- 若采用右值来初始化万能引用,就会得到一个右值引用。若采用左值来初始化万能引用,就会得到一个左值引用。
条款25: 针对右值引用实施std::move,针对万能引用实施std::forward
当转发右值引用给其他函数时,应当对其实施向右值的无条件强制型别转换(通过std:move),因为它们一定绑定到右值;
class Widget {
public:
Widget(Widget&& rhs) // rhs is rvalue reference
: name(std::move(rhs.name)),
p(std::move(rhs.p))
{ … }
…
private:
std::string name;
std::shared_ptr<SomeDataStructure> p;
};
而当转发万能引用时,应当对其实施向右值的有条件强制型别转换(通过std:forward),因为它们不一定绑定到右值。
class Widget {
public:
template<typename T>
void setName(T&& newName) // newName is
{ name = std::forward<T>(newName); } // universal reference
…
};
Matrix // by-value return
operator+(Matrix&& lhs, const Matrix& rhs)
{
lhs += rhs;
return std::move(lhs); // move lhs into
} // return value
Matrix // as above
operator+(Matrix&& lhs, const Matrix& rhs)
{
lhs += rhs;
return lhs; // copy lhs into
} // return value
假如Matrix不支持移动,将其转换为右值也并无大碍,因为右值也就是会通过Matrix的复制构造函数来完成复制而已(参见条款23)。但若Matrix后来被修改为支持移动操作,则下次编译完成后,operator-+就能自动获益。这个例子中,对函数按值返回的右值引用实施std:move,不会付出任何代价(可能还会有不小收益,有没有收益要看对应类是否支持移动构造,以及移动构造的收益)
负优化
编译器的RVO优化,。人们很久之前就认识到,makeWidget的“复制”版本可以通过直接在为函数返回值分配的内存上创建局部变量W来避免复制之,这就是我们熟知的返回值优化。
内容具体是说:编译器若要在一个按值返回的函数里省略对局部对象注2的复制(或者移动),则需要满足两个前提条件:①局部对象型别和函数返回值型别相同;②返回的就是局部对象本身。据此,我们再重新审视下makeWidget的“复制”版本:
Widget makeWidget() // Moving version of makeWidget
{
Widget w;
…
return std::move(w); // move w into return value
} // (don't do this!)
std::move()后,返回一个局部对象的引用,不在满足实施RVO的前提条件2,编译器必须把w移入函数返回值的位置,本来企图通过对即将返回的局部变量实施std::move来帮助编译器进行优化,然而实际上却适得其反地限制了本来可用的编译优化选项!
总结
- 针对右值引用的最后一次使用实施std::moVe,针对万能引用的最后一次使用实施std::forward。
- 作为按值返回的函数的右值引用和万能引用,依上一条所述采取相同行为。
- 若局部对象可能适用于返回值优化,则请勿针对其实施std::moVe或std::forward.。
条款26:避免依万能引用型别进行重载
看上去很美
template<typename T>
void logAndAdd(T&& name)
{
auto now = std::chrono::system_clock::now();
log(now, "logAndAdd");
names.emplace(std::forward<T>(name));
}
一旦加入特化int型别,当传入short型时,会优先选用万能引用
void logAndAdd(int idx) // new overload
{
auto now = std::chrono::system_clock::now();
log(now, "logAndAdd");
names.emplace(nameFromIdx(idx));
}
总结
- 把万能引用作为重载候选型别,几乎总会让该重载版本在始料未及的情况下被调用到。
- 完美转发构造函数的问题尤其严重,因为对于非常量的左值型别而言,它们一般都会形成相对于复制构造函数的更佳匹配,并且它们还会劫持派生类中对基类的复制和移动构造函数的调用。
条款27:熟悉依万能引用型别进行重载的替代方案
舍弃重载
logAndAddIndex版本
传递const T& 型别的形参
一种替代方法是回归C++98,使用传递左值常量引用型别来代替传递万能引用型别。其实,这就是条款26做的第一种问题解决尝试。这种方法的缺点是达不到我们想要的高效率。不过,在已知依万能引用型别进行重载会带来的不良效应以后,放弃部分效率来保持简洁性不失为仍有一定吸引力的权衡结果。
传值
explicit
class Person {
public:
explicit Person(std::string n) // replaces T&& ctor; see Item 41 for use of std::move
: name(std::move(n)) {}
explicit Person(int idx) // as before
: name(nameFromIdx(idx)) {}
…
private:
std::string name;
};
由于std::string型别并没有只接受单个整型形参的构造函数,所有int或者类int型别(例如,std::size t,short,long)的实参都会汇集到接受int型别的那个构造函数重载版本的调用。类似地,所有std:string型别的实参(包括可以构造出std::string型别对象之物,例如字面量“Ruth”)都会被传递给接受std:string型别的那个构造函数。
标签分派
型别特征,std::remove reference,正如其名,也正如所需:它会移除型别所附的的一切引用饰词。因此,正确的logAndAdd如下:防止传入int&导致问题
型别std::false type和std::true type就是所谓“标签”,强制重载决议按我们想要的方向推进。
template<typename T>
void logAndAdd(T&& name)
{
logAndAddImpl(
std::forward<T>(name),
std::is_integral<typename std::remove_reference<T>::type>()
);
}
template<typename T> // non-integral argument:
void logAndAddImpl(T&& name, std::false_type)
{ // add it to
auto now = std::chrono::system_clock::now(); // global data
log(now, "logAndAdd"); // structure
names.emplace(std::forward<T>(name));
}
void logAndAddImpl(int idx, std::true_type)
{ // 整型实参,查找名字,并调用logAndAdd
logAndAdd(nameFromIdx(idx));
}
就目的而言,更重要的不在于标签分派的工作细节,而是它让我们得以将万能引用和重载加以组合却不会引发条款26所描述的问题的能力。分派函数logAndAdd接受的是个不受限制的万能引用形参,但该函数并未重载。实现函数logAndAddImpl则实施了重载,每个重载版本都接受一个万能引用形参,但重载决议却并不仅对这个万能引用形参有依赖,还对标签有依赖,而标签值则又加以设计以保证可以命中匹配的函数不会超过一个。这样设计的结果是,只有标签值才决定了调用的是哪个重载版本。万能引用形参总是给出精确匹配这个事实,也就无关紧要了。
对接受万能引用的模板施加限制
只要提供了一个接受万能引用形参的构造函数,会导致复制非常量左值时总会调用到万能引用构造函数(而非复制构造函数)。同一条款也解释了,如果基类中声明了一个完美转发构造函数,则派生类以传统方式实现其复制和移动构造函数时,总会调用到该构造函数,尽管正确行为应该是调用到基类的复制和移动构造函数。
std::enable_if可以强制编译器表现出来的行为如同特定的模板不存在一般。这样的模板称为禁用的。默认地,所有模板都是启用的。可是,实施了std:enable_if的模板只会在满足了std::enable_if指定的条件的前提下才会启用。在我们讨论的情况下,仅在传递给完美转发构造函数的型别不是Person时才启用它。如果传递的型别是Person,我们就会想要禁用完美转发构造函数(即,让编译器忽略它),因为这么一来,就会由类的复制或移动构造函数来接手处理这次调用了,而这才是在用一个Person型别的对象来初始化另一个Person型别的对象时应有的结果。
语法
class Person {
public:
template<typename T, typename = typename std::enable_if<condition>::type> //满足条件才启用的模板
explicit Person(T&& n);
...
};
std:enable_if,也要看“SFINAE”:Substitution failure is not an error (替换失败不是错误)。因为SFINAE是使得std:enable if得以运作的技术。https://zhuanlan.zhihu.com/p/21314708
如果我们更加精细地加以反思,我们说仅当T不是Person型别时才启用Person类中模板构造函数到底是什么意思的话,我们就会意识到,在审查T的时应该忽略:
-
它是否是个引用。为判定万能引用构造函数是否应该被启用,型别Person、Person&和Person&&都应该与Person作相同处理。
-
它是否带有const和volatile饰词。对目前关注的目的而言,const Person、volatile Person和const volatile Person都应该与Person作相同处理。
这意味着在判定T是否与Person相同之间,需要一种手段来移除T型别带有的所有引用、const和volatile饰词。再一次地,标准库以型别特征的形式赐予了我们所需之物,该特征叫做std:decay。std:decay<.T>:type和T相同,区别在于它移除了T的引用和cv饰词(即const或volatile饰词)[这里我捏造了事实,因为std:decay,顾名思义,也用于把数组和函数型别强制转型成指针型别(参见条款1),但是对于这里的讨论,std::decay的行为确实和我前面描述的一致]。这么一来,我们心心念念的判定构造函数是否启动的条件就成了这样:
!std::is_same<Person, typename std::decay<T>::type>::value
std:decay之前的“typename”不能省略,因为std:decay<T>:type对模板形参T有依赖
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);
…
};
//is_base_of替代is_same_of,防止出现子类
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规则
class Person { // C++14
public:
template<typename T, typename = std::enable_if_t<!std::is_base_of<Person, std::decay_t<T> >::value>>
explicit Person(T&& n);
…
};
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) // ctor for std::strings and
: name(std::forward<T>(n)) // args convertible to
{ … } // std::strings
explicit Person(int idx) // ctor for integral args
: name(nameFromIdx(idx))
{ … }
… // copy and move ctors, etc.
private:
std::string name;
};
报错友好型,std::isconstructible这个型别特征能够在编译期间判定具备某个型别的对象是否从另一型别(或另一组型别)的对象(或另一组对象)出发完成构造,所以这个断言很容易撰写:
class Person {
public:
template< // as before
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))
{
// assert that a std::string can be created from a T object
static_assert(
std::is_constructible<std::string, T>::value,
"Parameter n can't be used to construct a std::string"
);
… // the usual ctor work goes here
}
… // remainder of Person class (as before)
};
总结
-
如果不使用万能引用和重载的组合,则替代方案包括使用彼此不同的函数名字、传递const T&型别的形参、传值和标签分派。
-
经由std:enable if对模板施加限制,就可以将万能引用和重载一起使用,不过这种技术控制了编译器可以调用到接受万能引用的重载版本的条件。
-
万能引用形参通常在性能方面具备优势,但在易用性方面一般会有劣势。
* 条款28:理解引用折叠
条款23曾经提及,实参在传递给函数模板时,推导出来的模板形参会将实参是左值还是右值的信息编码到结果型别中。但该条款未曾提及,这个编码操作只有在实参被用以初始化的形参为万能引用时才会发生。模板形参T的推导结果型别中,会把传给param的实参是左值还是右值的信息给编码进去。
编码机制是直截了当的:如果传递的实参是个左值,T的推导结果就是个左值引用型别;如果传递的实参是个右值,T的推导结果就是个非引用型别(注意这里的非对称性:左值的编码结果为左值引用型别,但右值的编码结果却是非引用型别)。是故有下面的结果:
template<typename T>
void func(T&& param);
Widget widgetFactory(); // function returning rvalue
Widget w; // a variable (an lvalue)
func(w); // call func with lvalue; T deduced to be Widget&, param 是 Widget &
func(widgetFactory()); // call func with rvalue; T deduced to be Widget, param 是 Widget&&
在C++中,“引用的引用”是非法的。
int x = 1;
auto && rx = x; //不可以声明"引用的引用"
//当左值被传入接受万能引用的参数模板时,就会得到如下推导结果
template<typename T>
void func(T&& param); // as before
func(w); // invoke func with lvalue; T deduced as Widget&
void func(Widget& && param);//?
答案就是引用折叠。是的,你是被禁止声明引用的引用,但编译器却可以在特殊的语境中产生引用的引用,模板实例化就是这样的语境之一。当编译器生成引用的引用时,引用折叠机制便支配了接下来发生的事情。
有两种引用(左值和右值),所以就有四种可能的引用一引用组合(左值一左值,左值一右值,右值一左值,右值一右值)。如果引用的引用出现在允许的语境(例如,在模板实例化过程中),该双重引用会折叠成单个引用,规则如下:
如果任一引用为左值引用,则结果为左值引用。否则(即两个皆为右值引用),结果为右值引用
由于fParam是个万能引用,我们就知道,传递给f的实参(即用以初始化fParam的表达式)是左值还是右值的信息会被编码到型别形参T中。std::forward的任务是,当且仅当编码T中的信息表明传递给实参是个右值,即T的推导结果型别是个非引用型别时,对fParam(左值)实施到右值的强制型别转换。
* forward原理与实现
std::forward的一种简单实现
底层是个返回值为万能引用的模板,只管返回T&&,会发生引用折叠
如果模板T的型别由上层的万能引用传入,记录了编码类型即
如果传递的实参是个左值,T的推导结果就是个左值引用型别,返回左值引用
如果传递的实参是个右值,T的推导结果就是个非引用型别,返回右值引用。因此达到了转发的作用
template<typename T>
void f(T&& fParam)
{
// do some work
someFunc(std::forward<T>(fParam)); // forward fParam to
} // someFunc
template<typename T> // in // namespace // std
T&& forward(typename remove_reference<T>::type& param)
{
return static_cast<T&&>(param);
}
转发左值
//假设传递给函数f的实参的型别是个左值Widget,则T会被推导为Widget&型别,然后对std:forward的调用就会实例化为std:forward<Widget&>。而将Widget&插入std:forward的实现就会产生如下结果:
Widget& && forward(typename remove_reference<Widget&>::type& param)
{ return static_cast<Widget& &&>(param); }
//由于型别特征remove_reference<Widget&>:type的产生结果是Widget型别(参见条款9),所以std:forward又变换成了下面的结果:
Widget& && forward(Widget& param)
{ return static_cast<Widget& &&>(param); }
//引用折叠同样在返回值和强制型别转换的语境中得到了实施,导致实际调用结果是这样的终极版本std::forward:
Widget& forward(Widget& param) // still in
{ return static_cast<Widget&>(param); } // namespace std
如你所见,当左值实参被传递给函数模板f时,std::forward实例化结果是:接受一个左值引用,并返回一个左值引用。而std:forward内部的强制型别转换未做任何事情,因为param的型别已经是Widget&了,所以再要把它强制转换到Widget&型别不会产生什么效果。综上,被传递给std:forward的左值实参会返回一个左值引用。根据定义,左值引用是左值,所以传递左值给std:forward会导致返回一个左值,符合认知。
转发右值
//再假设传递给f的实参是右值Widget型别。在此情况下,f的型别形参T的推导结果是个光秃秃的Widget。因此,f内部的std:forward就成了std:forward<Widget>。在std:forward的实现中,在T之处用Widget代入,就得出下面的代码:
Widget&& forward(typename remove_reference<Widget>::type& param)
{ return static_cast<Widget&&>(param); }
//针对非引用Widget型别实施std::remove reference会产生和起始型别相同的结果(Widget),所以std:forward又变成了这样:
Widget&& forward(Widget& param)
{ return static_cast<Widget&&>(param); }
//这里没有发生引用的引用,所以也就没有发生引用折叠,所以这也就已经是本次std:forward调用的最终实例化版本了。
由函数返回的右值引用是定义为右值的,所以在此情况下,std::forward会把f的形参fParam(左值)转换成右值。最终的结果是,传递给函数f的右值实参会作为右值转发到someFunc函数,这也精确地符合认知。
std::forward的一种简单实现 在c++14中
template<typename T> // C++14; still in namespace std
T&& forward(remove_reference_t<T>& param)
{
return static_cast<T&&>(param);
}
引用折叠会出现的语境有四种。第一种,最常见的一种,就是模板实例化。第二种,是auto变量的型别生成。技术细节本质上和模板实例化一模一样,因为auto变量的型别推导和模板的型别推导在本质上就是一模一样的(参见条款2)。
template<typename T>
void func(T&& param);
Widget widgetFactory(); // function returning rvalue
Widget w; // a variable (an lvalue)
func(w); // call func with lvalue; T deduced to be Widget&
func(widgetFactory()); // call func with rvalue; T deduced to be Widget
auto&& w1 = w;
//初始化w1的是个左值,因此auto的型别推导结果为Widget&。在w1声明中以Widget&代入auto,就产生了以下这段涉及引用的引用的代码,
Widget& &&w1 =w;
//引用折叠之后,又会变成
Widget&w1 w;
//这就是结果:w1乃是左值引用。
auto&& w2 = widgetFactory();
//以右值初始化w2,auto的型别推导结果为非引用型别Widget。将widget代入auto就得到:
Widget &&w2 = widgetFactory();
//没有引用的引用发生,w2是个右值引用
话说到这里,我们才真正地理解了条款24中介绍的万能引用。万能引用并非一种新的引用型别,其实它就是满足了下面两个条件的语境中的右值引用:
- 型别推导的过程会区别左值和右值。T型别的左值推导结果为T&,而T型别的右值则推导结果为T。
- 会发生引用折叠
万能引用的概念是有用的,有了这个概念以后,就避免了需要识别出存在引用折叠的语境,根据左值和右值的不同脑补推导过程,然后再脑补针对推导的结果型别代入引用折叠发生的语境后应用引用折叠规则。
两种:模板实例化和auto型别生成。第三种语境是生成和使用typedef和别名声明(参见条款9)。如
template<typename T>
class Widget {
public:
typedef T&& RvalueRefToT;
…
};
Widget<int&> w;
typedef int& && RvalueRefToT;
typedef int& RvalueRefToT;
最后一种会发生引用折叠的语境在于decltype的运用中。如果在分析一个涉及dec1type的型别过程中出现了引用的引用,则引用折叠亦会介入并消灭之(有关decltype的信息,详见条款3)。
总结
- 引用折叠会在四种语境中发生:模板实例化、auto型别生成、创建和运用typedef和别名声明,以及decltype。
- 当编译器在引用折叠的语境下生成引用的引用时,结果会变成单个引用。如果原始的引用中有任一引用为左值引用,则结果为左值引用。否则,结果为右值引用。
- 万能引用就是在型别推导的过程会区别左值和右值,以及会发生引用折叠的语境中的右值引用
条款29:假定移动操作不存在、成本高、未使用
其他标准容器vector等都是将其内容存放在堆上的,从而在概念上,只需(以数据成员的方式)持有一个指涉到存放容器内容的堆内存的指针(实际情况当然比这更复杂些,但出于这里的分析目的,这些差异并不要紧)。由于该指针的存在,把整个容器的内容在常数时间内加以移动就成为了可能:仅仅把那个指涉到容器内容的指针从源容器复制到目标容器,尔后把源容器包含的指针置空即可:
vw1 --> area from heap
vw1 --> nullptr
vw2 --> area from heap
反例
std::array型别的对象则缺少这样一个指针,因为其内容数据是直接存储在对象内部的。所以std::array的移动和赋值效率差不多
std::string型别提供的是常数时间的移动和线性时间的复制。这听起来像是在说,它的移动比复制更快,但可能并非如此。许多string的实现都采用了小型字符串优化(small string optimization,SSO)。采用了SSO以后,“小型”字符串(例如,容量不超过15个字符的字符串)会存储在的std:string对象内的某个缓冲区内,而不去使用在堆上分配的存储。在使用了基于SSO的实现的前提下,对小型字符串实施移动并不比复制更快,因为通常在移动的性能优于复制背后“仅复制一个指针”的把戏会失灵了
移动语义不讨好的场景
- 没有移动操作:待移动的对象未能提供移动操作。因此,移动请求就变成了复制请求。
- 移动未能更快:待移动的对象虽然有移动操作,但并不比其复制操作更快。
- 移动不可用:移动本可以发生的语境下,要求移动操作不可发射异常,但该操作未加上noexcept声明。
条款30:熟季完美转发的失败情形
在我们的“小节”探险启程之前,还是值得反思一下“完美转发”的确切含义。“转发”的含义不过是一个函数把自己的形参传递(转发)给另一个函数而已。其目的是为了让第二个函数(转发目的函数)接受第一个函数(转发发起函数)所接受的同一对象。这就排除了按值传递形参,因为它们只是原始调用者所传递之物的副本。我们想要转发目的函数能够处理原始传入对象。指针形参也只能出局,因为我们不想强迫调用者传递指针。论及一般意义上的转发时,都是在处理形参为引用型别的情形。
完美转发的含义是我们不仅转发对象,还转发其显著特征:型别、是左值还是右值,以及是否带有const或volation饰词等。结合前面的观察分析,即我们一般是会和引用形参打交道,这就是说,我们会运用万能引用(参见条款24),因为只有万能引用形参才会将传入的实参是左值还是右值这一信息加以编码。
//单参
template<typename T>
void fwd(T&& param) // accept any argument
{
f(std::forward<T>(param)); // forward it to f
}
//多参
template<typename... Ts>
void fwd(Ts&&... params) // accept any arguments
{
f(std::forward<Ts>(params)...); // forward them to f
}
//如果本语句执行了某操作,而本语句执行了不同的操作,则称fwd完美转发expression到f失败
f( expression );
fwd( expression );
转化失败,大括号初始化物
f({ 1, 2, 3 }); //√
fwd({ 1, 2, 3 }); //×
auto il = { 1, 2, 3 }; // il's type deduced to be std::initializer_list<int>
fwd(il); // fine, perfect-forwards il to f
凡是归类于此的失败,原因都一模一样。在对f的直接调用中(如f({1,2,3})),编译器先领受了调用端的实参型别,又领受了 f 所声明的形参型别。编译器会比较这两个型别来确定它们是否兼容,尔后,如有必要,会实施型隐式型别转换来使得调用得以成功。在上面的例子中,编译器从{1,2,3}出发生成一个临时的std:vector<int>型别对象,从而f的形参就有了一个std:vector<int>对象得以绑定。
而经由转发函数模板fwd来对f实施间接调用时,编译器就不再会比较fwd的调用处传入的实参和f中所声明的形参了。取而代之的是,编译器会采用推导的手法来取得传递给fwd实参的型别结果,尔后它会比较推导型别结果和f声明的形参型别。完美转发会在下面两个条件中的任何一个成立时失败:
- 编译器无法为一个或多个fwd的形参推导出型别结果。在此情况下,代码无法编译通过。
- 编译器为一个或多个fwd的形参推导出了“错误的”型别结果。这里所谓“错误的”,既可以指fwd根据型别推导结果的实例化无法通过编译,也可以指以fwd推导而得的型别调用f与直接以传递给fwd的实参调用f行为不一致。这种分裂行为的源泉之一,可能在于f乃是个重载函数的名字,然后,依据“不正确的”推导型别,fwd里调用到的f重载版本,就与直接调用f的版本有异。
在上述“fwd({1,2,3})”这句调用一例中,问题在于向未声明为std:initializer_list型别的函数模板形参传递了大括号初始化物,因为这样的语境按规定,用标准委员会的行话说,叫做“非推导语境”。通俗地说,这个词的意思是,由于fw的形参未声明为std::initializer list,编译器就会被禁止在fwd的调用过程中从表达式{1,2,3}出发来推导型别。而既然从fwd的形参出发进行推导是被阻止的行为,所以编译器拒绝这个调用也是合情合理的。
0和NULL用作空指针
仅有声明的整型static const成员变量
class Widget {
public:
static const std::size_t MinVals = 28; // MinVals' declaration
…
};
… // 没有给出 const std::size_t Widget::MinVals; // in Widget's .cpp file
std::vector<int> widgetData;
widgetData.reserve(Widget::MinVals); // use of MinVals
f(Widget::MinVals);//√
fwd(Widget::MinVals); //×
//按引用传递整型static const成员变量通常要求其加以定义,而这个需求就会导致代码完美转发失败而等价的、未使用完美转发的代码却能成功。
重载的函数名字和模板名字
void f(int (*pf)(int));
void f(int pf(int)); //和上面相同
//重载
int processVal(int value);
int processVal(int value, int priority);
f(processVal); //√
// fwd就不行了,因为作为一个函数模板,它没有任何关于型别需求的信息这也使得编译器不可能决议应该传递哪个函数重载版本:
fwd(processVal); //×
同一个问题,会出现在使用函数模板来代替(或附加于)重载函数名字的场合。函数模板不是只代表一个函数,而是代表着许许名名函数
template<typename T>
T workOnVal(T param) // template for processing values
{ … }
fwd(workOnVal); // error! which workOnVal instantiation?
解
using ProcessFuncType = int (*)(int);
ProcessFuncType processValPtr = processVal; // specify needed
fwd(processValPtr); // fine
fwd(static_cast<ProcessFuncType>(workOnVal)); // also fine
位域
无法对位域取地址(完美转发需要)
struct IPv4Header {
std::uint32_t version:4,
IHL:4,
DSCP:6,
ECN:2,
totalLength:16;
…
};
void f(std::size_t sz); // function to call
IPv4Header h;
f(h.totalLength); // √
fwd(h.totalLength); // ×
// copy bitfield value; see Item 6 for info on init. form
auto length = static_cast<std::uint16_t>(h.totalLength);
fwd(length); // forward the copy
总结
- 完美转发的失败情形,是源于模板型别推导失败,或推导结果是错误的型别。
- 会导致完美转发失败的实参种类有大括号初始化物、以值0或NULL表达的空指针、仅有声明的整型static const成员变量、模板或重载的函数名字,以及位域。
第六章 lambda表达式
1 2 3 4 5
[=] () mutable throw -> int
{
6
}
- capture 子句(在 C++ 规范中也称为 Lambda 引导。)
- 参数列表(可选)。 (也称为 Lambda 声明符)
- mutable 规范(可选)。
- exception-specification(可选)。
- trailing-return-type(可选)。
- Lambda 体。
std:find_if(container.begin(),container.end(),
[](int val){return 0 < val && val < 10;})
闭包是lambda式创建的运行期对象,根据不同的捕获模式,闭包会持有数据的副本或引用。
闭包类就是实例化闭包的类。每个lambda式都会触发编译器生成一个独一无二的闭包类。而闭包中的语句会变成它的闭包类成员函数的可执行指令。
lambda式常用于创建闭包并仅将其用作传递给函数的实参。上述对std:find if的调用就符合这个情形。不过,一般而言,闭包可以复制。所以,对应于单独一个lambda式的闭包型别可以有多个闭包。例如,在下面的代码中:
{
int x; // x is local variable
…
auto c1 = [x](int y) { return x * y > 55; }; // c1 is copy of the closure produced by the lambda
auto c2 = c1; // c2 is copy of c1
auto c3 = c2; // c3 is copy of c2
…
}
条款31:避免默认捕获模式
C++11中有两种默认捕获模式:按引用或按值。
template<typename C>
void workWithContainer(const C& container)
{
auto calc1 = computeSomeValue1(); // as above
auto calc2 = computeSomeValue2(); // as above
auto divisor = computeDivisor(calc1, calc2); // as above
using ContElemT = typename C::value_type; // type of elements in container
using std::begin; // for
using std::end; // genericity;
// see Item 13
if (std::all_of( // if all values
begin(container), end(container), // in container
[&](const ContElemT& value) // are multiples
{ return value % divisor == 0; }) // of divisor...
) {
… // they are...
} else {
… // at least one
} // isn't...
}
//c++14顺便说下,C++l4提供了在的lambda式的形参声明中使用auto的能力,
//这意味着上面的代码在C++14中可以简化,ContE1emT的声明可以删去,而if条件可以更改如下:
if (std::all_of(begin(container), end(container),
[&](const auto& value) // C++14
{ return value % divisor == 0; }))
捕获只能针对于在创建lambda式的作用域内可见的非静态局部变量(包括形参)。在Widget:addFilter的函数体内,divisor并非局部变量,而是Widget类的成员变量。它压根无法被捕获。这么一来,如果默认捕获模式被消除,代码就不会通过编译:
class Widget {
public:
… // ctors, etc.
void addFilter() const; // add an entry to filters
private:
int divisor; // used in Widget's filter
};
关键在于一个裸指针隐式应用,这就是this。每一个非静态成员函数都持有一个this指针,然后每当提及该类的成员变量时都会用到这个指针。例如,在Widget的任何成员函数中,编译器内部都会把divisor替换成this-divisor。在Widget:addFiliter的按值默认捕获版本中,被捕获的实际上是Widget的this指针,而不是divisor.。
//√
void Widget::addFilter() const
{
filters.emplace_back(
[=](int value) { return value % divisor == 0; }
);
}
空 capture 子句 [ ]
指示 lambda 表达式的主体不访问封闭范围中的变量。
//×
void Widget::addFilter() const
{
filters.emplace_back( // error!
[](int value) { return value % divisor == 0; } // 没有可捕获的divisor,不可见
);
}
而且,如果试图显式捕获divisor(无论按值还是按引用,这无关紧要),这个捕获语句不能通过编译,因为divisor既不是局部变量,也不是形参:
//×
void Widget::addFilter() const
{
filters.emplace_back(
[divisor](int value) // error! no local
{ return value % divisor == 0; } // divisor to capture
);
}
这一特定问题可以通过将你想捕获的成员变量复制到局部变量中,尔后捕获该局部副本加以解决
//√
void Widget::addFilter() const
{
auto divisorCopy = divisor; // copy data member
filters.emplace_back(
[divisorCopy](int value) // capture the copy
{ return value % divisorCopy == 0; } // use the copy
);
}
在C++14中,捕获成员变量的一种更好的方法是使用广义lambda捕获(generalizedlambda capture,参见条款32):divisor = divisor
void Widget::addFilter() const
{
filters.emplace_back( // C++14:
[divisor = divisor](int value) // copy divisor to closure
{ return value % divisor == 0; } // use the copy
);
}
- 按引用的默认捕获会导致空悬指针问题。
- 按值的默认捕获极易受空悬指针影响(尤其是this),并会误导人们认为lambda式是自洽的,实际只能捕获非静态局部变量(包括形参)。
条款32:使用初始化捕获将对象移入闭包
如何捕获一个unique_ptr
class Widget { // some useful type
public:
…
bool isValidated() const;
bool isProcessed() const;
bool isArchived() const;
private:
…
};
auto pw = std::make_unique<Widget>();
...
auto func = [pw = std::move(pw)] // init data mbr in closure w std::move(pw) //初始化捕获
{ return pw->isValidated() && pw->isArchived(); };
突出标示的那段代码就是初始化捕获,位于“=”左侧的,是你所指定的闭包类成员变量的名字,而位于其右侧的则是其初始化表达式。可圈可点之处在于,“=”的左右两侧处于不同的作用域。左侧作用域就是闭包类的作用域,而右侧的作用域则与lambda式加以定义之处的作用域相同。在上述例子中,“=”左侧的名字pw指涉的是闭包类的成员变量,而右侧的名字pw指涉的则在是在lambda式上面一行声明的对象,即经由调用make_unique所初始化的对象。所以,“pw=std:move(pw)”表达了“在闭包中创建一个成员变量pw,然后使用针对局部变量pw实施std::move的结果来初始化该成员变量”。初始化捕获也称为广义lambda捕获。
cpp11模拟cpp14,lambda函数的实现,实现了operator()(…) const函数的闭包类。
class IsValAndArch { // "is validated
public: // and archived"
using DataType = std::unique_ptr<Widget>;
explicit IsValAndArch(DataType&& ptr) // Item 25 explains
: pw(std::move(ptr)) {} // use of std::move
bool operator()() const
{ return pw->isValidated() && pw->isArchived(); }
private:
DataType pw;
};
auto func = IsValAndArch(std::make_unique<Widget>());
如果你非想要用lambda式(考虑到它们的便利性,你极有可能如此),按移动捕获在C++11中可以采用以下方法模拟,只需要:
使用std::bind绕过lambda的种种限制
- 把需要捕获的对象移动到std::bind产生的函数对象中。
- 给到lambda式一个指涉到欲“捕获”的对象的引用。
//cpp14
std::vector<double> data;
auto func = [data = std::move(data)] { /* uses of data */ }; // C++14 init capture
//cpp11 const lambda式
std::vector<double> data;
auto func = std::bind(
[](const std::vector<double>& data){/* uses of data */},
std::move(data)
)
// C+11中针对 可变lambda式 模拟初始化捕获的部分
auto func = std::bind(
[](std::vector<double>& data) mutable { /* uses of data */ },
std::move(data)
);
和lambda表达式类似地,std:bind也生成函数对象。我称std::bind返回的函数对象为绑定对象(bind object)。std:bind的第一个实参是个可调用对象,接下来的所有实参表示传给该对象的值。绑定对象含有传递给std::bind所有实参的副本。对于每个左值实参,在绑定对象内的对应的对象内对其实施的是复制构造;而对于每个右值实参,实施的则是移动构造。在这个例子中,第二个实参是个右值(即std::move的结果,参见条款23),所以data在绑定对象中实施的是移动构造。而该移动构造动作正是实现模拟移动捕获的核心所在,因为把右值移入绑定对象,正是绕过C++11无法将右值到闭包的手法。
cpp11中std::bind和lambda表达式的相互转化
- 移动构造一个对象入C++11闭包是不可能实现的,但移动构造一个对象入绑定对象则是可能实现的。
- 欲在C++11中模拟移动捕获包括以下步骤:先移动构造一个对象入绑定对象,然后按引用把该移动构造所得的对象传递给lambda式。
- 因为绑定对象的生命期和闭包相同,所以针对绑定对象中的对象和闭包里的对象可以采用同样手法加以处置。
总结
- 使用C++14的初始化捕获将对象移入闭包。
- 在C++11中,经由手工实现的类或std::bind去模拟初始化捕获。
条款33:对auto&&型别的形参使用decltype,以std::forward之
//单参
auto f =
[](auto&& param){
return func(normalize(std::forward<decltype(param)>(param)));
}
//多参
auto f =
[](auto&&... params){
return func(normalize(std::forward<decltype(params)>(params)...));
};
条款34:优先选用lambda式,而非std::bind
见仁见智:可读性是第一位
// typedef for a point in time (see Item 9 for syntax)
using Time = std::chrono::steady_clock::time_point;
// see Item 10 for "enum class"
enum class Sound { Beep, Siren, Whistle };
// typedef for a length of time
using Duration = std::chrono::steady_clock::duration;
// at time t, make sound s for duration d
void setAlarm(Time t, Sound s, Duration d);
lambda
//setSoundL(“L”表示“lambda”)是个函数对象,
//它接受指定一个声音,
//该声音将在设定后1小时发出,并持续30秒
auto setSoundL =
[](Sound s)
{
// make std::chrono components available w/o qualification
using namespace std::chrono;
setAlarm(steady_clock::now() + hours(1), // alarm to go off
s, // in an hour for
seconds(30)); // 30 seconds
};
//c++14的时分秒标准后缀简化代码,建立在cpp11中用户定义字面量(std::literals)的功能
auto setSoundL =
[](Sound s)
{
using namespace std::chrono;
using namespace std::literals; // for C++14 suffixes
setAlarm(steady_clock::now() + 1h, // C++14, but
s, // same meaning
30s); // as above
};
std::bind
using namespace std::chrono; // as above
using namespace std::literals;
using namespace std::placeholders; // needed for use of "_1"
auto setSoundB = // "B" for "bind"
std::bind(setAlarm,
steady_clock::now() + 1h, // incorrect! see below
_1,
30s);
但是,正如我前面提到的,这段代码不甚正确。在lambda式中,表达式“steady_clock::now()+1h”是setAlarm的实参之一,这一点清清楚楚。该表达式会在setAlarm被调用的时刻评估求值。这样做合情合理:我们就是想要在setAlarm被调用的时刻之后的一个小时启动警报。但在std:bind的调用中,“steady_clock:now()+1h”作为实参被传递给了std::bind,而非setAlarm。意味着表达式评估求值的时刻是在调用std::bind的时刻,并且求得的时间结果值会被存储在结果绑定对象中。最终导致的结果是,警报被设定的启动时刻是在调std::bind的时刻之后的一个小时,而非调用setAlarm的时刻之后的一个小时!
欲解决这个问题,要使std::bind以延迟表达式的评估求值到调用setAlarm的时刻,而实现这一点的途径,就是在原来的std::bind里嵌套第二层std::bind的调用:
auto setSoundB =
std::bind(setAlarm,
std::bind(std::plus<>(), steady_clock::now(), 1h),
//std::bind(std::plus<steady_clock::time_point>(), steady_clock::now(), 1h), //c++11中模板型别实参要写
_1,
30s);
遭遇重载
void setAlarm(Time t, Sound s, Duration d, Volume v);
之前那个lambda式会一如既往地运作如仪,因为重载决议会选择那个三形参版本的setAlarm:
对于std::bind,问题在于,编译器无法确定应该将哪个setAlarm版本传递给std::bind。它拿到的所有信息就只有一个函数名,而仅函数名本身是多义的。需要强转到适当的函数指针型别:
using SetAlarm3ParamType = void(*)(Time t, Sound s, Duration d);
auto setSoundB = // now
std::bind(static_cast<SetAlarm3ParamType>(setAlarm), // okay
std::bind(std::plus<>(), steady_clock::now(), 1h),
_1,
30s);
遭遇内联
lambda式所对应的闭包类的函数调用运算符中调用setA1arm采用的是常规的函数唤起方式,这么一来,编译器就可以用惯常的手法将其内联。
std::bind的调用传递了一个指涉到setAlarm的函数指针,而那就意味着在setSoundB的函数调用运算符中(即,绑定对象的函数调用运算符中),setAlarm的调用是通过函数指针发生的。由于编译器不太会内联掉通过函数指针发起的函数调用,那也就意味着通过setSoundB调用setAlarm而被完全内联的几率,比起通过setSoundL调用setAlarm要低。效率低。
遭遇模板
//c++14 lambda
auto betweenL =
[lowVal, highVal]
(const auto& val)
{ return lowVal <= val && val <= highVal; };
//c++14 std::bind
using namespace std::placeholders; // as above
auto betweenB =
std::bind(std::logical_and<>(), // C++14
std::bind(std::less_equal<>(), lowVal, _1),
std::bind(std::less_equal<>(), _1, highVal));
//c++11 std::bind 需要指定被比较之物的型别
auto betweenB = // C++11 version
std::bind(std::logical_and<bool>(),
std::bind(std::less_equal<int>(), lowVal, _1),
std::bind(std::less_equal<int>(), _1, highVal));
//c++11 lambda
auto betweenL =
[lowVal, highVal]
(int val)
{ return lowVal <= val && val <= highVal; };
遭遇按值/按引用
lambda:明显
std::bind:按值,std::bind的工作原理。传引用需要std::ref
auto compressRateB = std:bind(compress, std::ref(w), _1);
在C++14中,根本没有使用std::bind的适当用例。而在C++11中,std:bind仅在两个受限的场合还算有着使用的理由,可以发挥余热:
移动捕获
多态函数对象
class PolyWidget {
public:
template<typename T>
void operator()(const T& param);
…
};
PolyWidget pw;
auto boundPW = std::bind(pw, _1);
boundPW(1930);
boundPW(nullptr);
boundPW("Rosebud");
在C++11中的lambda式,是无法达成上面的效果的。但是在C++14中,使用带有auto型别形参的lambda式就可以轻而易举地达成同样的效果:
auto boundPW = [pw](const auto& param) // C++14
{ pw(param); };
第七章 并发API
std::future / std::shared_future
条款35:优先选用基于任务而非基于线程的程序设计
如果你想以异步方式运行函数doAsyncWork,有两种基本选择。你可以创建一个std:thread,并在其上运行doAsyncWork,因此这是**基于线程(thread–based)**的途径:
int doAsyncwork();
std::thread t(doAsyncWork);
抑或,你可以把doAsynWork传递给std:async,这种策略叫做基于任务(task-based):
auto fut = std::async(doAsyncWork); //fut是future的缩写
- std::thread的API未提供直接获取异步运行函数返回值的途径,而且如果那些函数抛出异常,程序就会终止。
- 基于线程的程序设计要求手动管理线程耗尽、超订、负载均衡,以及新平台适配。
- 经由应用了默认启动策略的std:async进行基于任务的程序设计,大部分这类问题都能找到解决之道。
条款36:如果异步是必要的,则指定std::launch::async
- std::launch::async启动策略意味着函数f必须以异步方式运行,亦即,在另一线程之上执行。
- std:launch:deferred启动策略意味函数f只会在std:async所返回的期值的get或wait得到调用时才运行。注2亦即,执行会推迟至其中一个调用发生的时刻。当调用get或wait时,f会同步运行。即,调用方会阻塞至f运行结束为止。如果get或wait都没有得到调用,f是不会运行的。
auto fut = std::async(std::launch::async, f); // launch f asynchronously 异步方式启动
// c++11
template<typename F, typename... Ts>
inline
std::future<typename std::result_of<F(Ts...)>::type>
reallyAsync(F&& f, Ts&&... params) // return future
{ // for asynchronous
return std::async(std::launch::async, // call to f(params...)
std::forward<F>(f),
std::forward<Ts>(params)...);
}
//c++14
template<typename F, typename... Ts>
inline
auto // C++14
reallyAsync(F&& f, Ts&&... params)
{
return std::async(std::launch::async,
std::forward<F>(f),
std::forward<Ts>(params)...);
}
std::promise
#include <iostream>
#include <functional>
#include <thread>
#include <future> // std::promise, std::future
void print_int(std::future<int>& fut) {
int x = fut.get(); // 获取共享状态的值.
std::cout << "value: " << x << '\n'; // 打印 value: 10.
}
int main ()
{
std::promise<int> prom; // 生成一个 std::promise<int> 对象.
std::future<int> fut = prom.get_future(); // 和 future 关联.
std::thread t(print_int, std::ref(fut)); // 将 future 交给另外一个线程t.
prom.set_value(10); // 设置共享状态的值, 此处和线程t保持同步. 不能不设置,否则会一直等待
//prom.set_value(1); // error 不能设置多次
t.join();
return 0;
}
- std::async的默认启动策略既允许任务以异步方式执行,也允许任务以同步方式执行。
- 如此的弹性会导致使用thread local变量时的不确定性,隐含着任务可能永远不会执行,还会影响运用了基于超时的wait调用的程序逻辑。
- 如果异步是必要的,则指定std::launch:async。
条款37:使std::thread型别对象在所有路径皆不可联结
Unjoinable std::thread objects include,不能对其调用join()方法
1 默认构造的std::thread。此类std::thread没有可以执行的函数,因此也没有对应的底层执行线程。
2 已移动的std::thread。移动操作的结果是,一个std::thread所对应的底层执行线程(若有)被对应到另外一个std:thread。
3 std::threads that have been joined. After a join, the std::thread object nolonger corresponds to the underlying thread of execution that has finished run‐ning.
4 std::threads that have been detached. A detach severs the connection between a std::thread object and the underlying thread of execution it corresponds to.
销毁一个可联结的线程实在太过可怕,所以实际上已经封印了这件事(通过规定 可联结的线程的析构函数 导致程序终止),在析构之前要保证调用过join或者detach
任何时候,只要想在每条出向路径上都执行某动作,最常用的方法就是在局部对象的析构函数中执行该动作。这样的对象称为RAII对象,它们来自RAII类(RAII本身代表“Resource Acquisition Is Initialization”,资源获取即初始化,即使该技术的关键其实在于析构而非初始化)。RAII类在标准库中很常见,例如STL容器(各个容器的析构函数都会析构容器内容并释放其内存)、标准智能指针(条款18、条款19和条款20解释过,std::unique_ptr的析构函数会对它指涉的对象调用删除器,而std::shared_ptr和std::weak ptr的析构函数会对引用计数实施自减),std:fstream型别对象(其析构函数会关闭对应的文件),还有很多。
class ThreadRAII {
public:
enum class DtorAction { join, detach };
ThreadRAII(std::thread&& t, DtorAction a) // in dtor, take
: action(a), t(std::move(t)) {} // action a on t
~ThreadRAII()
{ // see below for
if (t.joinable()) { // joinability test
if (action == DtorAction::join) {
t.join();
} else {
t.detach();
}
}
}
std::thread& get() { return t; } // see below
private:
DtorAction action;
std::thread t;
};
像std::thread一样支持移动构造和移动赋值,可以把 ThreadRAII放入容器中管理,初始化。
class ThreadRAII {
public:
enum class DtorAction { join, detach }; // as before
ThreadRAII(std::thread&& t, DtorAction a) // as before
: action(a), t(std::move(t)) {}
~ThreadRAII()
{
… // as before
}
ThreadRAII(ThreadRAII&&) = default; // support
ThreadRAII& operator=(ThreadRAII&&) = default; // moving
std::thread& get() { return t; } // as before
private: // as before
DtorAction action;
std::thread t;
};
条款38:对变化多端的线程句柄析构函数行为保持关注
https://c-cpp.com/cpp/thread/future.html 期值的例子
std::promise
caller(调用方)<------------------callee(被调方)
被调方的结果要存储在哪里:但被调方的结果要存储在哪里呢?
在调用方唤起对应期值的get之前,被调方可能已经执行完毕,因此结果不会存储在被调方的std:promise型别对象里。那个对象,对于被调方来说是个局部量,在被调方结束后会实施析构。
该结果也不能存储在调用方的期值中,因为(出于其他种种原因)可能会从std::future型别对象出发创建std::shared future型别对象(因此把被调方结果的所有权从std::future型别对象转移至std::shared future型别对象),而后者可能会在原始的std::future析构之后复制多次。如果被调方的结果型别不都是可复制的(即只移型别),而该结果至少生存期要延至和最后一个指涉到它的期值一样长。
共享状态
共享状态通常使用堆上的对象来表示
共享状态 std::promise
caller(调用方)<----------被调方结果--------callee(被调方)
共享状态的存在很重要,因为期值析构函数的行为(这也是本条款的议题)是由与其关联的共享状态决定的。具体来说就是:
-
指涉到经由std::aysnc启动的未推迟任务的共享状态的最后一个期值会保持阻塞,直至该任务结束。本质上,这样一个期值的析构函数是对底层异步执行任务的线程实施了一次隐式join。
-
其他所有期值对象的析构函数只仅仅将期值对象析构就结束了。对于底层异步运行的任务,这样做类似于对线程实施了一次隐式detach。对于那些被推迟任务而言,如果这一期值是最后一个,也就意味着被推迟的任务将不会有机会运行了。
特例
这些规则听上去复杂,其实不然。我们真正需要关心的,是一个平凡的“常规”行为外加一个不甚常见的例外而已。常规行为是指期值的析构函数仅会析构期值对象。就这样。它不会针对任何东西实施join,也不会从任何东西实施detach,也不会运行任何东西。它仅会析构期值的成员变量(好吧。实际上,它还多做了一件事。它针对共享状态里的引用计数实施了一次自减。该共享状态由指涉到它的期值和被调方的std:promise共同操纵。该引用计数使得库能够知道何时可以析构共享状态。关于引用计数的一般材料,参见条款19)
例外:只有在期值满足以下全部条件时才会发挥作用:
- 期值所指涉的共享状态是由于调用了std:async才创建的。
- 该任务的启动策略是std::launch::async(参见条款36),这既可能是运行时系统的选择,也可能是在调用std:async时指定的。
- 该期值是指涉到该共享状态的最后一个期值。对于std::future型别对象而言,这一点总是成立。而对于std:shared future型别对象而言,在析构时如果不是最后一个指涉到共享状态的期值,则它会遵循常规行为准则(即仅析构其成员变量)
只有当所有条件都满足,期值的析构函数才会表现出特别行为。而行为的具体表现为阻塞直到异步运行的任务结束。从效果来看,这相当于针对正在运行std::async所创建的任务的线程实施了一次隐式join。
期值的API没有提供任何方法判断其指涉的共享状态是否诞生于std:async的调用,所以给定任意期值对象的前提下,它不可能知道自己是否会在析构函数中阻塞到异步任务执行结束。这个事实暗示着一些意味深长的推论:
// 该容器的析构函数可能会在其析构函数中阻塞,因为它所持有的期值中可能会有一个或多个
// 指涉到经由std::async启动未推迟任务所产生的共享状态
std::vector<std::future<void>> futs; // see Item 39 for info on std::future<void>
// Widget objects 可能在其析构中阻塞
class Widget {
public:
…
private:
std::shared_future<double> fut;
};
只有因std::async调用而出现的共享状态才够格去展示特别行为,但是还有其他方法可以创建出共享状态。其中一个方法就是运用std:packaged_task,std:packaged_.task型别对象会准备一个函数(或其他可调用的对象)以供异步执行,手法是将它加上一层包装,把其结果置入共享状态。而指涉到该共享状态的期值则可以经由std:packaged_task的get_future函数得到:
int calcValue(); // func to run
//给calcValue加上包装使之能以异步方式运行
std::packaged_task<int()> pt(calcValue);
auto fut = pt.get_future(); // get future for pt
std::packaged_task不能复制,所以欲将pt传递给std:thread的构造函数就一定要将它强制转型到右值(经由std:move,参见条款23):
std::thread t(std::move(pt));
未对t实施任何操作。在这种情况下,t在作用域结束点是可联结的,而这将导致程序终止(参见条款37)
针对t实施了join。在此情况下,fut无须在析构函数中阻塞,因为在调用的代码已经有过join。
针对t实施了detach。在此情况下,fut无须在析构函数中实施detach,因为在调用的代码已经做过这件事了。
换句话说,当你的期值所对应的共享状态是由std::packaged_task产生的,则通常无需采用特别析构策略。因为,关于是终止、联结还是分离的决定,会由操纵std::thread的代码作出,而std::packaged_task通常就运行在该线程之上。
- 期值的析构函数在常规情况下,仅会析构期值的成员变量。
- 指涉到经由std::aysnc启动的未推迟任务的共享状态的最后一个期值会保持阻塞,直至该任务结束
条款39:考虑针对一次性事件通信使用以void为模板型别实参的期值
版本1
std::condition_variable cv; // condvar for event
std::mutex m; // mutex for use with cv
cv.notify_one();
{
std::unique_lock<std::mutex> lk(m); // lock mutex
cv.wait(lk); // wait for notify;
}
在本例中,怪代码源于需要使用互斥体。互斥体是用于控制共享数据访问的,但检测和反应任务之间大有可能根本不需要这种介质。
唤醒丢失:如果检测任务在反应任务调用wa1t之前就通知了条件变量,则反应任务将失去响应。为了实现通知条件变量唤醒另一个任务,该任务必须已在等待该条件变量。如果检测任务在响应任务执行Wait之前就执行了通知动作,则反应任务就将错过该通知,并且将等待到地老天荒。
虚假唤醒:反应任务的wait语句无法应对虚假唤醒。线程API的存在一个事实情况(很多语言中都如此,不仅仅是C++),即使没有通知条件变量,针对该条件变量等待的代码也可能被唤醒。这样的唤醒称为虚假唤醒。正确的代码通过确认等待的条件确实已经发生,并将其作为唤醒后的首个动作来处理这种情况。C++的条件变量API使得做到一点异常简单,因为它允许测试等待条件的lambda式(或其他函数对象)被传递给wait。换言之,反应任务中调用wait时可以这样撰写:
cv.wait(lk, []{ return whether the event has occurred; });
版本2
升级:原子标志位,缺点,轮询占用硬件线程
std::atomic<bool> flag(false);
flag = true;
...
while (!flag);
...
版本3
升级:常用的手法是结合条件变量和基于标志位的设计。标志位表示是否发生了有意义的事件,但是访问该标志要通过互斥量加以同步。因为互斥锁会阻止并发访问该标志位,所以,如条款40所说,不需要该标志位采用std::atomic型别对象来实现,一个平凡的布尔量足矣。这么一来,检测任务会长成这样:不需要轮询,也避免了虚假唤醒。
std::condition_variable cv; // as before
std::mutex m;
bool flag(false); // not std::atomic detect event
...
{
std::lock_guard<std::mutex> g(m); // lock m via g's ctor
flag = true; // tell reacting task
// (part 1)
} // unlock m via g's dtor
cv.notify_one();
{ // as before
std::unique_lock<std::mutex> lk(m); // as before
cv.wait(lk, [] { return flag; }); // use lambda to avoid
// spurious wakeups
… // react to event
// (m is locked)
}
… // continue reacting (m now unlocked)
然而,还是有一丝异味存在,因为探测任务和反应任务的沟通方式非常奇特。通知条件变量在这里的目的是告诉反应任务,它正在等待的事件可能已经发生了,然而反应任务必须检查标志位才能确定。设置标志位在这里的目的是告诉反应任务事件确确实实已经发生了,但是检测任务仍然需要通知条件变量才能让反应任务被唤醒并去检查标志位。这一途径是能够运作的,但是不够干净利落。
版本4
另一种方法是摆脱条件变量,互斥量和标志位,方法是让反应任务去等待检测任务设置的期值。这看似是一种怪异的想法。毕竟,条款38曾经解释说,期值代表了从被调者到(通常以异步方式运行的)调用者的信道接收端,在检测和反应任务之间并不存在这种调用者和被调者的关系。不过,条款38又指出,发送端是std::promise型别对象,并且其接收端是期值的通信信道用途不止于调用者和被调者一种。这种信道可以用于任何需要将信息从一处传输到另一处的场合。在本例中,我们将使用它来将信息从检测任务传输到响应任务,传达信息则是有意义的事件已经发生。
std::promise<void> p;
p.set_value();//通知反应任务
...//准备反应
p.get_future().wait();//等待p对应的期值
... //针对事件作出的反应
缺点:可能这一点是最重要的:std:promise型别对象只能设置一次。std::promise型别对象和期值之间的通信通道是个一次性机制:它不能重复使用。这是它基于条件变量和基于标志位的设计之间的显著差异,前两者都可以用来进行多次通信(条件变量可以被重复通知,标志位可以被清除并重新设置)。
假定你只想暂停线程一次(在它创建之后,但在它运行其线程函数之前),使用void期值的设计就是合理的选择。下面是该技术的重要部分:
std::promise<void> p;
void react(); // func for reacting task
void detect() // func for detecting task
{
// create thread suspend t until future is set
std::thread t([]{
p.get_future().wait();
react();
});
// here, t is suspended prior to call to react
p.set_value(); // unsuspend t (and thus call react)
t.join();
}
多个反应任务,每个反应线程都需要自己的那份std::shared_future副本去指涉到共享状态,所以,从share中获取的std::shared_future被运行在反应线程上的lambda式按值捕获:
std:promise<void>p;//同前
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]{
sf.wait(); //sf局部副本之上的wait
react();
});//关于emplace back,参见条款42
}
//若此“…”抛出异常,则detect会失去响应!
p.set_value();
//让所有线程取消暂停
for (auto& t : vt){
//把所有线程置为不可联结状态
t.join();
}
}
- 如果仅为了实现平凡事件通信,基于条件变量的设计会要求多余的互斥量,这会给相互关联的检测和反应任务带来约束,并要求反应任务校验事件确已发生。
- 使用标志位的设计可以避免上述问题,但这一设计基于轮询而非阻塞。条件变量和标志位可以一起使用,但这样的通信机制设计结果不甚自然。
- 使用std::promise型别对象和期值就可以回避这些问题,但是一来这个途径为了共享状态需要使用堆内存,而且仅限于一次性通信。
条款40:对并发使用std::atomic,对特种内存使用volatile
然而,std::atomic型别对象的运用会对代码可以如何重新排序施加限制,并且这样的限制之一就是,在源代码中,不得将任何代码提前至后续会出现std::atomic型别变量的写入操作的位置(或使其他内核视作这样的操作会发生)。注6这意味着在我们的代码中:
而volatile的用处就是告诉编译器,正在处理的是特种内存。它的意思是通知编译器“不要对在此内存上的操作做任何优化”。所以,如果x对应于特种内存,则它应该加上volatile声明饰词:
std::atomic对于并发程序设计有用,但不能用于访问特种内存。
volatile对于访问特种内存有用,但不能用于并发程序设计。
auto y=x;//读取x
y=x; //再次读取x
X=10; //写入x
X=20; //再次写入X
此类优化仅在内存行为符合常规时才合法。“特种”内存就是另一回事。可能最常见的特种内存是用于内存映射/O的内存。这种内存的位置实际上是用于与外部设备(例如,外部传感器、显示器、打印机和网络端口等)通信,而非用于读取或写入常规内存(即RAM)。在此情况下,再次考虑看似冗余的代码:如果x对应于,比如说,由温度传感器报告的值,则x的第二次读取操作并非多余,因为在第一次和第二次读取之间,温度可能已经改变。如果x对应于无线电发射器的控制端口,则有可能是代码在向无线电发出命令,并且值10对应于与值20不同的命令。如果把第一个赋值优化掉,就将改变发送到无线电的命令序列了。
总结
- std::atomic用于多线程访问的数据,且不用互斥量。它是撰写并发软件的工具。(原子性,原子不可再分,多线程累加不出错)
- volatile用于读写操作不可以被优化掉的内存。它是在面对特种内存时使用的工具。(顺序性,可见性,不做优化)
第八章 微调
条款41:针对可复制的形参,在移动成本低并且一定会被复制的前提下,考虑将其按值传递
- 对于可复制的、在移动成本低廉的并且一定会被复制的形参而言,按值传递可能会和按引用传递的具备相近的效率,并可能生成更少量的目标代码。
- 经由构造复制形参的成本可能比经由赋值复制形参高出很多。
- 按值传递肯定会导致切片问题,所以基类型别特别不适用于按值传递。
条款42:考虑置入而非插入
- 从原理上说,置入函数(emplace_back)应该有时比对应的插入函数(push_back)高效,而且不应该有更低效的可能。
- 从实践上说,置入函数在以下几个前提成立时,极有可能会运行得更快:①待添加的值是以构造而非赋值方式加入容器;②传递的实参型别与容器持有之物的型别不同;③容器不会由于存在重复值而拒绝待添加的值。
- 置入函数可能会执行在插入函数中会被拒绝的型别转换