23:理解std::move
和std::forward
std::move
:返回变量的右值引用- 对const对象的移动操作会被转换为拷贝操作
- 因为const对象经过
std::move
会返回一个const右值引用,而一般函数重载的移动版本形参都是非const的右值引用,无法匹配
- 因为const对象经过
std::move
不移动对象,而且也不保证对象一定被移动,仅仅返回对象的右值引用
1 2 3 4 5 6
template<typename T> // C++14 decltype(auto) move(T&& param) { using ReturnType = remove_reference_t<T>&&; return static_cast<ReturnType>(param); }
- 对const对象的移动操作会被转换为拷贝操作
std::forward
:实现完美转发(保持对象的左值性或右值性)- 通常情况下,形参总是左值,即使其类型是右值引用
std::move
和std::forward
只是进行类型转换,在运行时不做任何事- 参考
24:区分万能引用和右值引用
万能引用和右值引用只是形式上类似,但这是两个概念
- 万能引用:形式为
T&&
或auto&&
,并且存在类型推导- 函数模板参数:
template <typename T> void func(T&& param);
- auto类型推导:
auto&& val = myVal;
1
auto myFunc = [] (auto&& func, auto&&... params) {/* do something */}
- 函数模板参数:
- 右值引用
- 带const(不是纯粹的
T&&
形式):template <typename T> void func(const T&& param);
- 形式是
T&&
,但是不存在类型推导:比如vector的push_back
,但是emplace_back
中参数是万能引用1 2 3 4 5 6 7 8
template <typename T, typename Allocator = allocator<T>> class vector{ public: void push_back(T&& x); // 调用push_back时,类型T已知 template <typename... Args> void emplace_back(Args&&... args); // 参数包args的类型Args独立于T,存在类型推导,这里是万能引用 }
- 带const(不是纯粹的
- 参考
25:针对右值引用实施std::move
,针对万能引用实施std::forward
|
|
- 重载
setName
不是一个好的设计- 可能效率低:如果传入字面量,即使匹配到右值版本的函数,形参仍然会作为临时对象
- 如果有多个参数,需要重载$2^N$种,如果使用参数包,则无法实现
- 在函数中使用move或forward时,使用的位置应该是该参数最后一次使用的时候
- 如果函数中将形参进行处理,然后返回
- 传值返回:如果形参是右值引用(比如
Widget operator+
成员函数),使用move返回;如果形参是万能引用(比如doNothing
成员函数),使用forward返回 - 如果返回值是函数中的局部变量,则编译器有特定的优化:RVO
- 传值返回:如果形参是右值引用(比如
- 返回值优化RVO(Return Value Optimization):减少函数返回时产生临时对象,进而消除部分拷贝或移动操作
1 2 3 4 5 6 7 8 9 10 11 12
// 原来 Widget func() { return Widget(); } // 有一次默认构造,一次拷贝构造 Widget w = func(); // 再加上一次拷贝构造 // 使用RVO优化,上面过程相当于: void func(Widget& w) { w.Widget::Widget(); } // Widget w在外面分配空间,直接传入func中进行构造,因此只需要一次(默认)构造 // NRVO(Named Return Value Optimization)原理类似 Widget func() { Widget w; return w; // 返回对象已经具名 }
- 使用前提:局部对象的类型和返回值类型相同,而且局部对象就是返回值
- 限制场景:
- 返回
std::move()
:默认构造+移动构造 - 进行赋值而非初始化
Widget w; w = func();
:默认构造+func中的默认构造和拷贝构造 - 不同的分支条件下,返回不同的局部对象
- 返回
- 参考
26:避免依万能引用类型进行重载
- 原因:函数匹配规则
- 如果模板实例化出的函数和普通重载函数都精确匹配,则优先选择普通重载函数,其次选择模板函数实例化出来的精确版本
- 例子
|
|
27:熟悉依万能引用类型进行重载的替代方案
放弃重载,使用不同的函数名
- 但是对于构造函数就无能为力
普通函数形参为
const type&
类型- 因此传入const实参,会优先使用原来的普通版本,而非重载的万能引用版本
将形参从引用类型换成值类型:当知道肯定要复制形参时,考虑按值传递
1 2 3 4 5 6 7
class Person{ public: explicit Person(std::string name): _name(std::move(name)) {} explicit Person(int idx): name(nameFromIdx(idx)) {} private: std::string _name; }
使用Tag分发:使用Tag对参数进行区分,进而分发到不同的函数实现
- 背景:如果想使用完美转发,就必须要使用万能引用
- 例子:
1 2 3 4 5 6 7 8 9 10
// 两个函数实现的版本 template <typename T> void logAndAddImpl(T&& name, std::false_type) {} template <typename T> void logAndAddImpl(int idx, std::true_type) {} // 使用Tag对参数进行区分 template <typename T> void logAndAdd(T&& name) { logAndAddImpl( std::forward<T>(name), std::is_integral<typename std::remove_reference<T>::type>() ); // 或者C++14:std::is_integral<typename std::remove_reference_t<T> }
- 如果传入true or false,到运行时才能决定
- 在编译阶段进行模板匹配,
std::is_integral
在编译阶段就可以判断类型是否为整型
约束接受万能引用的模板:
std::enable_if
判断- 背景:构造函数无法使用Tag分发
- 例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
#include <type_traits> class Person{ public: explicit Person(int idx): _name(nameFromIdx(idx)) {} template<typename T, typename = std::enable_if_t< !std::is_base_of_v<Person, std::decay_t<T>> && !std::is_integral_v<std::remove_reference_t<T>> > > // 当类型T不为Person或者其派生类,抑或T不为int型时,才会选择这个的重载版本,使用万能引用进行重载并实现完美转发 explicit Person(T&& name): _name(std::forward<T>(name)) { static_assert( std::is_constructible(std::string, T)::value, "Parameter name can't be used to construct a std::string" ); // 验证类型为std::string的对象能否被类型为T的对象构造 } private: std::string _name; };
std::enable_if<condition>::type
:只有满足条件的模板才会使用(C++14std::enable_if_t
)std::is_same<T1, T2>::value
(C++17std::is_same_v
)std::is_base_of<T1, T2>::value
:如果T2继承于T1,则为true;且std::is_base_of<T, T>::value==true
(C++17std::is_base_of_v
)std::decay<T>::type
的类型与T的类型相同,忽略了引用、const、volatile(C++14std::decay_t
)
权衡
- 前三种方案都需要对需要调用的函数形参逐一指定其类型,后两种方案使用万能引用实现了完美转发
- 虽然完美转发效率更高(避免创建临时对象),但是某些对象无法实现完美转发,并且使用完美转发并编译报错时,报错信息的可读性很差
std::is_constructible
可以在编译期测试一个类型的对象能否被另一个不同类型的对象(或者多个不同类型的多个对象)构造,因此可以用来验证转发函数的万能引用参数是否合法
参考
28:理解引用折叠
- 几种引用折叠的应用场景:
- 万能引用的实例化:在模板类型推导时,可能出现“引用的引用”的情况,此时需要用到引用折叠
std::forward
完美转发:1 2 3 4
template <typename T> T&& forward(typename remove_reference<T>::type& param){ return static_cast<T&&>(param); }
- auto类型推导,decltype类型推导
- typedef类型别名
- 参考
29:假定移动操作不存在、成本高、未使用
- 几种移动语义不可用、不高效的情况:
- 没有移动操作:编译器只有在没有用户自定义拷贝操作和析构函数时,才自动生成移动操作
- 移动未能更快:
std:array
- 一般STL中容器的对象都分配在堆上,对象中有指向堆上内存的指针,因此移动操作只需要进行指针的更新、源对象的指针置空即可
- 但是
std::array
中内容分配在栈上(栈上的数组),移动操作等于复制操作
std::string
std::string
针对小对象有一个优化SSO(Small String Optimization),小对象直接存储在栈上而非堆上,省去动态内存分配
- 移动不可用:移动操作没有标记为
noexcept
- 如果移动操作没有标记为
noexcept
,即使是适合使用移动操作的场景,编译器也会使用复制操作替代
- 如果移动操作没有标记为
- 源对象是左值:只有右值可以作为移动操作的源(左值可以用,但是很容易造成空悬问题)
- 参考
30:熟悉完美转发的失败情形
- 完美转发的含义:不仅转发对象,而且转发其特征(左值、右值、const、volatile)
- 完美转发的失败情形
- 列表初始化
1 2 3 4 5 6 7 8
void f(const std::vector<int>& v) {} template <typename T> void fwd(T&& param) {} f({1,2,3}); // ok fwd({1,2,3}); // 编译报错:无法推断出T的类型 auto il = {1,2,3}; fwd(il); // T=initializer_list<int>
- 0或NULL作为空指针
- 0或NULL会被推导为int型而非空指针类型,因此完美转发后得到的类型是int,但是形参是指针类型
- 仅仅声明整型的静态常量数据成员
1 2 3 4 5
class Widget{ public: static cosnt int cnst = 12; // 声明而非定义,不会分配实际的存储空间,而是常量传播(直接将用到cnst的地方替换为12) }; fwd(Widget::cnst); // 编译报错:找不到cnst的定义
- 只声明不会分配空间,因此无法取地址,也无法使用引用,不能使用完美转发
- 解决方法:在类外或是对应.cpp文件中添加定义:
const int cnst = 12;
- 函数重载和函数模板
1 2 3 4 5 6 7 8 9 10
void f(int (*pf)(int)); int func(int a); int func(int a, int b); f(func); // ok fwd(func); // 模板类型推导失败:无法确定是哪个重载版本 // 解决方法: using FuncType = int (*)(int); fwd(static_cast<FuncType>(func)); // 但是万能引用和完美转发一般是针对任意类型的,这里限定了类型,语义与实现矛盾
- 位域:位域只是int类型的一部分,没有一个确切地址,也就无法引用
- 列表初始化
- 参考