《effective modern C++》Chapter 4-5
[TOC]
本文记录《effective modern C++》第 4, 5 章的学习笔记.
CHAPTER 4 Smart Pointers
raw pointer 的不好地方:
- Its declaration doesn’t indicate whether it points to a single object or to an array. 不是类型安全的.
- Its declaration reveals nothing about whether you should destroy what it points to when you’re done using it, i.e., if the pointer owns the thing it points to. 无法自动管理资源.
- If you determine that you should destroy what the pointer points to, there’s no way to tell how. Should you use
delete
, or is there a different destruction mechanism (e.g., a dedicated destruction function the pointer should be passed to)? 无法判断使用正确的方式析构. - If you manage to find out that delete is the way to go, Reason 1 means it may not be possible to know whether to use the single-object form (
delete
) or the array form (delete []
). If you use the wrong form, results are undefined. - Assuming you ascertain that the pointer owns what it points to and you discover how to destroy it, it’s difficult to ensure that you perform the destruction exactly once along every path in your code (including those due to exceptions). Missing a path leads to resource leaks, and doing the destruction more than once leads to undefined behavior. 多重路径下都要执行
delete
导致混乱. - There’s typically no way to tell if the pointer dangles, i.e., points to memory that no longer holds the object the pointer is supposed to point to. Dangling pointers arise when objects are destroyed while pointers still point to them. 无法判断是否是 dangling.
Item 18: Use std::unique_ptr
for exclusive-ownership resource management.
基础特点:
- By default,
std::unique_ptr
s are the same size as raw pointers std::unique_ptr
embodies exclusive ownership semantics,- a move-only type. Copying a
std::unique_ptr
isn’t allowed. - By default, resource destruction is accomplished by applying delete to the raw pointer inside the
std::unique_ptr
.
常用场景举例 1 工厂模式
工厂模式会生产出实例的指针(heap 上的 object), 正好可以用 std::unique_ptr
包裹之, 作为返回值.
还有一个优点导致其适合工厂模式: std::unique_ptr
可以很方便地转化为 std::shared_ptr
.
1 | class Investment { … }; |
即便遇到 the ownership chain got interrupted due to an exception or other a typical control flow (e.g., early function return or break
from a loop), 也能保证资源安全.
虽然也有例外: Most stem from abnormal program termination.
- If an exception propagates out of a thread’s primary function (e.g.,
main
, for the program’s initial thread) or - if a
noexcept
specification is violated (see Item 14), local objects may not be destroyed, and - if
std::abort
or anexit
function (i.e.,std::_Exit
,std::exit
, orstd::quick_exit
)is called.
std::unique_ptr
can be configured to use custom deleters:
1 | //custom deleter (a lambda expression) |
几点需要说明:
Attempting to assign a raw pointer (e.g., from
new
) to astd::unique_ptr
won’t compile, because it would constitute an implicit conversion from a raw to a smart pointer. That’s whyreset
is used.The custom deleter takes a parameter of type
Investment*
. This means we’ll be deleting a derived class object via a base class pointer. For that to work, the base class—Investment
—must have a virtual destructor:使用自定义 deleter 的代价: Deleters that are function pointers generally cause the size of a
std::unique_ptr
to grow from one word to two.
因为 stateless function objects (e.g., from lambda expressions with no captures) incur no size penalty, 因此优先选 lambda 函数.
1 | template <typename... Ts> |
常用场景举例 2 Pimpl Idiom
Item 22
std::unique_ptr<T[]>
two forms, one for individual objects (std::unique_ptr<T>
) and one for arrays (std::unique_ptr<T[]>
).
不推荐使用:
- because
std::array
,std::vector
, andstd::string
are virtually always better data structure choices than raw arrays. - only application situation: when you’re using a C-like API that returns a raw pointer to a heap array that you assume ownership of.
easily and efficiently converts to a std::shared_ptr
1 | std::shared_ptr<Investment> sp = |
This is a key part of why std::unique_ptr
is so well suited as a factory function return type. Factory functions can’t know whether callers will want to use exclusive-ownership semantics for the object they return or whether shared ownership.
Things to Remember
std::unique_ptr
is a small, fast, move-only smart pointer for managing resources with exclusive-ownership semantics.- By default, resource destruction takes place via delete, but custom deleters can be specified. Stateful deleters and function pointers as deleters increase the size of
std::unique_ptr
objects. - Converting a
std::unique_ptr
to astd::shared_ptr
is easy.
Item 19: Use std::shared_ptr
for shared-ownership resource management.
背景
garbage collection VS deterministic
“How primitive! Didn’t you get the memo from Lisp in the 1960s? Machines should manage resource lifetimes, not humans.”
VS
“You mean the memo where the only resource is memory and the timing of resource reclamation is nondeterministic? We prefer the generality and predictability of destructors, thank you.”
std::shared_ptr
is the C++11 way of binding these worlds together.
performance implications
std::shared_ptrs
are twice the size of a raw pointer, because they internally contain a raw pointer to the resource as well as a raw pointer to the resource’s reference count.Memory for the reference count must be dynamically allocated. Conceptually, the reference count is associated with the object being pointed to, but pointed-to objects know nothing about this. They thus have no place to store a reference count.
std::make_shared
可能帮助减轻此问题.Increments and decrements of the reference count must be atomic. Atomic operations are typically slower than non-atomic operations, so even though reference counts are usually only a word in size, you should assume that reading and writing them is comparatively costly.
Move-constructing
Move-constructing a std::shared_ptr
from another std::shared_ptr
sets the source std::shared_ptr
to null, and that means that the old std::shared_ptr
stops pointing to the resource at the moment the new std::shared_ptr starts
. As a result, no reference count manipulation is required.
so move construction is faster than copy construction, and move assignment is faster than copy assignment.
内存布局
仅包含 2 个指针, 一个指向 reference count 另一个指向 control block. 这个内存结构可以解释下面的现象.
custom deleter
deleter type of std::shared_ptr
is not part of ptr type(可以从声明的模板参数中看出来, 这一点与 std::unique_ptr
的区别)
1 | auto loggingDel = [](Widget *pw) // custom deleter |
因此可以让包含不同 deleter 的 std::shared_ptr
放入同质容器, 例如 std::vector
中.
1 | auto customDeleter1 = [](Widget *pw) { … }; // custom deleters, |
specifying a custom deleter doesn’t change the size of a std::shared_ptr
object.
Multiple control blocks
如何创建一个 control block?
An object’s control block is set up by the function creating the first std::shared_ptr
to the object. At least that’s what’s supposed to happen.
In general, it’s impossible for a function creating a std::shared_ptr
to an object to know whether some other std::shared_ptr
already points to that object, so the following rules for control block creation are used:
std::make_shared
always creates a control block. It manufactures a new object to point to, so there is certainly no control block for that object at the timestd::make_shared
is called.A control block is created when a
std::shared_ptr
is constructed from a unique-ownership pointer (i.e., astd::unique_ptr
orstd::auto_ptr
). As part of its construction, thestd::shared_ptr
assumes ownership of the pointed-to object, so the unique-ownership pointer is set to null.When a
std::shared_ptr
constructor is called with a raw pointer, it creates a control block.If you wanted to create a
std::shared_ptr
from an object that already had a control block, you’d presumably pass astd::shared_ptr
or astd::weak_ptr
as a constructor argument, not a raw pointer.std::shared_ptr
constructors takingstd::shared_ptrs
orstd::weak_ptrs
as constructor arguments don’t create new control blocks, because they can rely on the smart pointers passed to them to point to any necessary control blocks.
注意: 不要用同一个 raw pointer 进行多次 std::shared_ptr
的初始化, 会导致 Multiple control blocks. It means multiple reference counts, and multiple reference counts means the object will be destroyed multiple times. 如下:
1 | auto pw = new Widget; // pw is raw ptr |
建议:
try to avoid passing raw pointers to a
std::shared_ptr
constructor. The usual alternative is to usestd::make_shared
.if you must pass a raw pointer to a
std::shared_ptr
constructor, pass the result ofnew
directly instead of going through a raw pointer variable.
1 | std::shared_ptr<Widget> spw1(new Widget, loggingDel);// direct use of new |
std::enable_shared_from_this
this
pointer 导致的 Multiple control blocks
1 | std::vector<std::shared_ptr<Widget>> processedWidgets; |
危害在于 This code will compile, but it’s passing a raw pointer (this
) to a container of std::shared_ptrs
. The std::shared_ptr
thus constructed will create a new control block for the pointed-to Widget (*this)
.
If there are std::shared_ptr
s outside the member function that already point to that Widget
==> undefined behavior.
Standard C++ Library: std::enable_shared_from_this
(a base class template) 专门适合这种场景. 使用方法如下:
1 | class Widget: public std::enable_shared_from_this<Widget> { |
std::enable_shared_from_this
应用了设计模式: The Curiously Recurring Template Pattern (CRTP).
工作机制: Internally, shared_from_this
looks up the control block for the current object(without duplicating control blocks), and it creates a new std::shared_ptr
that refers to that control block.
注意使用的前提是外面已经有一个 std::share_ptr
在指向该实例了, 没有的话是 UB.
The design relies on the current object having an associated control block. there must be an existing std::shared_ptr
(e.g., one outside the member function calling shared_from_this
) that points to the current object. If no such std::shared_ptr
exists (i.e., if the current object has no associated control block), behavior is undefined, although shared_from_this
typically throws an exception.
如何解决上面说到前提不会是 bottleneck 呢? 思路很简单: 不允许误触发 shared_from_this
.
To prevent clients from calling member functions that invoke shared_from_this
before a std::shared_ptr
points to the object, classes inheriting from std::enable_shared_from_this
often declare their constructors private
and have clients create objects by calling factory functions that return std::shared_ptrs
. Widget
:
1 | class Widget: public std::enable_shared_from_this<Widget> { |
control block 与 inheritance
control block implementation makes use of inheritance, and there’s even a virtual function. (It’s used to ensure that the pointed-to object is properly destroyed.)
其他
可以看到 std::shared_ptr
的复杂度: dynamically allocated control blocks, arbitrarily large deleters and allocators, virtual function machinery, and atomic reference count manipulations.
其他特点:
你无法把
std::shared_ptr
转换成std::unique_ptr
.std::shared_ptrs
can’t do is work with arrays. nostd::shared_ptr<T[]>
.
std::shared_ptr
offers nooperator[]
.std::shared_ptr
supports derived-to-base pointer conversions that make sense for single objects, but that open holes in the type system when applied to arrays. (For this reason, thestd::unique_ptr<T[]>
API prohibits such conversions.)
Things to Remember
std::shared_ptrs
offer convenience approaching that of garbage collection for the shared lifetime management of arbitrary resources.
- Compared to
std::unique_ptr
,std::shared_ptr
objects are typically twice as big, incur overhead for control blocks, and require atomic reference
count manipulations. - Default resource destruction is via delete, but custom deleters are supported. The type of the deleter has no effect on the type of the
std::shared_ptr
. - Avoid creating
std::shared_ptrs
from variables of raw pointer type.
Item 20: Use std::weak_ptr
for std::shared_ptr
-like pointers that can dangle.
用法基础
通过与 std::shared_ptr
联合, std::weak_ptr
可以实现: A truly smart pointer would deal with this problem by tracking when it dangles, i.e., when the object it is supposed to point to no longer exists.
std::weak_ptr
isn’t a standalone smart pointer. It’s an augmentation of std::shared_ptr
.
std::weak_ptr
cannot be tested for nullness.
std::weak_ptrs
that dangle are said to have expired.
std::weak_ptrs
are typically created from std::shared_ptrs
. but they don’t affect the reference count of the object they point to:
1 | auto spw = // after spw is constructed, |
理想的状态是既可以检查是否 dangle 又可以访问指向的对象, 但是这是不现实的. but often what you desire is a check to see if a std::weak_ptr
has expired and, if it hasn’t (i.e., if it’s not dangling), to access the object it points to.
std::weak_ptrs
lack dereferencing operations. Why? separating the check and the dereference would introduce a race condition.
检查 dangle
从 std::weak_ptr
产生 std::shared
的两种方式(atomic operation), 两种方式的区别在于 depending on what you’d like to have happen if the std::weak_ptr
has expired when you try to create a std::shared_ptr
from it.
std::weak_ptr::lock
, returnedstd::shared_ptr
is null if thestd::weak_ptr
has expired:
1 | std::shared_ptr<Widget> spw1 = wpw.lock(); // if wpw's expired, spw1 is null |
std::shared_ptr
constructor taking astd::weak_ptr
as an argument. In this case, if thestd::weak_ptr
has expired, an exception is thrown:
1 | std::shared_ptr<Widget> spw3(wpw); // if wpw's expired, throw std::bad_weak_ptr |
应用举例
IO 缓存
使用 factory 生产 IO 数据, 读取后缓存起来, 在使用数据时, 先判断数据是否在缓存中, 是的话直接用, 不是的话读取后再缓存起来.
同时要注意用完的缓存数据, 不需要继续保存下来的话, 也需要提供管理其生命周期的方法(destroy cached Widget
s when they’re no longer in use). 这个 destruction 有可能导致访问其的智能指针不晓得其状态继续访问之, 导致 dangle. 因此使用如下不太好(quick-and-dirty)的例子, 只用来说明思想.
cache 是通过 std::weak_ptr
产生的 std::shared_ptr
进行访问的, 既增加了 shared reading(与 std::unique_ptr
相比), 又可以检查 dangle 与否.
1 | std::unique_ptr<const Widget> loadWidget(WidgetID id);//an expensive call |
上面代码不完美的点在于 std::unorder_map
没有在删除特定缓存数据后将对应的 entry 一并删除.
Observer design pattern
subjects (objects whose state may change) and observers (objects to be notified when state changes occur).
each subject contains a data member holding pointers to its observers. That makes it easy for subjects to issue state change notifications. Subjects have no interest in controlling the lifetime of their observers (i.e., when they’re destroyed), but they have a great interest in making sure that if an observer gets destroyed, subjects don’t try to subsequently access it.
A reasonable design is for each subject to hold a container of std::weak_ptrs
to its observers.
std::shared_ptr
cycles
有什么办法增加一个 B
到 A
的指针吗?
There are three choices:
- A raw pointer. if
A
is destroyed, butC
continues to point toB
,B
will contain a pointer toA
that will dangle. - A
std::shared_ptr
. The resultingstd::shared_ptr
cycle (A
points toB
andB
points toA
) will prevent bothA
andB
from being destroyed. Even ifA
andB
are unreachable from other program data structures (e.g., becauseC
no longer points toB
), each will have a reference count of one. If that happens,A
andB
will have been leaked, for all practical purposes: it will be impossible for the program to access them, yet their resources will never be reclaimed. - A
std::weak_ptr
. This avoids both problems above. IfA
is destroyed,B
’s pointer back to it will dangle, butB
will be able to detect that. Furthermore, thoughA
andB
will point to one another,B
’s pointer won’t affectA
’s reference count, hence can’t keepA
from being destroyed whenstd::shared_ptrs
no longer point to it.
循环指向在现实中并不 common, 例如 strictly hierarchical 父子节点, 子节点的生命周期一定不会长于父节点. When a parent node is destroyed, its child nodes should be destroyed, too.
- Links from parents to children are thus generally best represented by
std::unique_ptrs
. - Back-links from children to parents can be safely implemented as raw pointers, because a child node should never have a lifetime longer than its parent. There’s thus no risk of a child node dereferencing a dangling parent pointer.
efficiency perspective
std::weak_ptr
the same size as std::shared_ptr
(make use of the same control blocks).
std::weak_ptrs
don’t participate in the shared ownership of objects and hence don’t affect the pointed-to object’s reference count.
There’s actually a second reference count in the control block, and it’s this second reference count(weak count) that std::weak_ptrs
manipulate.
Things to Remember
- Use
std::weak_ptr
forstd::shared_ptr
-like pointers that can dangle. - Potential use cases for
std::weak_ptr
include caching, observer lists, and the prevention ofstd::shared_ptr
cycles.
Item 21: Prefer std::make_unique
and std::make_shared
to direct use of new
.
基础
C++11 没有 std::make_unique
, C++ 14 中存在. 自己添加:
1 | template<typename T, typename... Ts> |
缺陷: This form of the function doesn’t support arrays or custom deleters.
进一步注意: Just remember not to put your version in namespace std
, because you won’t want it to clash with a vendor-provided version when you upgrade to a C++14 Standard Library implementation.
three make functions, that take an arbitrary set of arguments, perfect-forward them to the constructor for a dynamically allocated object, and return a smart pointer to that object:
std::make_unique
std::make_shared
std::allocate_shared
why using such functions is preferable
software engineering(code duplication)
Repeating types in new
form. It often evolves into inconsistent code, and inconsistency in a code base often leads to bugs.
1 | auto upw1(std::make_unique<Widget>()); // with make func |
exception safety
如下是一个处理 Widget
的函数, 其参数是 Widget
的指针, 以及一个计算 priority 的函数.
1 | void processWidget(std::shared_ptr<Widget> spw, int priority); |
对于系统而言会经历如下过程:
- The expression
new Widget
must be evaluated, i.e., aWidget
must be created on the heap. - The constructor for the
std::shared_ptr<Widget>
responsible for managing the pointer produced bynew
must be executed. computePriority
must run.
但是 C++ 中对一个函数参数的执行的顺序是不确定的(new Widget
与 std::shared_ptr<Widget>
有依赖关系, 顺序是确定的), 可能的执行顺序如下:
- Perform
new Widget
. - Execute
computePriority
. - Run
std::shared_ptr
constructor.
at runtime, computePriority
produces an exception, the dynamically allocated Widget
from Step 1 will be leaked.
但是换成 std::make_shared
就不会产生这个问题:
1 | processWidget(std::make_shared<Widget>(), // no potential resource leak |
原因在于 std::make_shared
是一个“原子性”的参数, 上面的 step 1 与 3 合在一起执行了. 因此两个参数的执行顺序不再重要, 也不会有资源泄漏的风险.
efficiency
Using std::make_shared
allows compilers to generate smaller, faster code that employs leaner data structures.
- memory allocation
Direct use of new
, requires one memory allocation for the Widget
and a second allocation for the control block.
std::make_shared
allocates a single chunk of memory to hold both the Widget
object and the control block.
This optimization reduces the static size of the program, because the code contains only one memory allocation call, and it increases the speed of the executable code.
- Furthermore, using
std::make_shared
obviates the need for some of the bookkeeping information in the control block, potentially reducing the total memory footprint for the program.
circumstances where make
functions can’t or shouldn’t be used
custom deleters
none of the make
functions permit the specification of custom deleters.
braced initializer
if you want to construct your pointed-to object using a braced initializer, you must use new
directly. make
function only support parentheses initializer.
Why?
Using a make function would require the ability to perfect-forward a braced initializer, but, as Item 30 explains, braced initializers can’t be perfect-forwarded.
束手无策? No
a workaround: use auto
type deduction to create a std::initializer_list
object from a braced initializer.
1 | // create std::initializer_list |
objects of types with class-specific versions of operator new
and operator delete
Often, class-specific routines are designed only to allocate and deallocate chunks of memory of precisely the size of objects of the class, e.g., operator new
and operator delete
for class Widget
are often designed only to handle allocation and deallocation of chunks of memory of exactly size sizeof(Widget)
.
the amount of memory that std::allocate_shared
requests isn’t the size of the dynamically allocated object, it’s the size of that object plus the size of a control block.
因此 2 者无法较好地混合在一起使用.
lag between when an object is destroyed and when the memory it occupied is freed
This is a problem for std::shared_ptr
with std::make_shared
.
上面提及的 std::make_shared
的好处一部分来源于, 其在动态分配内存时, 不像 new
+ std::shared_ptr
那样分成两次分配 2 块内存, 而是分配一大块内存. 这就是 std::make_shared
的 pros and cons 的来源.
这一大块内存包含 2 部分, 一个是被指向 object 的内存, 一个是 control block. control block 里在有 std::weak_ptr
指向 std::shared_ptr
及其对象的时候, 还有 weak count, one that tallies how many std::weak_ptrs
refer to the control block.
考虑如下情况, std::shared_ptr
指向对象的同时还存在 std::weak_ptrs
, 由于是同一块内存, 释放必须一整块释放, 无法单独释放, 这也就是说即便指向对象以及不需要了,需要接下来进行内存释放了,但是由于 control block 中的 weak count 还没有被清空, 这块内存会一直不动, 直到 std::wek_ptr
检查是否已经 expire, 才会整块释放.
The memory allocated by a
std::shared_ptr
make function, then, can’t be deallocated until the laststd::shared_ptr
and the laststd::weak_ptr
referring to it have been destroyed.
If the object type is quite large and the time between destruction of the last std::shared_ptr
and the last std::weak_ptr
is significant, a lag can occur between when an object is destroyed and when the memory it occupied is freed:
1 | class ReallyBigType { … }; |
使用 new
的方式就没有这个情况了.
1 | class ReallyBigType { … }; // as before |
如果无法使用 std::make_shared
如何保证 new
的 exception safety
when you use new
directly, you immediately pass the result to a smart pointer constructor in a statement that does nothing else.
如下:
1 | std::shared_ptr<Widget> spw(new Widget, cusDel); |
最优的做法是通过 std::move
实现这个 exception safe 的过程还能实现效率的优化:
1 | processWidget( |
有一点注意: copying a std::shared_ptr
requires an atomic increment of its reference count, while moving a std::shared_ptr
requires no reference count manipulation at all.
Things to Remember
- Compared to direct use of
new
, make functions eliminate source code duplication, improve exception safety, and, forstd::make_shared
andstd::allocate_shared
, generate code that’s smaller and faster. - Situations where use of
make
functions is inappropriate include the need to specify custom deleters and a desire to pass braced initializers. - For
std::shared_ptrs
, additional situations where make functions may be ill-advised include classes with custom memory management and systems with memory concerns, very large objects, andstd::weak_ptrs
that outlive the correspondingstd::shared_ptrs
.
Item 22: When using the Pimpl Idiom, define special member functions in the implementation file.
背景
关于 Pimpl Idiom 可以查阅博文.
一个概念: A type that has been declared, but not defined, is known as an incomplete type.
使用 Pimpl Idiom 之前:
1 |
|
gadget.h
或者 gadget.h
包含的其他头文件有变化会导致包含 Widget
头文件的 clients 都必须 recompile.
无智能指针版
1 | // still in header "widget.h" |
std::unique_ptr
版本
1 | // in "widget.h" |
但是上面的代码有问题, user 调用后
1 |
|
编译器报错: generally mentions something about applying sizeof
or delete
to an incomplete type.
代码中报错的位置: The message itself often refers to the line where w
is created, because it’s the source code explicitly creating the object that leads to its later implicit destruction. 虽然报错的是 destructor 但是由于 inline
导致报错位置为创建 w
的位置. all compiler-generated special member functions, is implicitly inline
.
原理:
The issue arises due to the code that’s generated when w
is destroyed (e.g., goes out of scope). At that point, its destructor is called.
由于我们没有在 Widget
中显式地定义其析构函数 ~widget()
, 因此编译器会帮我们自动生成一个, 这个自动生成的版本会去调用里面的成员变量(sub-object)的析构函数, 其中一个成员变量是 std::unique_ptr<Impl> pImpl
, 到这里编译器发现无法进行下去了, 因为没有可用的 destructor 可用. 为什么编译器不会帮我们自动生成一个 std::unique_ptr<Impl> pImpl
的析构函数呢?
std::unique_ptr<Impl> pImpl
里的模板参数有 2 个, 一个是 Impl
另一个是其 deleter, 如果没有显式地声明 deleter 那就是 delete
raw pointer 的方式进行析构以及资源回收, 而这显然对 std::unique_ptr<Impl> pImpl
指向的 Impl
struct 是不合适的(应该先析构再 delete
). 之所以使用 default deleter 是因为 std::unique_ptr<Impl> pImpl
无法生成 destructor, 进一步是因为其依赖的 Impl
是 incomplete type. 在 std::unique_ptr
的实现里采用了 C++11’s static_assert
to ensure that the raw pointer doesn’t point to an incomplete type. 报错即来自于此处.
这部分原文内容: std::unique_ptr
using the default deleter. The default deleter is a function that uses delete on the raw pointer inside the std::unique_ptr
. Prior to using delete
, however, implementations typically have the default deleter employ C++11’s static_assert
to ensure that the raw pointer doesn’t point to an incomplete type.
解决方法的思路是让 pImpl
成为 complete type(补上 pImpl
所依赖的部分): 在 widget.cpp
中显式增加 destructor.
1 | //in "widget.h" |
第二种更简单的办法, 并且还能 emphasize that the compiler-generated destructor would do the right thing.
1 | Widget::~Widget() = default; // same effect as above |
support move operations
std::unique_ptr
天然地支持 move, 但是由于我们显式定义了 widget
的析构函数, 因此抑制了自动生成的 move constructor 以及 move assignment. 因此手动添加其如下:
1 | // still in "widget.h" |
有问题, 还是跟上面一样的问题, 但是比较如果思考一下的话, 可能会有疑问: 这可是 move 啊, 在移动构造的过程不涉及到析构的过程, 不应该出现与 copy 一样的错误.
move assignment
The compiler-generated move assignment operator needs to destroy the object pointed to by
pImpl
before reassigning it, but in theWidget
header file,pImpl
points to an incomplete type. 与 copy 的场景类似.move constructor
The situation is different for the move constructor. The problem there is that compilers typically generate code to destroy
pImpl
in the event that an exception arises inside the move constructor, and destroyingpImpl
requires thatImpl
be complete.
解决办法还是把定义放到 .cpp
中.
1 | // still in "widget.h" |
support copy operations
如果 Gadget
data members 是 copyable, 可以考虑实现 copy semantics.
We have to write these functions ourselves, because
- compilers won’t generate copy operations for classes with move-only types like
std::unique_ptr
. - even if they did, the generated functions would copy only the
std::unique_ptr
(i.e., perform a shallow copy), and we want to copy what the pointer points to (i.e., perform a deep copy).
1 | // still in "widget.h" |
通过智能指针的解引用深拷贝 Widget::Impl
里的内容, 而不用一个一个 field 地去拷贝. 剩余地利用编译器自动生成的各级 copy 相关函数即可.
Rather than copy the fields one by one, we take advantage of the fact that compilers will create the copy operations for Impl
, and these operations will copy each field automatically. We thus implement Widget
’s copy operations by calling Widget::Impl
’s compiler-generated copy operations.
use std::shared_ptr
instead of std::unique_ptr
for pImpl
We’d find that the advice of this Item no longer applied. There’d be no need to declare a destructor in Widget
, and without a user-declared destructor, compilers would happily generate the move operations, which would do exactly what we’d want them to.
1 | // in "widget.h" |
为什么 std::shared_ptr
能做到呢?
The difference in behavior between std::unique_ptr
and std::shared_ptr
for pImpl
pointers stems from the differing ways these smart pointers support custom deleters.
- For
std::unique_ptr
, the type of the deleter is part of the type of the smart pointer, and this makes it possible for compilers to generate smaller runtime data structures and faster runtime code. A consequence of this greater efficiency is that pointed-to types must be complete when compiler-generated special functions (e.g.,destructors or move operations) are used. - For
std::shared_ptr
, the type of the deleter is not part of the type of the smart pointer. This necessitates larger runtime data structures and somewhat slower code, but pointed-to types need not be complete when compiler-generated special functions are employed.
但是在 Pimpl Idiom 中 exclusive ownership 排除了 std::shared_ptr
。
Nevertheless, it’s worth knowing that in other situations—situations where shared ownership exists (and std::shared_ptr
is hence a fitting design choice).
Things to Remember
- The Pimpl Idiom decreases build times by reducing compilation dependencies between class clients and class implementations.
- For
std::unique_ptr
pImpl
pointers, declare special member functions in the class header, but implement them in the implementation file. Do this even if the default function implementations are acceptable. - The above advice applies to
std::unique_ptr
, but not tostd::shared_ptr
.
CHAPTER 5 Rvalue References, Move Semantics, and Perfect Forwarding
Rvalue references are the glue that ties these two rather disparate features together. They’re the underlying language mechanism that makes both move semantics and perfect forwarding possible.
It’s especially important to bear in mind that a parameter is always an lvalue, even if its type is an rvalue reference.
1 | void f(Widget&& w); |
the parameter w
is an lvalue, even though its type is rvalue-reference-to-Widget.
下面话说的太好了:
The more experience you have with these features, the more you realize that your initial impression was based on only the metaphorical tip of the proverbial iceberg.
No matter how far you dig into these features, it can seem that there’s always more to uncover. Fortunately, there is a limit to their depths.
Item 23: Understand std::move
and std::forward
.
先打个预防针, 防止看到下面违反直觉的内容觉得不可接受. std::move
doesn’t move anything. std::forward
doesn’t forward anything. At runtime, neither does anything at all. They generate no executable code. Not a single byte.
std::move
std::move
and std::forward
are merely functions (actually function templates) that perform casts. std::move
unconditionally casts its argument to an rvalue, while std::forward
performs this cast only if a particular condition is fulfilled.
- A sample implementation of
std::move
in C++11.
1 | template<typename T> // in namespace std |
结论:
std::move
casts its argument to an rvalue.
那怎么做到的呢?
The &&
part of the function’s return type implies that std::move
returns an rvalue reference.
If the type T
happens to be an lvalue reference, T&&
would become an lvalue reference. To prevent this from happening, the type trait std::remove_reference
is applied to T
, thus ensuring that &&
is applied to a type that isn’t a reference. This guarantees that std::move
truly returns an rvalue reference.
- C++14 implementation
1 | template<typename T> // C++14; still in namespace std |
there have been suggestions that a better name for it might have been something like rvalue_cast
.
有人会说(例如我)既然都返回右值了, 那不就是为了移动语义的吗? 难道右值除了用来移动还有别的更合适的用法吗?
其实 std::move
转换后的右值还真不一定会被用来移动, 而是 copy, 换句话说, std::move
最多只是提示”我准备好了右值, 你有需求就用吧, 比如说移动语义”(to make it easy to designate objects that may be moved from). 也就再一次论证了作者上面的说法, std::move
只提供了转换, 没有其他任何附加的信息与操作.
例子如下, 先定义一个利用 std::move
试图移动 std::string
减少开支来构造 Annotation
类的. 注意, 因为Annotation
类对 text
是只读的, 因此将其声明为 const
.
1 | class Annotation { |
但不幸的是, 我们希望移动 text
结果却是 copy. 下面是 std::string
的构造函数例子.
1 | class string { // std::string is actually a typedef for std::basic_string<char> |
我们可以很清楚地看到 const std::string &&
是不符合移动构造函数的参数类型匹配的. 但是 lvalue-reference-to-const
is permitted to bind toconst rvalue, 匹配拷贝构造函数, 因此最终看起来像是移动实际还是拷贝.
这么做的思想是非常合理的: Moving a value out of an object generally modifies the object, so the language should not permit const
objects to be passed to functions (such as move constructors) that could modify them.
In the Annotation
constructor’s member initialization list, the result of std::move(text)
is an rvalue of type const std::string
. That rvalue can’t be passed to std::string
’s move constructor, because the move constructor takes an rvalue reference to a non-const std::string
. The rvalue can, however, be passed to the copy constructor, because an lvalue-reference-to-const is permitted to bind to a const
rvalue. The member initialization therefore invokes the copy constructor in std::string
, even though text
has been cast to an rvalue!
这个案例给了我们如下 2 点启示:
don’t declare objects
const
if you want to be able to move from them. Move requests onconst
objects are silently transformed into copy operations.std::move
not only doesn’t actually move anything, it doesn’t even guarantee that the object it’s casting will be eligible to be moved. The only thing you know for sure about the result of applyingstd::move
to an object is that it’s an rvalue.
std::forward
std::forward
与 std::move
的区别: std::move
unconditionally casts its argument to an rvalue. std::forward
is a conditional cast.
std::forward
casts to an rvalue only if its argument was initialized with references to which rvalues have been bound. 否则传入的参数都会被当作左值处理.
std::forward
如何知道传入的是 rvalue 呢? 看 Item 28, 引用折叠.
可以用 std::forward
取代 std::move
吗?
Whether we can dispense with std::move
and just use std::forward
everywhere. From a purely technical perspective, the answer is yes: std::forward
can do it all. std::move
isn’t necessary. Of course, neither function is really necessary, because we could write casts everywhere.
std::move
’s attractions are convenience, reduced likelihood of error, and greater clarity.
对比例子如下:
1 | class Widget { |
std::move
的优点:
std::move
requires only a function argument (rhs.s
), whilestd::forward
requires both a function argument (rhs.s
) and a template type argument. 间接地减少了出错的可能性: eliminates the possibility of our passing an incorrect type (e.g.,std::string&
, which would result in the data members
being copy constructed instead of move constructed).语义信息不一样, 需要强制转成 rvalue 的时候, 还是需要
std::move
. More importantly, the use ofstd::move
conveys an unconditional cast to an rvalue, while the use ofstd::forward
indicates a cast to an rvalue only for referenceswhich rvalues have been bound.
Things to Remember
std::move
performs an unconditional cast to an rvalue. In and of itself, it doesn’t move anything.std::forward
casts its argument to an rvalue only if that argument is bound to an rvalue.- Neither
std::move
norstd::forward
do anything at runtime.
Item 24: Distinguish universal references from rvalue references.
T&&
不见得一定是指 rvalue reference.
1 | void f(Widget&& param); // rvalue reference |
T&&
has two different meanings.
- One is rvalue reference.
- the other is universal references.
It is either rvalue reference or lvalue reference. It can bind toconst
or non-const
objects, tovolatile
or non-volatile
objects, even to objects that are bothconst
andvolatile
. They can bind to virtually anything.
universal references 的使用场景, 只存在于 type deduction 环境中:
function template parameters
1
2template<typename T>
void f(T&& param); // param is a universal referenceauto
declarations1
auto&& var2 = var1; // var2 is a universal reference
如何区分 T&&
下的 rvalue reference 与 universal references?
the presence of type deduction or not.
如何确定 universal references 最终是左值还是右值引用?
Because universal references are references, they must be initialized. The initializer for a universal reference determines whether it represents an rvalue reference or an lvalue reference.
1 | template<typename T> |
注意 universal reference 存在的其他要点
- For a reference to be universal, type deduction is necessary, but it’s not sufficient. The form of the reference declaration must also be correct. It must be precisely
T&&
.
1 | template<typename T> |
the form of param
’s type declaration isn’t T&&
, it’s std::vector<T>&&
. That rules out the possibility that param
is a universal reference. param
is therefore an rvalue reference. 使用左值调用编译器会报错.
- Even the simple presence of a
const
qualifier is enough to disqualify a reference from being universal:
1 | template<typename T> |
- being in a template doesn’t guarantee the presence of type deduction. 换句话说, 调用函数里的参数
T&&
必须直接依赖于T
而不是间接地依赖于T
.
Consider push_back
member function in std::vector
:
1 | template<class T, class Allocator = allocator<T>> // from C++ Standards |
there’s no type deduction in this case. That’s because push_back
can’t exist without a particular vector instantiation for it to be part of, and the type of that instantiation fully determines the declaration for push_back
.
1 | std::vector<Widget> v; |
conceptually similar emplace_back
member function in std::vector
does employ type deduction:
1 | template<class T, class Allocator = allocator<T>> // still from C++ Standards |
the type parameter Args
is independent of vector’s type parameter T
. Args
is really a parameter pack, not a type parameter, but for purposes of this discussion, we can treat it as if it were a type parameter.
- variables declared with the type
auto&&
are universal references
C++14 lambda expressions may declare auto&&
parameter. 下面是一个记录函数运行时间的函数.
1 | auto timeFuncInvocation = |
func
is a universal reference that can be bound to any callable object, lvalue or rvalue. args
is zero or more universal references (i.e., a universal reference parameter pack) that can be bound to any number of objects of arbitrary types.
The result, thanks to auto universal references, is that timeFuncInvocation
can time pretty much any function execution. Difference between “any” and “pretty much any,” turn to Item 30.
Things to Remember
- If a function template parameter has type
T&&
for a deduced typeT
, or if an object is declared usingauto&&
, the parameter or object is a universal reference. - If the form of the type declaration isn’t precisely
type&&
, or if type deduction does not occur,type&&
denotes an rvalue reference. - Universal references correspond to rvalue references if they’re initialized with rvalues. They correspond to lvalue references if they’re initialized with lvalues.
Item 25: Use std::move
on rvalue references, std::forward
on universal references.
In short, rvalue references should be unconditionally cast to rvalues (via std::move
) when forwarding them to other functions, because they’re always bound to rvalues.
Universal references should be conditionally cast to rvalues (via std::forward
) when forwarding them, because they’re only sometimes bound to rvalues.
分析
用在不是规定场景的理由:
using std::forward
on rvalue references can be made to exhibit the proper behavior, but the source code is wordy, error-prone, and unidiomatic, so you should avoid using std::forward
with rvalue references.
Even worse is the idea of using std::move
with universal references, because that can have the effect of unexpectedly modifying lvalues (e.g., local variables):
1 | class Widget { |
上面的结果是强制把 n
变成 rvalue, 然后通过 T&&
推导为 rvalue reference, 导致 n
的值被 w
偷走了(留下的是 unspecified value). 如果下面接着使用 n
的话, 会导致 UB.
解决此方法, overloaded for const
lvalues and for rvalues:
1 | class Widget { |
重载有缺陷:
- more source code to write and maintain
- might be less efficient. 考虑如下场景
1 | w.setName("Adela Novak"); |
With the version of
setName
taking a universal reference, the string literal"Adela Novak"
would be passed tosetName
, where it would be conveyed to the assignment operator for thestd::string
insidew
.w
’s name data member would thus be assigned directly from the string literal(即std::string
assignment operator taking aconst char*
pointer); no temporarystd::string
objects would arise.With the overloaded versions of
setName
, however, a temporarystd::string
object would be created forsetName
’s parameter to bind to, and this temporarystd::string
would then be moved intow
’s data member. A call tosetName
would thus entail execution of onestd::string
constructor (to create the temporary), onestd::string
move assignment operator (to movenewName
intow.name
), and onestd::string
destructor (to destroy the temporary).
虽然不同 implementation 不同数据结构上面的两种过程对比的效率不是绝对的, 但是可以说 likely to incur a runtime cost in some cases.
这里初始化过程的差异, 我觉得是一个很重要的点, 使用万能引用可以延迟对参数的 evaluation, 直接到行参构建的那一步.
反映到这里就是如果不使用万能引用, 字符串字面量是左值会去匹配 void setName(const std::string& newName)
的重载版本, 在进行函数体 name = newName;
之前需要对参数 evaluation, 结果是构造一个 temporary 然后传递给行参.
而使用万能引用则是延迟对参数的 evaluation, 而是在构建行参 newName
的时候直接利用字符串字面量进行初始化, 从而省略了临时 subject 的产生. 这个与后面介绍(Item 41)的 emplacement 是类似的思想.
poor scalability of the design
$n$ parameters necessitates $2^n$ overloads.
Some functions—function templates, actually—take an unlimited number of parameters, each of which could be an lvalue or rvalue.
基于上面 3 个不好的点, 推荐使用 std::forward
.
一些应用场景
- 一个函数中多次使用 rvalue reference 或者 universal reference 保证最后再 apply
std::move
(for rvalue references) orstd::forward
(for universal references).
In some cases, you’ll want to use the object bound to an rvalue reference or a universal reference more than once in a single function, and you’ll want to make sure that it’s not moved from until you’re otherwise done with it. In that case, you’ll want to apply std::move
(for rvalue references) or std::forward
(for universal references) to only the final use of the reference. For example:
1 | template<typename T> // text is univ. reference |
std::move_if_noexcept
In rare cases, you’ll want to call std::move_if_noexcept
instead of std::move
. 防止在 move 过程中有异常导致程序异常中止.
std::move_if_noexcept
is a variation of std::move
that conditionally casts to an rvalue, depending on whether the type’s move constructor is noexcept
. In turn, std::move_if_noexcept
consults std::is_nothrow_move_constructible
, and the value of this type trait is set by compilers, based on whether the move constructor has a noexcept
(or throw()
) designation.
- return by value
std::move
If you’re in a function that returns by value, and you’re returning an object bound to an rvalue reference or a universal reference(这么做的目的是 have its storage reused to hold the sum of the matrices), you’ll want to apply std::move
or std::forward
when you return the reference. 例子, 两个矩阵相加通过 operator +
实现.
1 | // 返回 rvalue reference |
If Matrix
does not support moving, casting it to an rvalue won’t hurt, because the rvalue will simply be copied by Matrix’s copy constructor. If Matrix
is later revised to support moving, operator+
will automatically benefit the next time it is compiled.
std::forward
If the original object is an rvalue, its value should be moved into the return value, but if the original is an lvalue, an actual copy must be created.
1 | template<typename T> |
return value optimization (RVO)
不要在 RVO 下画蛇添足
1 | //before "optimization" |
有些聪明人想到, 是否可以通过把函数里的 local variables move 从而提升效率呢? 上面是其例子. 但是不要这么做, 如果我们再加上一个 move 成 rvalue 的操作反而会破坏返回值优化.
ROV 的标准规定: compilers may(不是强制) elide the copying (or moving) of a local object in a function that returns by value if
- the type of the local object is the same as that returned by the function and
- the local object is what’s being returned.
注: 这里的定义有些人的说法是 named return value optimization (NRVO). 而 local object 没有 name 而是直接通过 return 返回的场景才叫做 RVO, 作者这里没有加以细分.
在画蛇添足之前, 是满足 RVO 条件的, 但是 return std::move(w);
这句话使得 ROV 的条件 2 不满足了. 因为 std::move
后的类型是 rvalue reference 而返回值不包含 reference. 即 Developers trying to help their compilers optimize by applying std::move
to a local variable that’s being returned are actually limiting the optimization options available to their compilers! 具体如何限制的? (assuming Widget
offers a move constructor): 这样 moves the contents of w
into makeWidget
’s
return value location. 换句话说, w
与 return value 是并行存在的, 只不过从 copy 过去变成了 move 过去.
不启用 RVO 下也不要随意指定 move
有些时候我想关闭 RVO, 理由如下:
- you worry that your compilers will punish you with copy operations, just because they can.
- perhaps you’re insightful enough to recognize that there are cases where the RVO is difficult for compilers to implement, e.g., when different control paths in a function return different local variables. (Compilers would have to generate code to construct the appropriate local variable in the memory allotted for the function’s return value, but how could compilers determine which local variable would be appropriate?)
然后再加 std::move
是不是能起到改善作用? 答案是否, 加不加都一样.
解释: 因为 C++ 的标准还有附加条款, if the conditions for the RVO are met, but compilers choose not to perform copy elision, the object being returned must be treated as an rvalue.
1 | Widget makeWidget() // as before |
在不执行 copy elision 优化的编译器必须转换成如下形式:
1 | Widget makeWidget() |
对于使用 by-value 传入的参数即便不满足 ROV 的条件也会执行类似的 copy elision 优化. The situation is similar for by-value function parameters. They’re not eligible for copy elision with respect to their function’s return value, but compilers must treat them as rvalues if they’re returned.
1 | Widget makeWidget(Widget w) // by-value parameter of same type as function's return |
Things to Remember
- Apply
std::move
to rvalue references andstd::forward
to universal references the last time each is used. - Do the same thing for rvalue references and universal references being returned from functions that return by value.
- Never apply
std::move
orstd::forward
to local objects if they would otherwise be eligible for the return value optimization.
Item 26: Avoid overloading on universal references.
一个不考虑使用移动优化的案例:
1 | std::multiset<std::string> names; // global data structure |
第一个 petName
是左值, 因此不存在优化空间.
第二个是临时的 std::string
对象, 然后通过 std::multiset
的 emplace
函数将临时对象 copy 到容器内, 因此存在优化空间.
第三个是 string literal, 如果我们能够直接在 std::multiset
里利用 string literal 构造出一个 std::string
元素, no need to create a temporary std::string at all
, 并且连 move 都省下来了.
改造后:
1 | template<typename T> |
但这样做会有风险.
增加函数参数, 重载 universal references 可行吗?
如果有新的需求: 增加 look-up table, 使用 index 添加 std::multiset
的方式, 直接添加 overload 函数.
1 | std::string nameFromIdx(int idx); // return name corresponding to idx |
但是出错了:
1 | short nameIdx; |
原因是:
The overload with an int
parameter can match the short
argument only with a promotion. so the universal reference overload is invoked.
然而 There is no constructor for std::string
that takes a short
, so the std::string
constructor call inside the call to multiset::emplace
inside the call to logAndAdd
fails.
Functions taking universal references are the greediest functions in C++. They instantiate to create exact matches for almost any type of argument. (The few kinds of arguments where this isn’t the case are described in Item 30.)
那使用 std::froward
如何呢?
1 | class Person { |
由于 template function 无法阻止编译器自动生成构造函数, 编译器眼里的函数声明是下面这样的:
1 | class Person { |
这样做也会有问题:
1 | Person p("Nancy"); |
直观上来看, 既然有自动生成的 copy constructor, 那我用左值来构建一个对象应该没问题, 然后并不是. 报错的原因: this code won’t call the copy constructor. It will call the perfect forwarding constructor. That function will then try to initialize Person
’s std::string
data member with a Person
object (p
). std::string
having no constructor taking a Person
.
问题就来了, 为什么 template constructor 函数会比 copy constructor 的匹配优先级更高呢?
overload resolution: cloneOfP
is being initialized with a non-const lvalue (p
), and that means that the templatized constructor can be instantiated to take a non-const
lvalue of type Person
.
After such instantiation, the Person
class looks like this:
1 | class Person { |
相比于 const
类型的 copy constructor, 明显实例化后的 template function 匹配度更高, 编译器 generate a call to the better-matching function.
如果声明为 const
就变成优先匹配 copy constructor 了. one of the overload-resolution rules in C++ is that in situations where a template instantiation and a non-template function (i.e., a “nor‐mal” function) are equally good matches for a function call, the normal function is preferred.
1 | const Person cp("Nancy"); // object is now const |
还要考虑重载对类继承的影响
1 | class SpecialPerson: public Person { |
这样做编译不通过.
- the derived class copy and move constructors don’t call their base class’s copy and move constructors, they call the base class’s perfect forwarding constructor!
- the derived class functions are using arguments of type
SpecialPerson
to pass to their base class. There’s nostd::string
constructor taking aSpecialPerson
.
Things to Remember
- Overloading on universal references almost always leads to the universal reference overload being called more frequently than expected.
- Perfect-forwarding constructors are especially problematic, because they’re typically better matches than copy constructors for non-const lvalues, and they can hijack derived class calls to base class copy and move constructors.
Item 27: Familiarize yourself with alternatives to overloading on universal references.
Either through designs that avoid overloading on universal references or by employing them in ways that constrain the types of arguments they can match.
Abandon overloading
simply using different names for the would-be overloads.
Pass by const T&
to revert to C++98 and replace pass-by-universal-reference with pass-by-lvalue-reference-to-const
.
The drawback is that the design isn’t as efficient as we’d prefer.
Pass by value
An approach that often allows you to dial up performance without any increase in complexity is to replace pass-by-reference parameters with, counterintuitively, pass by value. 详细在 Item 41 中解释.
1 | class Person { |
Because there’s no std::string
constructor taking only an integer, all int
and int
-like arguments to a Person
constructor (e.g., std::size_t
, short
, long
) get funneled to the int
overload.
Similarly, all arguments of type std::string
(and things from which std::strings
can be created, e.g., literals such as “Ruth”) get passed to the constructor taking a std::string
.
There are thus no surprises for callers.
除了效率, 唯一的可攻击点 using 0 or NULL
to indicate a null pointer would invoke the int
overload. 但这种情况, 我们应该使用 nullptr
.
Use Tag dispatch
原理: A universal reference parameter generally provides an exact match for whatever’s passed in, but if the universal reference is part of a parameter list containing other parameters that are not universal references, sufficiently poor matches on the non-universal reference parameters can knock an overload with a universal reference out of the running.
1 | std::multiset<std::string> names; // global data structure |
logAndAdd
itself will accept all argument types, both integral and non-integral. The two functions doing the real work will be named logAndAddImpl
.
1 | template<typename T> |
上面代码的问题是: if an lvalue argument is passed to the universal reference name, the type deduced for T
will be an lvalue reference. So if an lvalue of type int
is passed to logAndAdd
, T
will be deduced to be int&
. That’s not an integral type.
remove any reference qualifiers from a type: std::remove_reference
1 | template<typename T> |
完成重载的函数
1 | template<typename T> // non-integral argument: |
std::false_type
介绍
Conceptually, logAndAdd
passes a boolean
to logAndAddImpl
indicating whether an integral type was passed to logAndAdd
, but true and false
are runtime values, and we need to use overload resolution—a compile-time phenomenon—to choose the correct logAndAddImpl
overload. That means we need a type that corresponds to true and a different type that corresponds to false. This need is common enough that the Standard Library provides what is required under the names std::true_type
and std::false_type
.
In this design, the types std::true_type
and std::false_type
are tags whose only purpose is to force overload resolution to go the way we want. Notice that we don’t even name those parameters. They serve no purpose at runtime, and in fact we hope that compilers will recognize that the tag parameters are unused and will optimize them out of the program’s execution image.
The call to the overloaded implementation functions inside logAndAdd
dispatches the work to the correct overload by causing the proper tag object to be created. Hence the name for this design: tag dispatch.
It’s a standard building block of template metaprogramming, and the more you look at code inside contemporary C++ libraries, the more often you’ll encounter it.
Constraining templates that take universal references
Compilers may generate copy and move constructors themselves, so even if you write only one constructor and use tag dispatch within it, some constructor calls may be handled by compiler-generated functions that bypass the tag dispatch system.
In truth, the real problem is not that the compiler-generated functions sometimes bypass the tag dispatch design, it’s that they don’t always pass it by.
For situations like these, where an overloaded function taking a universal reference is greedier than you want, yet not greedy enough to act as a single dispatch function, tag dispatch is not the droid you’re looking for.
std::enable_if
gives you a way to force compilers to behave as if a particular template didn’t exist. Such templates are said to be disabled. By default, all templates are enabled.
1 | class Person { |
std::enable_if
的后面是 SFINAE
技术.
如何判断呢? 比较 type trait!
when we’re looking at T
, we want to ignore
- Whether it’s a reference. For the purpose of determining whether the universal reference constructor should be enabled, the types
Person
,Person&
, andPerson&&
are all the same asPerson
. - Whether it’s
const
orvolatile
. As far as we’re concerned, aconst Person
and avolatile Person
and aconst volatile Person
are all the same as aPerson
.
这就要使用 std::decay<T>::type
is the same as T
, except that references and cv-qualifiers (i.e.,const
or volatile
qualifiers) are removed.
std::decay
, as its name suggests, also turns array and function types into pointers.
!std::is_same<Person, typename std::decay<T>::type>::value
: Person
is not the same type as T
, ignoring any references or cv-qualifiers.
插入完工:
1 | class Person { |
处理继承关系的 universal reference 重载
首先确定修改方向: The derived class is just following the normal rules for implementing derived class copy and move constructors, so the fix for this problem is in the base class.
We now realize that we don’t want to enable the templatized constructor for any argument type other than Person
, we want to enable it for any argument type other than Person
or a type derived from Person
. Pesky inheritance!
使用 std::is_base_of<T1, T2>::value
is true if T2
is derived from T1
.
1 | class Person { |
C++14 可以再简单一些:
1 | class Person { // C++14 |
在上面的基础上判断类型进行重载
1 | class Person { |
Trade-offs
perfect forwarding has drawbacks.
some kinds of arguments can’t be perfect-forwarded, even though they can be passed to functions taking specific types. Item 30 explores these perfect forwarding failure cases.
comprehensibility of error messages when clients pass invalid arguments. 解决办法:
static_assert
The
std::is_constructible
type trait performs a compile-time test to determine whether an object of one type can be constructed from an object (or set of objects) of a different type (or set of types), so the assertion is easy to write:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22class 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)
};This causes the specified error message to be produced if client code tries to create a
Person
from a type that can’t be used to construct astd::string
.
Things to Remember
- Alternatives to the combination of universal references and overloading include the use of distinct function names, passing parameters by lvalue-
reference-to-const, passing parameters by value, and using tag dispatch. - Constraining templates via
std::enable_if
permits the use of universal references and overloading together, but it controls the conditions under which compilers may use the universal reference overloads. - Universal reference parameters often have efficiency advantages, but they typically have usability disadvantages.
Item 28: Understand reference collapsing.
1 | template<typename T> |
When an lvalue is passed as an argument, T
is deduced to be an lvalue reference. When an rvalue is passed, T
is deduced to be a non-reference(Note the asymmetry).
references to references are illegal in C++.
1 | int x; |
1 | template<typename T> |
how?
The answer is reference collapsing. When compilers generate references to references, reference collapsing dictates what happens next.
If a reference to a reference arises in a context where this is permitted (e.g., during template instantiation), the references collapse to a single reference according to this rule:
If either reference is an lvalue reference, the result is an lvalue reference. Otherwise (i.e., if both are rvalue references) the result is an rvalue reference.
Reference collapsing is a key part of what makes std::forward
work.
Here’s how std::forward
can be implemented to do that:
1 | template<typename T> // in namespace std |
Reference collapsing is also applied to the return type and the cast.
C++14 更简洁一些
1 | template<typename T> // C++14; still in namespace std |
Reference collapsing occurs in four contexts.
most common is template instantiation.
type generation for
auto
variables.1
2
3
4
5
6
7
8
9template<typename T>
void func(T&& param);
Widget widgetFactory(); // function returning rvalue a variable (an lvalue)
Widget w;
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; //deducing the type Widget& for auto
auto&& w2 = widgetFactory(); //initializes w2 with an rvalue, w2 is an rvalue reference.the generation and use of
typedefs
and alias declarations.uses of
decltype
.
A universal reference isn’t a new kind of reference, it’s actually an rvalue reference in a context where two conditions are satisfied:
- Type deduction distinguishes lvalues from rvalues. Lvalues of type
T
are deduced to have typeT&
, while rvalues of typeT
yieldT
as their deduced type. - Reference collapsing occurs.
Things to Remember
- Reference collapsing occurs in four contexts: template instantiation,
auto
type generation, creation and use oftypedefs
and alias declarations, anddecltype
. - When compilers generate a reference to a reference in a reference collapsing context, the result becomes a single reference. If either of the original references is an lvalue reference, the result is an lvalue reference. Otherwise it’s an rvalue reference.
- Universal references are rvalue references in contexts where type deduction distinguishes lvalues from rvalues and where reference collapsing occurs.
Item 29: Assume that move operations are not present, not cheap, and not used.
For types in your applications (or in the libraries you use) where no modifications for C++11 have been made, the existence of move support in your compilers is likely to do you little good. For types without explicit support for moving and that don’t qualify for compiler-generated move operations, there is no reason to expect C++11 to deliver any kind of performance improvement over C++98.
Even types with explicit move support may not benefit as much as you’d hope. All containers in the standard C++11 library support moving, for example, but it would be a mistake to assume that moving all containers is cheap.
std::vector
VS std::array
std::array
, a new container in C++11. std::array
is essentially a built-in array with an STL interface.
both moving and copying a std::array
have linear-time computational complexity, because each element in the container must be copied or moved.
std::string
是另外一个例子.
std::string
offers constant-time moves and linear-time copies. That makes it sound like moving is faster than copying, but that may not be the case.
Many string implementations employ the small string optimization (SSO). With the SSO, “small” strings (e.g., those with a capacity of no more than 15 characters) are stored in a buffer within the std::string
object; no heap-allocated storage is used. Moving small strings using an SSO-based implementation is no faster than copying them, because the copy-only-a-pointer trick that generally underlies the performance advantage of moves over copies isn’t applicable.
Using an internal buffer to store the contents of such strings eliminates the need to dynamically allocate memory for them, and that’s typically an efficiency win. An implication of the win, however, is that moves are no faster than copies, though one could just as well take a glass-half-full approach and say that for such strings, copying is no slower than moving.
noexcept
Even if a type offers move operations that are more efficient than the corresponding copy operations, and even if, at a particular point in the code, a move operation would generally be appropriate (e.g., if the source object is an rvalue), compilers might still be forced to invoke a copy operation because the corresponding move operation isn’t declared noexcept
.
总结一下:
There are thus several scenarios in which C++11’s move semantics do you no good:
- No move operations: The object to be moved from fails to offer move operations. The move request therefore becomes a copy request.
- Move not faster: The object to be moved from has move operations that are no faster than its copy operations.
- Move not usable: The context in which the moving would take place requires a move operation that emits no exceptions, but that operation isn’t declared
noexcept
. - Source object is lvalue: With very few exceptions (see e.g., Item 25) only rvalues may be used as the source of a move operation.
you must be as conservative about copying objects as you were in C++98—before move semantics existed.
Things to Remember
- Assume that move operations are not present, not cheap, and not used.
- In code with known types or support for move semantics, there is no need for assumptions.
Item 30: Familiarize yourself with perfect forwarding failure cases.
forwarding 的概念: “Forwarding” just means that one function passes—forwards —its parameters to another function.
The goal is for the second function (the one being forwarded to) to receive the same objects that the first function (the one doing the forwarding) received.
That rules out by-value parameters, because they’re copies of what the original caller passed in.
Pointer parameters are also ruled out, because we don’t want to force callers to pass pointers.
we’ll be dealing with parameters that are references.
Perfect forwarding means we don’t just forward objects, we also forward their salient characteristics: their types.
variadic templates: accepts any number of arguments.
1 | template<typename... Ts> |
perfect forwarding failure 定义
1 | f( expression ); // if this does one thing, |
Braced initializers
现象描述:
1 | void f(const std::vector<int>& v); |
Compilers no longer compare the arguments passed at fwd
’s call site to the parameter declarations in f
. Instead, they deduce the types of the arguments being passed to fwd
, and they compare the deduced types to f
’s parameter declarations.
Perfect forwarding fails when either of the following occurs:
Compilers are unable to deduce a type for one or more of
fwd
’s parameters.Compilers deduce the “wrong” type for one or more of
fwd
’s parameters. One source of such divergent behavior would be iff
were an overloaded function name, and, due to “incorrect” type deduction, the overload off
called insidefwd
were different from the overload that would be invoked iff
were called directly.
Standard puts a braced initializer as a non-deduced context . a braced initializer is not a a std::initializer_list
.
解决办法, 使用 auto
:
1 | auto il = { 1, 2, 3 }; // il's type deduced to be std::initializer_list<int> |
0 or NULL
as null pointers
deducing an integral type (typically int
) instead of a pointer type
pass nullptr
instead of 0 or NULL
.
Declaration-only integral static const
data members
const
propagation
1 | class Widget { |
As a general rule, there’s no need to define integral static const
data members in classes; declarations alone suffice. That’s because compilers perform const
propagation on such members’ values, thus eliminating the need to set aside memory for them.
used as pointer/reference(需要分配内存)
If MinVals
’ address were to be taken (e.g., if somebody created a pointer to MinVals
), then MinVals
would require storage (so that the pointer had something to point to), and the code above, though it would compile, would fail at link-time until a definition for MinVals
was provided.
1 | void f(std::size_t val); |
fwd
’s parameter is a universal reference, and references, in the code generated by compilers, are usually treated like pointers.
But not all implementations enforce this requirement. So, depending on your compilers and linkers, you may find that you can perfect-forward integral static const
data members that haven’t been defined.
To make it portable, simply provide a definition for the integral static const
data member in question.
1 | const std::size_t Widget::MinVals; // in Widget's .cpp file |
Note that the definition doesn’t repeat the initializer.
Overloaded function names and template names
Suppose our function f
can have its behavior customized by passing it a function that does some of its work.
1 | void f(int (*pf)(int)); // pf = "processing function" |
区别的原因:
compilers know which processVal
they need: the one matching f
’s parameter type. They thus choose the processVal
taking one int
, and they pass that function’s address to f
. What makes this work is that f
’s declaration lets compilers figure out which version of processVal
is required.
函数指针无法被推断出类型.
fwd
, however, being a function template, doesn’t have any information about what type it needs. processVal
alone has no type. Without a type, there can be no type deduction.
同样地, 模板函数是多个函数的重载. A function template doesn’t represent one function, it represents many functions:
1 | template<typename T> |
解决办法: 显式指定类型
1 | using ProcessFuncType = int (*)(int);// make typedef; |
因此这造成了对重载函数使用 universal reference 的限制: 必须知道函数的类型.
This requires that you know the type of function pointer that fwd
is forwarding to. It’s not unreasonable to assume that a perfect-forwarding function will document that.
Bitfields
1 | struct IPv4Header { |
The problem is that fwd
’s parameter is a reference, and h.totalLength
is a non-const
bitfield. C++ Standard: “A non-const
reference shall not be bound to a bit-field.”
Why? Bitfields may consist of arbitrary parts of machine words (e.g., bits 3-5 of a 32-bit int), but there’s no way to directly address such things.
底层的原因是 C++ 的指针能指向的最小单位是 char
, 小于此类型的数据无法通过指针表达出来, 因此只能通过 copy 的形式先转换成 int
等能够被识别的类型, 然后再细分读取相应的数据段.
Pointers to bitfields don’t exist: there’s no way to create a pointer to arbitrary bits (C++ dictates that the smallest thing you can point to is a char
), there’s no way to bind a reference to arbitrary bits, either.
解决办法: once you realize that any function that accepts a bitfield as an argument will receive a copy of the bitfield’s value.
The only kinds of parameters to which a bitfield can be passed are by-value parameters and, interestingly, references-to-const
.
In the case of by-value parameters, the called function obviously receives a copy of the value in the bitfield, and it turns out that in the case of a reference-to-const
parameter, the Standard requires that the reference actually bind to a copy of the bitfield’s value that’s stored in an object of some standard integral type (e.g., int
). References-to-const
don’t bind to bitfields, they bind to “normal” objects into which the values of the bitfields have been copied.
1 | // copy bitfield value; see Item 6 for info on init. form |
Things to Remember
- Perfect forwarding fails when template type deduction fails or when it deduces the wrong type.
- The kinds of arguments that lead to perfect forwarding failure are braced initializers, null pointers expressed as 0 or
NULL
, declaration-only integralconst
static data members, template and overloaded function names, and bitfields.
《effective modern C++》Chapter 4-5