Item 28 理解引用折叠
Item23 中指出,当参数传递给模板函数时,模板参数的类型是左值还是右值被推导出来。但是并没有提到只有当参数被声明为通⽤引⽤时,上述推导才会发⽣,但是有充分的理由忽略这⼀点:因为通⽤引⽤是 Item24 中才提到。回过头来看,通⽤引⽤和左值/右值编码意味着:
12template<typename T>void func(T&& param);
被推导的模板参数T将根据被传⼊参数类型被编码为左值或者右值。
编码机制是简单的。当左值被传⼊时,T被推导为左值。当右值被传⼊时,T被推导为⾮引⽤(请注意不对称性:左值被编码为左值引⽤,右值被编码为⾮引⽤),因此:
1234Widget widgetFactory(); // function returning rvalueWidget w; // a variable(an lvalue)func(w); // call func with lvalue; T deduced to be Widget&func(widgetFactory()); // call func with rvalu ...
Item 27 熟悉通⽤引⽤重载的替代⽅法
Abandon overloading在 Item 26 中的第⼀个例⼦中,logAndAdd 代表了许多函数,这些函数可以使⽤不同的名字来避免在通⽤引⽤上的重载的弊端。例如两个重载的 logAndAdd 函数,可以分别改名为 logAndAddName 和 logAndAddNameIdx。但是,这种⽅式不能⽤在第⼆个例⼦,Person 构造函数中,因为构造函数的名字本类名固定了。此外谁愿意放弃重载呢?
Pass by const T&⼀种替代⽅案是退回到 C++98,然后将通⽤引⽤替换为 const 的左值引⽤。事实上,这是 Item 26 中⾸先考虑的⽅法。缺点是效率不⾼,会有拷⻉的开销。现在我们知道了通⽤引⽤和重载的组合会导致问题,所以放弃⼀些效率来确保⾏为正确简单可能也是⼀种不错的折中。
Pass by value通常在不增加复杂性的情况下提⾼性能的⼀种⽅法是,将按引⽤传递参数替换为按值传递,这是违反直觉的。该设计遵循 Item 41 中给出的建议,即在你知道要拷⻉时就按值传递,因此会参考 Item 41 来详细讨论如何设计与⼯作,效率如何。这⾥,在 Person 的 ...
Item 26 避免在通⽤引⽤上重载 std::forward
假定你需要写⼀个函数,它使⽤ name 这样⼀个参数,打印当前⽇期和具体时间到⽇志中,然后将 name 加⼊到⼀个全局数据结构中。你可能写出来这样的代码:
1234567std::multiset<std::string> names; // global data structurevoid logAndAdd(const std::string& name){ auto now = std::chrono::system_lock::now(); // get current time log(now, "logAndAdd"); // make log entry names.emplace(name); // add name to global data structure; see Item 42 for infoon emplace}
这份代码没有问题,但是效率方面不足。考虑这三个调⽤:
1234std::string petName("Darla");logAndAdd( ...
Item 25 对右值引⽤使⽤std::move,对通⽤引⽤使⽤ std::forward
右值引⽤仅绑定可以移动的对象。如果你有⼀个右值引⽤参数,你就知道这个对象可能会被移动:
1234class Widget { Widget(Widget&& rhs); //rhs definitely refers to an object eligible for moving ...};
这是个例⼦,你将希望通过可以利⽤该对象右值性的⽅式传递给其他使⽤对象的函数。这样做的⽅法是将绑定次类对象的参数转换为右值。如 Item23 中所述,这不仅是 std::move 所做,而且是为它创建:
12345678class Widget {public: Widget(Widget&& rhs) :name(std::move(rhs.name)), p(std::move(rhs.p)) {...} ...private: std::string name; std::shared_ptr<SomeDataStructure> p;};
另⼀⽅⾯ ...
Item 24 区分通用引用和右值引用
为了声明一个指向某个类型 T 的右值引用,我们会写下 T&&。由此一个合理的假设是:当你看到一个 T&& 出现在代码中,你看到的是一个右值引用,但往往不是这样的:
123456789void 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); //不是右值引⽤
事实上,T&& 有两种不同的意思。第⼀种,当然是右值引⽤。这种引⽤表现得正如你所期待的那样: 它们只绑定到右值上,并且它们主要的存在原因就是为了声明某个对象可以被移动。
T&& 的第⼆层意思,是它既可以是⼀个右值引⽤,也可以是⼀个左值引⽤。 ...
Item 23 理解std::move和std::forward
前言当第一次了解到移动语义和完美转发的时候,他们看起来很直观:
移动语义使编译器有可能⽤廉价的移动操作来代替昂贵的复制操作。正如复制构造函数和复制赋值操作符给了你赋值对象的权利⼀样,移动构造函数和移动赋值操作符也给了控制移动语义的权利。移动语义也允许创建只可移动(move-only)的类型,例如 std::unique_ptr, std::future 和 std::thread。
完美转发使接受任意数量参数的函数模版成为可能,它可以将参数转发到其他函数,使目标函数接受到的参数与被传递给转发函数的参数保持一致。
右值引⽤是连接这两个截然不同的概念的胶合剂。它隐藏在语⾔机制之下,使移动语义和完美转发变得可能。
但是移动语义、完美转发和右值引⽤的世界⽐它所呈现的更加微妙。举个例⼦,std::move 并不移动任何东西,完美转发也并不完美。移动操作并不永远⽐复制操作更廉价;即便如此,它也并不总是像你期望的那么廉价。而且,它也并不总是被调⽤,即使在当移动操作可⽤的时候。构造 type&& 也并⾮总是代表⼀个右值引⽤。
⽆论你挖掘这些特性有多深,它们看起来总是还有更多隐藏起来 ...
Item 22 当使⽤Pimpl惯⽤法,请在实现⽂件中定义特殊成员函数
Pimpl凭借这样⼀种技巧,你可以将⼀个类数据成员替换成⼀个指向包含具体实现的类或结构体的指针, 并将放在主类(primary class)的数据成员们移动到实现类去(implementation class), 而这些数据成员的访问将通过指针间接访问。举个例子,假如有⼀个类 Widget 看起来如下:
123456789class Widget() {//定义在头⽂件`widget.h`=public: Widget(); ...private: std::string name; std::vector<double> data; Gadget g1, g2, g3; //Gadget是⽤⼾⾃定义的类型}
因为类 Widget 的数据成员包含有类型 std::string,std::vector 和 Gadget, 定义有这些类型的头⽂件在类 Widget 编译的时候,必须被包含进来,这意味着类 Widget 的使⽤者必须要 #include<string>,<vector> 以及 gadget ...
Item 21 优先考虑使⽤ std::make_unique 和 std::make_shared 而⾮ new
条款21:优先考虑使⽤ std::make_unique 和 std::make_shared 而⾮ new让我们先对 std::make_unique 和 std::make_shared 做个铺垫。 std::make_shared 是 C++11 标准的⼀部分,但很可惜的是, std::make_unique 不是。它从 C++14 开始加⼊标准库。如果你在使⽤ C++11,不⽤担⼼,⼀个基础版本的 std::make_unique 是很容易⾃⼰写出的,如下:
12345template<typename T, typename... Ts>std::unique_ptr<T> make_unique(Ts&&... params){ return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));}
正如你看到的,make_unique 只是将它的参数完美转发到所要创建的对象的构造函数,从新产⽣的原始指针⾥⾯构造出 std::u ...
Item 19 对于共享资源使⽤ std::shared_ptr
条款19:对于共享资源使用std::shared_ptr一个很有意思的说法:程序员使⽤带垃圾回收的语⾔指着 C++ 笑看他们如何防⽌资源泄露。“真是原始啊!”他们嘲笑着说。“你们没有从 1960 年的 Lisp 那⾥得到启发吗,机器应该⾃⼰管理资源的⽣命周期而不应该依赖⼈类。” C++ 程序员翻⽩眼。“你得到的启发就是只有内存算资源,其他资源释放都是⾮确定性的你知道吗?我们更喜欢通⽤,可预料的销毁,谢谢你。”为什么我们不能同时有两个完美的世界:⼀个⾃动⼯作的世界(垃圾回收),⼀个销毁可预测的世界(析构)?
C++11中的 std::shared_ptr 将两者组合了起来。⼀个通过 std::shared_ptr 访问的对象其⽣命周期由指向它的指针们共享所有权(shared ownership)。没有特定的 std::shared_ptr 拥有该对象。相反,所有指向它的 std::shared_ptr 都能相互合作确保在它不再使⽤的那个点进⾏析构。当最后⼀个 std::shared_ptr 到达那个点,std::shared_ptr 会销毁它所指向的对象。就垃圾回收来说,客⼾端不需要关⼼ ...
Item 20 当std::shard_ptr可能悬空时使⽤std::weak_ptr
条款20:当std::shard_ptr可能悬空时使⽤std::weak_ptr如果有⼀个像 std::shared_ptr 的指针但是不参与资源所有权共享的指针是很⽅便的。换句话说,是⼀个类似 std::shared_ptr 但不影响对象引⽤计数的指针。这种类型的智能指针必须要解决⼀个 std::shared_ptr 不存在的问题:可能指向已经销毁的对象。一个真正的智能指针应该跟踪所值对象,在悬空时知晓,悬空就是指针指向的对象不再存在。这就是对 std::weak_ptr 最精确的描述
你可能想知道什么时候该⽤ std::weak_ptr。它什么都好除了不太智能。 std::weak_ptr 不能解引⽤,也不能测试是否为空值。因为 std::weak_ptr 不是⼀个独⽴的智能指针。它是 std::shared_ptr 的增强。
这种关系在它创建之时就建⽴了。std::weak_ptr 通常从 std::shared_ptr 上创建。当从 std::shared_ptr 上创建 std::weak_ptr 时两者指向相同的对象,但是 std::weak_ptr 不会影响所指对象的引⽤ ...