条款二十一
优先使用make_unique(c++14)与nake_shared而非使用new
allccate_shared(分配子, 用以构建管理对象的参数)
1、make系列仅仅会引起一次动态分配;new则是两次,一次给指针,一次给控制块
2、new操作和构造函数之间可能执行其他导致异常的函数
4个例外
1、你需要定制析构器,make系列不可以自定义析构器
2、需要用 “{ }”(初始化列表)进行初始化时,make系列是默认使用()进行参数匹配的。不过也可以分成两步完成:
auto a = {...}; auto s = make_shared(a);
3、被管理类有自定义的new、delete等符号时不要使用make_shared,因为shared_ptr使用全局版本的delete和new时不仅仅释放了管理对象的内存,还要释放控制块的内存。
4、make系列创建的控制块是与管理对象在同一块的,即所有w_ptr与s_ptr都失效才会释放对象,因为那个时候控制块才释放。故如果你的确希望最后一个s_ptr失效时就析构被管理对象,那么你就不应该使用make_shared。
条款二十二
使用Pimpl用法时将特殊成员函数定义放到实现文件中
因为当你使用Pimpl用法时,大概率(其实就是会)要使用unique_ptr来管理分离出的数据类,而unique_ptr的删除器(即默认的delete)决定了其被析构时管理的不能是不完整类型(incomplete type),因为delete会在删除前使用static_assert检测自己删除的是否为完整类型。
对于Pimpl类来说,定义部分被放在了单独的头文件中,这个仅有定义的类中其实仍然包含了一些函数的定义,即特种构造成员的定义,因为系统帮你合成了它们。
这些特种构造函数中,有些是需要进行析构的,如析构函数,移动函数;这也就意味着这些函数需要析构其中的unique_ptr,然而unique_ptr锁管理的类在这个头文件中是只有定义的,即是一个不完整类型(incomplete type),由于析构函数等为隐式inline所以其中析构代码被展开,析构unique_ptr的代码也被展开执行,此时便检测到unique_ptr管理的是一个不完整类型(其实你在实现头文件中定义了这个类,但是在这个声明头文件中无法被看到),使得析构失败,无法通过编译,并抛出类似不完整类型的警告。
因此,要把特种构造成员放到实现文件里去,即使=default也要放过去,这样特种构造成员就可以在实现文件里看到你定义的数据类。
当然,可以看出这里是因为unique_ptr特殊的析构方式,即在自己的析构函数中delete所管理的指针,我们才有必须使用完整类型的限制;若是采用shared_ptr的话便不会出现这个问题,因为shared_ptr的删除器并不是shared_ptr的一部分,而是控制块的一部分,控制块是个完整的类型。
条款二十三
std::move与std::forward
首先我们要明确,所有的参数都是左值,即使这个参数是T&&类型的,那也只是说明其是右值引用类型的左值。只有临时对象是个右值。
std::move强制返回参数对应的右值(要注意移动操作是无法作用于const对象之上的,因为移动操作其实改变了参数)
template
decltype(auto) move(T&& t)
{
using res = remove_reference_t&&;
//using res = remove_reference::type&&;
return static_cast(t);
}
相比之下,std::forward
仅当参数param是右值类型并且T是普通值类型时才会把结果转换成右值。
forward常用在你调用了一个函数后,这个函数内部还需要调用其他函数,而你希望调用的其他函数采用和当前函数一样的版本(如你用右值调用了fun1,fun1调用fun2,若要fun2使用其右值版本,你就可以用forward)。
所以这两个函数其实在运行时不做任何事,只是转换了参数类型。
条款二十四
万能引用与右值引用
在模板函数中担任类型推断的【T&&】(const T&&也不是)才是万能引用,可以接受任何类型的参数;而一般函数的T&&参数只是右值引用,标志此函数接受右值参数。
条款二十五
对右值引用施以std::move而对万能引用施以std::forward
当你要把一个右值/万能引用不止一次地绑定到一个函数内的多次调用的时候,仅仅在最后一次使用move或forward因为要保证之前的调用都不会改变这个对象。(当你move或forward之后很有可能就把参数中的对象搬到调用方函数中去了,原来的对象剩下空壳,此时再一次使用move或forward显然没有意义甚至出错)
当按值返回的函数返回的对象是绑定到右值/万能引用的时候,使用std::move/std::forward来减少复制(如果这个返回的对象是个右值的话就可以直接移动到返回值;如果直接返回,右值引用类型的参数其实是个左值)
但是这一点对于一般的局部对象(值类型T)无效,一方面局部对象是左值,另一方面编译器具有RVO优化或者说NRVO优化,即返回一个等于返回类型的局部变量时编译器不进行拷贝,而是把局部变量“移动”给返回值,相当于编译器自主使用了std::move,所以如果再使用forward或move反而阻止了这项优化。
条款二十六
避免依万能引用进行重载
因为万能引用无论如何都是精确匹配,所以会阻止 需要也可以进行类型转换 的其他重载版本
条款二十七
熟悉依万能引用进行重载的替代方案
首先明确完美转发的好处是避免了函数接受参数时需要创建的临时对象,比如传值的参数若改用传引用就可以省去一次复制,或返回值时创建的临时对象。
1、放弃重载,用多个相似名字来定义不同的需要版本,这个方法自然无法用于构造函数。
2、用const T&代替T&&,达不到完美转发的高效率,但是简洁有效未尝不可。
3、传值,如果知道函数是一定要进行复制的,那就先值传递再使用std::move,这样的话一定程度上效率相当,并且没有重载问题。
4、标签分派,即采用STL泛型算法的分层思想,令用户使用的表层函数采用万能引用,实现功能的实际函数根据一个额外的无名参数来判断需要调用的实际功能函数类型。这一方法对构造函数同样无效,因为系统会合成某些特种成员,被合成的那些成员就无法享受完美转发了(当然你可以手动定义来避免自动合成,但结果没差)。
可能用到的函数:
(1)is_integral
返回std::true_type/std::false_type来标识参数是否为整型
(2)remove_reference_t
移除万能引用判断结束后T上的&,得到原本的值类型
5、模板限制
std::enable_if
仅在c++11后,14后具有了_t版本,功能为条件启用模板
使用示例
template< typename T, typename = typename std::enable_if
.....函数......
模板只会在condition为True时启用,进行判断可能用到的一些函数
std::is_same
std::decay
std::is_base_of
static_assert(condition, "..."); 编译期报错
is_constructible
条款二十八
引用折叠
(1)type & && -> type&
当万能引用T&&接受左值时T被推断为type&,故T&&整体的类型为T& &&,折叠后为T&,即左值引用。
(2)type && && -> type &&
当万能引用T&&接受右值时T被推断为type&&,故T&&整体类型为T&& &&,折叠后为T&&,即为右值引用。
条款二十九
假定移动操作不存在、成本高、未使用
移动 操作并非总是优于 复制 操作,比如这些情况:
(1)没有移动操作
(2)移动操作并不比复制快
(3)移动操作不是noexcept的
(4)移动的是个左值对象
实例:
如小字符串优化的string
将对象存储于本体内的array
这两者都将数据存在了对象内,即使使用移动也要把数据逐个移动
移动操作很多时候是因为只要复制指向数据的指针所以才比复制快,所以要把移动操作当做不存在来考虑程序的设计。
条款三十
完美转发失败的情形
一、大括号初始化物
这一点是由于模板没有办法直接判断{ }的类型,若直接将{ }传入模板函数会被判断成其他类型,因为{}可以隐式转换成某些类型,如vector相当于先用{}初始化了vector,再传入模板。但是可以直接指明{ }为initializer_list
fun
的形式指明类型。
当然auto可以判断initializer_list的类型,故如下也是可以的:
auto L = {....}
fun(L);
这样模板函数就可以判断L的类型了。
二、0与NULL表示空指针时
0与NULL本质上是某种整型,一般是int,所以模板不可能把它们判断成指针型,这也是为啥应该使用nullptr。
三、仅有声明的整型static const成员变量
如一个声明在类内并拥有初始值的static const int成员,这样的成员由于没有定义在外部,故编译器会使用实际的值来代替变量,这样便不必保存这个变量,即这个所谓的“变量”没有地址,也不必为其保留内存,算是优化。但是问题就出现在这里。万能引用在进行型别判断时由于static const int成员为左值,故T被推导为左值引用即type&,故T&&为type& && -> type&,也就是参数类型是左值引用,所以需要读取传入实参的地址,但是static const int成员又由于编译器的缘故没有地址,所以完美转发无法运作。
四、重载的函数名字和模板名字
因为无法决定使用哪个版本,所以也没法转发
修正方法是先使用相要的函数版本初始化一个指针,或者直接指定具体的模板类型。
五、位域
规定了位域的类成员
因为无法拥有指向比特的指针,故无法转发
指针可以指向的最小单位就是char,又因为指针性质上与引用是同样的。故无法拥有绑定比特的引用,但模板推断出的类型是绑定到位域的指针,而位域又是由机械字的若干部分(即比特)组成的,所以推断会失败。