《effective modern C++》Chapter 4-5

《effective modern C++》Chapter 4-5

[TOC]

本文记录《effective modern C++》第 4, 5 章的学习笔记.

CHAPTER 4 Smart Pointers

raw pointer 的不好地方:

  1. Its declaration doesn’t indicate whether it points to a single object or to an array. 不是类型安全的.
  2. 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. 无法自动管理资源.
  3. 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)? 无法判断使用正确的方式析构.
  4. 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.
  5. 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 导致混乱.
  6. 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_ptrs 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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

//caller
{

auto pInvestment = // pInvestment is of type
makeInvestment( arguments ); // std::unique_ptr<Investment>

} // destroy *pInvestment

即便遇到 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 an exit function (i.e., std::_Exit, std::exit, or std::quick_exit)is called.

std::unique_ptr can be configured to use custom deleters:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//custom deleter (a lambda expression)
//新增 log 输出
auto delInvmt = [](Investment* pInvestment)
{
makeLogEntry(pInvestment);
delete pInvestment;
};

template<typename... Ts> // revised return type
std::unique_ptr<Investment, decltype(delInvmt)>
makeInvestment(Ts&&... params)
{
std::unique_ptr<Investment, decltype(delInvmt)> // ptr to be returned
pInv(nullptr, delInvmt);
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;
}

//with C++ 14
//把 lambda 函数封装进去
template<typename... Ts>
auto makeInvestment(Ts&&... params) // C++14
{
auto delInvmt = [](Investment* pInvestment) // this is now inside make-Investment
{
makeLogEntry(pInvestment);
delete pInvestment;
};
std::unique_ptr<Investment, decltype(delInvmt)> // as before
pInv(nullptr, delInvmt);
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
}

几点需要说明:

  • Attempting to assign a raw pointer (e.g., from new) to a std::unique_ptr won’t compile, because it would constitute an implicit conversion from a raw to a smart pointer. That’s why reset 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
2
3
4
template <typename... Ts>
std::unique_ptr<Investment,
void (*)(Investment *)>// return type has size of Investment* plus at least size of function pointer!
makeInvestment(Ts &&...params);

常用场景举例 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, and std::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
2
3
std::shared_ptr<Investment> sp =
makeInvestment( arguments );
// converts std::unique_ptr to std::shared_ptr

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 a std::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
2
3
4
5
6
7
8
9
10
11
12
13
auto loggingDel = [](Widget *pw) // custom deleter
{
makeLogEntry(pw);
delete pw;
};

// deleter type is part of ptr type
std::unique_ptr<Widget, decltype(loggingDel)>
upw(new Widget, loggingDel);

// deleter type is not part of ptr type
std::shared_ptr<Widget>
spw(new Widget, loggingDel);

因此可以让包含不同 deleter 的 std::shared_ptr放入同质容器, 例如 std::vector 中.

1
2
3
4
5
6
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 };

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 time std::make_shared is called.

  • A control block is created when a std::shared_ptr is constructed from a unique-ownership pointer (i.e., a std::unique_ptr or std::auto_ptr). As part of its construction, the std::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 a std::shared_ptr or a std::weak_ptr as a constructor argument, not a raw pointer. std::shared_ptr constructors taking std::shared_ptrs or std::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
2
3
4
5
auto pw = new Widget; // pw is raw ptr

std::shared_ptr<Widget> spw1(pw, loggingDel); // create control block for *pw

std::shared_ptr<Widget> spw2(pw, loggingDel); // create 2nd control block for *pw!

建议:

  1. try to avoid passing raw pointers to a std::shared_ptr constructor. The usual alternative is to use std::make_shared.

  2. if you must pass a raw pointer to a std::shared_ptr constructor, pass the result of new directly instead of going through a raw pointer variable.

1
2
std::shared_ptr<Widget> spw1(new Widget, loggingDel);// direct use of new
std::shared_ptr<Widget> spw2(spw1); // spw2 uses same control block as spw1

std::enable_shared_from_this

this pointer 导致的 Multiple control blocks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
std::vector<std::shared_ptr<Widget>> processedWidgets;

class Widget {
public:

void process();

};

void Widget::process()
{
// process the Widget
processedWidgets.emplace_back(this); // add it to list of processed Widgets;this is wrong!
}

危害在于 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_ptrs 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
2
3
4
5
6
7
8
9
10
11
12
13
14
class Widget: public std::enable_shared_from_this<Widget> {
public:

void process();

};

void Widget::process()
{
// as before, process the Widget

// add std::shared_ptr to current object to processedWidgets
processedWidgets.emplace_back(shared_from_this());
}

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
2
3
4
5
6
7
8
9
10
11
class Widget: public std::enable_shared_from_this<Widget> {
public:
// factory function that perfect-forwards args to a private ctor
template<typename... Ts>
static std::shared_ptr<Widget> create(Ts&&... params);

void process(); // as before

private:
// ctors
};

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. no std::shared_ptr<T[]>.

  1. std::shared_ptr offers no operator[].
  2. 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, the std::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
2
3
4
5
6
7
8
auto spw = // after spw is constructed,
std::make_shared<Widget>(); // the pointed-to Widget's ref count (RC) is 1.

std::weak_ptr<Widget> wpw(spw); // wpw points to same Widget as spw. RC remains 1

spw = nullptr; // RC goes to 0, and the Widget is destroyed.
// wpw now dangles
if (wpw.expired()) … // if wpw doesn't point to an object…

理想的状态是既可以检查是否 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.

  1. std::weak_ptr::lock, returned std::shared_ptr is null if the std::weak_ptr has expired:
1
2
std::shared_ptr<Widget> spw1 = wpw.lock(); // if wpw's expired, spw1 is null
auto spw2 = wpw.lock(); // same as above, but uses auto
  1. std::shared_ptr constructor taking a std::weak_ptr as an argument. In this case, if the std::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 Widgets 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
std::unique_ptr<const Widget> loadWidget(WidgetID id);//an expensive call

std::shared_ptr<const Widget> fastLoadWidget(WidgetID id)
{
static std::unordered_map<WidgetID,
std::weak_ptr<const Widget>> //const 只读
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,
objPtr = loadWidget(id); // load it
cache[id] = objPtr; // cache it
}
return objPtr;
}

上面代码不完美的点在于 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

有什么办法增加一个 BA 的指针吗?

There are three choices:

  1. A raw pointer. if A is destroyed, but C continues to point to B, B will contain a pointer to A that will dangle.
  2. A std::shared_ptr. The resulting std::shared_ptr cycle (A points to B and B points to A) will prevent both A and B from being destroyed. Even if A and B are unreachable from other program data structures (e.g., because C no longer points to B), each will have a reference count of one. If that happens, A and B 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.
  3. A std::weak_ptr. This avoids both problems above. If A is destroyed, B’s pointer back to it will dangle, but B will be able to detect that. Furthermore, though A and B will point to one another, B’s pointer won’t affect A’s reference count, hence can’t keep A from being destroyed when std::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 for std::shared_ptr-like pointers that can dangle.
  • Potential use cases for std::weak_ptr include caching, observer lists, and the prevention of std::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
2
3
4
5
template<typename T, typename... Ts>
std::unique_ptr<T> make_unique(Ts&&... params)
{
return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
}

缺陷: 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:

  1. std::make_unique
  2. std::make_shared
  3. 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
2
3
4
auto upw1(std::make_unique<Widget>()); // with make func
std::unique_ptr<Widget> upw2(new Widget); // without make func
auto spw1(std::make_shared<Widget>()); // with make func
std::shared_ptr<Widget> spw2(new Widget); // without make func

exception safety

如下是一个处理 Widget 的函数, 其参数是 Widget 的指针, 以及一个计算 priority 的函数.

1
2
3
4
void processWidget(std::shared_ptr<Widget> spw, int priority);
int computePriority();
processWidget(std::shared_ptr<Widget>(new Widget), // potential resource leak!
computePriority());

对于系统而言会经历如下过程:

  • The expression new Widget must be evaluated, i.e., a Widget must be created on the heap.
  • The constructor for the std::shared_ptr<Widget> responsible for managing the pointer produced by new must be executed.
  • computePriority must run.

但是 C++ 中对一个函数参数的执行的顺序是不确定的(new Widgetstd::shared_ptr<Widget> 有依赖关系, 顺序是确定的), 可能的执行顺序如下:

  1. Perform new Widget.
  2. Execute computePriority.
  3. 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
2
processWidget(std::make_shared<Widget>(), // no potential resource leak
computePriority());

原因在于 std::make_shared 是一个“原子性”的参数, 上面的 step 1 与 3 合在一起执行了. 因此两个参数的执行顺序不再重要, 也不会有资源泄漏的风险.

efficiency

Using std::make_shared allows compilers to generate smaller, faster code that employs leaner data structures.

  1. 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.

  1. 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
2
3
4
// create std::initializer_list
auto initList = { 10, 20 };
// create std::vector using std::initializer_list ctor
auto spv = std::make_shared<std::vector<int>>(initList);

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 last std::shared_ptr and the last std::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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ReallyBigType { … };
auto pBigObj = // create very large object via std::make_shared
std::make_shared<ReallyBigType>();

// create std::shared_ptrs and std::weak_ptrs to
// large object, use them to work with it


// final std::shared_ptr to object destroyed here,
// but std::weak_ptrs to it remain

// during this period, memory formerly occupied
// by large object remains allocated

// final std::weak_ptr to object destroyed here;
// memory for control block and object is released

使用 new 的方式就没有这个情况了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ReallyBigType { … }; // as before
std::shared_ptr<ReallyBigType> pBigObj(new ReallyBigType);// create very large object via new

// as before, create std::shared_ptrs and
// std::weak_ptrs to object, use them with it

// final std::shared_ptr to object destroyed here,
// but std::weak_ptrs to it remain;
// memory for object is deallocated

// during this period, only memory for the
// control block remains allocated

// final std::weak_ptr to object destroyed here;
// memory for control block is released

如果无法使用 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
2
std::shared_ptr<Widget> spw(new Widget, cusDel);
processWidget(spw, computePriority()); // correct, but not optimal; see below

最优的做法是通过 std::move 实现这个 exception safe 的过程还能实现效率的优化:

1
2
3
4
5
6
7
8
9
processWidget(
std::shared_ptr<Widget>(new Widget, cusDel), // arg is rvalue
computePriority()
);

processWidget(spw, computePriority()); // arg is lvalue

processWidget(std::move(spw), // both efficient and exception safe
computePriority());

有一点注意: 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, for std::make_shared and std::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, and std::weak_ptrs that outlive the corresponding std::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
2
3
4
5
6
7
8
9
10
11
12
13
#include <string>
#include <vector>
#include "gadget.h"

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
};

gadget.h 或者 gadget.h 包含的其他头文件有变化会导致包含 Widget 头文件的 clients 都必须 recompile.

无智能指针版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// still in header "widget.h"
class Widget {
public:
Widget();
~Widget(); // dtor is needed—see below

private:
struct Impl; // declare implementation struct
Impl *pImpl; // and pointer to it
};

// in impl. file "widget.cpp"
#include "widget.h"
#include "gadget.h"
#include <string>
#include <vector>
struct Widget::Impl { // definition of Widget::Impl
std::string name; // with data members formerly in Widget
std::vector<double> data;
Gadget g1, g2, g3;
};

Widget::Widget() // allocate data members for this Widget object
: pImpl(new Impl)
{}

Widget::~Widget() // destroy data members for this object
{ delete pImpl; }

std::unique_ptr 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// in "widget.h"
class Widget {
public:
Widget();

private:
struct Impl;
std::unique_ptr<Impl> pImpl; // use smart pointer instead of raw pointer
};

// in "widget.cpp"
#include "widget.h"
#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 std::unique_ptr via std::make_unique
: pImpl(std::make_unique<Impl>())
{}
//Widget destructor is no longer present

但是上面的代码有问题, user 调用后

1
2
#include "widget.h"
Widget w; // error!

编译器报错: 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//in "widget.h"
class Widget { // as before,
public:
Widget();
~Widget(); // declaration only

private: // as before
struct Impl;
std::unique_ptr<Impl> pImpl;
};

//in "widget.cpp"
#include "widget.h" // as before
#include "gadget.h"
#include <string>
#include <vector>
struct Widget::Impl { // as before, definition of Widget::Impl
std::string name;
std::vector<double> data;
Gadget g1, g2, g3;
};

Widget::Widget() // as before
: pImpl(std::make_unique<Impl>())
{}

Widget::~Widget() // ~Widget definition
{}

第二种更简单的办法, 并且还能 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
2
3
4
5
6
7
8
9
10
11
12
// still in "widget.h"
class Widget {
public:
Widget();
~Widget();
Widget(Widget&& rhs) = default; // right idea, wrong code!
Widget& operator=(Widget&& rhs) = default;// right idea, wrong code!

private: // as before
struct Impl;
std::unique_ptr<Impl> pImpl;
};

有问题, 还是跟上面一样的问题, 但是比较如果思考一下的话, 可能会有疑问: 这可是 move 啊, 在移动构造的过程不涉及到析构的过程, 不应该出现与 copy 一样的错误.

  • move assignment

    The compiler-generated move assignment operator needs to destroy the object pointed to by pImpl before reassigning it, but in the Widget 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 destroying pImpl requires that Impl be complete.

解决办法还是把定义放到 .cpp 中.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 // still in "widget.h"
class Widget {
public:
Widget();
~Widget();
Widget(Widget&& rhs); // declarations only
Widget& operator=(Widget&& rhs);

private: // as before
struct Impl;
std::unique_ptr<Impl> pImpl;
};

// in "widget.cpp"
#include <string> // as before,

struct Widget::Impl { … }; // as before
Widget::Widget() // as before
: pImpl(std::make_unique<Impl>())
{}

Widget::~Widget() = default; // as before

Widget::Widget(Widget&& rhs) = default; // definitions
Widget& Widget::operator=(Widget&& rhs) = default;

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// still in "widget.h"
class Widget {
public:
// other funcs, as before
Widget(const Widget& rhs); // declarations only
Widget& operator=(const Widget& rhs); // declarations only
private: // as before
struct Impl;
std::unique_ptr<Impl> pImpl;
};

// in "widget.cpp"
#include "widget.h" // as before,


struct Widget::Impl { … }; // as before
Widget::~Widget() = default; // other funcs, as before
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;
}

通过智能指针的解引用深拷贝 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 // in "widget.h"
class Widget {
public:
Widget();
// no declarations for dtor
// or move operations
private:
struct Impl;
std::shared_ptr<Impl> pImpl; // std::shared_ptr
}; // instead of std::unique_ptr

// client code
#includes widget.h
Widget w1;
auto w2(std::move(w1)); // move-construct w2
w1 = std::move(w2); // move-assign w1

为什么 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 to std::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
2
3
4
5
6
7
8
template<typename T> // in namespace std
typename remove_reference<T>::type&&
move(T&& param)
{
using ReturnType = // alias declaration;
typename remove_reference<T>::type&&;
return static_cast<ReturnType>(param);
}

结论:

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
2
3
4
5
6
template<typename T> // C++14; still in namespace std
decltype(auto) move(T&& param)
{
using ReturnType = remove_reference_t<T>&&;
return static_cast<ReturnType>(param);
}

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
2
3
4
5
6
7
8
9
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 结果却是 copy. 下面是 std::string 的构造函数例子.

1
2
3
4
5
6
7
class string { // std::string is actually a  typedef for std::basic_string<char>
public:

string(const string& rhs); // copy ctor
string(string&& rhs); // move ctor

};

我们可以很清楚地看到 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 点启示:

  1. don’t declare objects const if you want to be able to move from them. Move requests on const objects are silently transformed into copy operations.

  2. 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 applying std::move to an object is that it’s an rvalue.

std::forward

std::forwardstd::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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Widget {
public:
Widget(Widget&& rhs)
: s(std::move(rhs.s))
{ ++moveCtorCalls; }

private:
static std::size_t moveCtorCalls;
std::string s;
};


class Widget {
public:
Widget(Widget&& rhs) // unconventional, undesirable implementation
: s(std::forward<std::string>(rhs.s))
{ ++moveCtorCalls; }

};

std::move 的优点:

  1. std::move requires only a function argument (rhs.s), while std::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 member s being copy constructed instead of move constructed).

  2. 语义信息不一样, 需要强制转成 rvalue 的时候, 还是需要 std::move. More importantly, the use of std::move conveys an unconditional cast to an rvalue, while the use of std::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 nor std::forward do anything at runtime.

Item 24: Distinguish universal references from rvalue references.

T&& 不见得一定是指 rvalue reference.

1
2
3
4
5
6
7
8
9
10
11
void f(Widget&& param); // rvalue reference

Widget&& var1 = Widget(); // rvalue reference

auto&& var2 = var1; // not rvalue reference

template<typename T>
void f(std::vector<T>&& param); // rvalue reference

template<typename T>
void f(T&& param); // not 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 to const or non-const objects, to volatile or non-volatile objects, even to objects that are both const and volatile. They can bind to virtually anything.

universal references 的使用场景, 只存在于 type deduction 环境中:

  • function template parameters

    1
    2
    template<typename T>
    void f(T&& param); // param is a universal reference
  • auto declarations

    1
    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
2
3
4
5
6
7
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)

注意 universal reference 存在的其他要点

  1. 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
2
3
4
5
template<typename T>
void f(std::vector<T>&& param); // param is an rvalue reference

std::vector<int> v;
f(v); // error! can't bind lvalue to rvalue reference

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. 使用左值调用编译器会报错.

  1. Even the simple presence of a const qualifier is enough to disqualify a reference from being universal:
1
2
template<typename T>
void f(const T&& param); // param is an rvalue reference
  1. being in a template doesn’t guarantee the presence of type deduction. 换句话说, 调用函数里的参数 T&& 必须直接依赖于 T 而不是间接地依赖于 T.

Consider push_back member function in std::vector:

1
2
3
4
5
6
template<class T, class Allocator = allocator<T>> // from C++ Standards
class vector {
public:
void push_back(T&& x);

};

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
2
3
4
5
6
7
std::vector<Widget> v;

class vector<Widget, allocator<Widget>> {
public:
void push_back(Widget&& x); // rvalue reference

};

conceptually similar emplace_back member function in std::vector does employ type deduction:

1
2
3
4
5
6
7
template<class T, class Allocator = allocator<T>> // still from C++ Standards
class vector {
public:
template <class... Args>
void emplace_back(Args&&... args);

};

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.

  1. variables declared with the type auto&& are universal references

C++14 lambda expressions may declare auto&& parameter. 下面是一个记录函数运行时间的函数.

1
2
3
4
5
6
7
8
9
10
11
auto timeFuncInvocation =
[](auto&& func, auto&&... params) // C++14
{
start timer;

std::forward<decltype(func)>(func)( // invoke func on params
std::forward<decltype(params)>(params)...
);

stop timer and record elapsed time;
};

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 type T, or if an object is declared using auto&&, 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Widget {
public:
template<typename T>
void setName(T&& newName) // universal reference
{ name = std::move(newName); } // compiles, but is bad, bad, bad!

private:
std::string name;
std::shared_ptr<SomeDataStructure> p;
};

std::string getWidgetName(); // factory function
Widget w;
auto n = getWidgetName(); // n is local variable
w.setName(n); // moves n into w!

// n's value now unknown, moved permanently

上面的结果是强制把 n 变成 rvalue, 然后通过 T&& 推导为 rvalue reference, 导致 n 的值被 w 偷走了(留下的是 unspecified value). 如果下面接着使用 n 的话, 会导致 UB.

解决此方法, overloaded for const lvalues and for rvalues:

1
2
3
4
5
6
7
8
class Widget {
public:
void setName(const std::string& newName) // set from const lvalue
{ name = newName; }
void setName(std::string&& newName) // set from rvalue
{ name = std::move(newName); }

};

重载有缺陷:

  1. more source code to write and maintain
  2. 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 to setName, where it would be conveyed to the assignment operator for the std::string inside w. w’s name data member would thus be assigned directly from the string literal(即 std::string assignment operator taking a const char* pointer); no temporary std::string objects would arise.

  • With the overloaded versions of setName, however, a temporary std::string object would be created for setName’s parameter to bind to, and this temporary std::string would then be moved into w’s data member. A call to setName would thus entail execution of one std::string constructor (to create the temporary), one std::string move assignment operator (to move newName into w.name), and one std::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 是类似的思想.

  1. 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.

一些应用场景

  1. 一个函数中多次使用 rvalue reference 或者 universal reference 保证最后再 apply std::move (for rvalue references) or std::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
2
3
4
5
6
7
8
9
template<typename T> // text is univ. reference
void setSignText(T&& text)
{
sign.setText(text); // use text, but don't modify it
auto now = // get current time
std::chrono::system_clock::now();
signHistory.add(now,
std::forward<T>(text)); // conditionally cast text to rvalue
}
  1. 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.

  1. 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 返回 rvalue reference
Matrix // by-value return
operator+(Matrix&& lhs, const Matrix& rhs)
{
lhs += rhs;
return std::move(lhs); // move lhs into return value
//lhs will be moved into the function’s return value location
//more efficient
}

//直接返回 value
Matrix // as above
operator+(Matrix&& lhs, const Matrix& rhs)
{
lhs += rhs;
return lhs; // copy lhs into return value
}

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
2
3
4
5
6
7
template<typename T>
Fraction // by-value return universal reference param
reduceAndCopy(T&& frac)
{
frac.reduce();
return std::forward<T>(frac); // move rvalue into return value, copy lvalue
}

return value optimization (RVO)

不要在 RVO 下画蛇添足

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//before "optimization" 
Widget makeWidget() // "Copying" version of makeWidget
{
Widget w; // local variable
// configure w
return w; // "copy" w into return value
}

//after "optimization"
Widget makeWidget() // Moving version of makeWidget
{
Widget w;

return std::move(w); // move w into return value (don't do this!)
}

有些聪明人想到, 是否可以通过把函数里的 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

  1. the type of the local object is the same as that returned by the function and
  2. 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
2
3
4
5
6
Widget makeWidget() // as before
{
Widget w;

return w;
}

在不执行 copy elision 优化的编译器必须转换成如下形式:

1
2
3
4
5
6
Widget makeWidget()
{
Widget w;

return std::move(w); // treat w as rvalue, because no copy elision was performed
}

对于使用 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
2
3
4
5
6
7
8
9
10
11
12
Widget makeWidget(Widget w) // by-value parameter of same type as function's return
{

return w;
}

//copy elision 优化的效果:
Widget makeWidget(Widget w)
{

return std::move(w); // treat w as rvalue
}

Things to Remember

  • Apply std::move to rvalue references and std::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 or std::forward to local objects if they would otherwise be eligible for the return value optimization.

Item 26: Avoid overloading on universal references.

一个不考虑使用移动优化的案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
std::multiset<std::string> names; // global data structure
void logAndAdd(const std::string& name)
{
auto now = // get current time
std::chrono::system_clock::now();
log(now, "logAndAdd"); // make log entry
names.emplace(name); // add name to global data structure;
}

//调用例子
std::string petName("Darla");
logAndAdd(petName); // pass lvalue std::string

logAndAdd(std::string("Persephone")); // pass rvalue std::string

logAndAdd("Patty Dog"); // pass string literal

第一个 petName 是左值, 因此不存在优化空间.

第二个是临时的 std::string 对象, 然后通过 std::multisetemplace 函数将临时对象 copy 到容器内, 因此存在优化空间.

第三个是 string literal, 如果我们能够直接在 std::multiset 里利用 string literal 构造出一个 std::string 元素, no need to create a temporary std::string at all, 并且连 move 都省下来了.

改造后:

1
2
3
4
5
6
7
8
9
10
11
template<typename T>
void logAndAdd(T&& name)
{
auto now = std::chrono::system_clock::now();
log(now, "logAndAdd");
names.emplace(std::forward<T>(name));
}
std::string petName("Darla"); // as before
logAndAdd(petName); // as before, copy lvalue into multiset
logAndAdd(std::string("Persephone")); // move rvalue instead of copying it
logAndAdd("Patty Dog"); // create std::string in multiset instead of copying a temporary std::string

但这样做会有风险.

增加函数参数, 重载 universal references 可行吗?

如果有新的需求: 增加 look-up table, 使用 index 添加 std::multiset 的方式, 直接添加 overload 函数.

1
2
3
4
5
6
7
std::string nameFromIdx(int idx); // return name corresponding to idx
void logAndAdd(int idx) // new overload
{
auto now = std::chrono::system_clock::now();
log(now, "logAndAdd");
names.emplace(nameFromIdx(idx));
}

但是出错了:

1
2
3
short nameIdx;
// give nameIdx a value
logAndAdd(nameIdx); // error!

原因是:

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
2
3
4
5
6
7
8
9
10
11
class Person {
public:
template<typename T>
explicit Person(T&& n) // perfect forwarding ctor;
: name(std::forward<T>(n)) {} // initializes data member
explicit Person(int idx) // int ctor
: name(nameFromIdx(idx)) {}

private:
std::string name;
};

由于 template function 无法阻止编译器自动生成构造函数, 编译器眼里的函数声明是下面这样的:

1
2
3
4
5
6
7
8
9
10
11
12
class Person {
public:
template<typename T> // perfect forwarding ctor
explicit Person(T&& n)
: name(std::forward<T>(n)) {}
explicit Person(int idx); // int ctor

Person(const Person& rhs); // copy ctor (compiler-generated)

Person(Person&& rhs); // move ctor (compiler-generated)

};

这样做也会有问题:

1
2
Person p("Nancy");
auto cloneOfP(p); // create new Person from p; this won't compile!

直观上来看, 既然有自动生成的 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
2
3
4
5
6
7
8
9
class Person {
public:
explicit Person(Person& n) // instantiated from
: name(std::forward<Person&>(n)) {} // perfect-forwarding template

explicit Person(int idx); // as before
Person(const Person& rhs); // copy ctor (compiler-generated)

};

相比于 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
2
3
4
5
6
7
8
9
const Person cp("Nancy"); // object is now const
auto cloneOfP(cp); // calls copy constructor!

class Person {
public:
explicit Person(const Person& n); // instantiated from template
Person(const Person& rhs); // copy ctor (compiler-generated)

};

还要考虑重载对类继承的影响

1
2
3
4
5
6
7
8
9
10
class SpecialPerson: public Person {
public:
SpecialPerson(const SpecialPerson& rhs) // copy ctor; calls base class
: Person(rhs)
{ … } // forwarding ctor!

SpecialPerson(SpecialPerson&& rhs) // move ctor; calls base class
: Person(std::move(rhs))
{ … } // forwarding ctor!
};

这样做编译不通过.

  1. 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!
  2. the derived class functions are using arguments of type SpecialPerson to pass to their base class. There’s no std::string constructor taking a SpecialPerson.

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
2
3
4
5
6
7
8
9
10
class Person {
public:
explicit Person(std::string n) // replaces T&& ctor
: name(std::move(n)) {}
explicit Person(int idx) // as before
: name(nameFromIdx(idx)) {}

private:
std::string name;
};

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
2
3
4
5
6
7
8
std::multiset<std::string> names; // global data structure
template<typename T> // make log entry and add
void logAndAdd(T&& name) // name to data structure
{
auto now = std::chrono::system_clock::now();
log(now, "logAndAdd");
names.emplace(std::forward<T>(name));
}

logAndAdd itself will accept all argument types, both integral and non-integral. The two functions doing the real work will be named logAndAddImpl.

1
2
3
4
5
6
7
template<typename T>
void logAndAdd(T&& name)
{
logAndAddImpl(
std::forward<T>(name),
std::is_integral<T>()); // not quite correct
}

上面代码的问题是: 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
2
3
4
5
6
7
8
template<typename T>
void logAndAdd(T&& name)
{
logAndAddImpl(
std::forward<T>(name),
std::is_integral<typename std::remove_reference<T>::type>()
);
}

完成重载的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<typename T> // non-integral argument:
void logAndAddImpl(T&& name, std::false_type)
{ // add it to global data structure
auto now = std::chrono::system_clock::now();
log(now, "logAndAdd");
names.emplace(std::forward<T>(name));
}

// integral argument: look up name and call logAndAdd with it
std::string nameFromIdx(int idx); // as in Item 26
void logAndAddImpl(int idx, std::true_type)
{
logAndAdd(nameFromIdx(idx));
}

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
2
3
4
5
6
7
class Person {
public:
template<typename T,
typename = typename std::enable_if<condition>::type>
explicit Person(T&& n);

};

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&, and Person&& are all the same as Person.
  • Whether it’s const or volatile. As far as we’re concerned, a const Person and a volatile Person and a const volatile Person are all the same as a Person.

这就要使用 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
2
3
4
5
6
7
8
9
10
11
12
13
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);

};

处理继承关系的 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
2
3
4
5
6
7
8
9
10
11
12
13
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 可以再简单一些:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person { // C++14
public:
template<
typename T,
typename = std::enable_if_t< // less code here
!std::is_base_of<Person,
std::decay_t<T> // and here
>::value
> // and here
>
explicit Person(T&& n);

};

在上面的基础上判断类型进行重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 args convertible to std::strings
: name(std::forward<T>(n))
{ … }

explicit Person(int idx) // ctor for integral args
: name(nameFromIdx(idx))
{ … }
// copy and move ctors, etc.
private:
std::string name;
};

Trade-offs

perfect forwarding has drawbacks.

  1. 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.

  2. 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
    22
    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)
    };

    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 a std::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
2
template<typename T>
void func(T&& param);

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
2
3
int x;

auto& & rx = x; // error! can't declare reference to reference
1
2
3
4
template<typename T>
void func(T&& param); // as before
func(w); // invoke func with lvalue;T deduced as Widget&
void func(Widget& && param); //A reference to a reference!

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
2
3
4
5
6
template<typename T> // in namespace std
T&& forward(typename
remove_reference<T>::type& param)
{
return static_cast<T&&>(param);
}

Reference collapsing is also applied to the return type and the cast.

C++14 更简洁一些

1
2
3
4
5
template<typename T> // C++14; still in namespace std
T&& forward(remove_reference_t<T>& param)
{
return static_cast<T&&>(param);
}

Reference collapsing occurs in four contexts.

  1. most common is template instantiation.

  2. type generation for auto variables.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    template<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.
  3. the generation and use of typedefs and alias declarations.

  4. 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 type T&, while rvalues of type T yield T as their deduced type.
  • Reference collapsing occurs.

Things to Remember

  • Reference collapsing occurs in four contexts: template instantiation, auto type generation, creation and use of typedefs and alias declarations, and decltype.
  • 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
2
3
4
5
template<typename... Ts>
void fwd(Ts&&... params) // accept any arguments
{
f(std::forward<Ts>(params)...); // forward them to f
}

perfect forwarding failure 定义

1
2
3
f( expression ); // if this does one thing,
fwd( expression ); // but this does something else, fwd fails
// to perfectly forward expression to f

Braced initializers

现象描述:

1
2
3
void f(const std::vector<int>& v);
f({ 1, 2, 3 }); // fine, "{1, 2, 3}" implicitly converted to std::vector<int>
fwd({ 1, 2, 3 }); // error! doesn't compile

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 if f were an overloaded function name, and, due to “incorrect” type deduction, the overload of f called inside fwd were different from the overload that would be invoked if f 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
2
auto il = { 1, 2, 3 }; // il's type deduced to be std::initializer_list<int>
fwd(il); // fine, perfect-forwards il to f

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
2
3
4
5
6
7
8
9
class Widget {
public:
static const std::size_t MinVals = 28; // MinVals' declaration

};

// no defn. for MinVals
std::vector<int> widgetData;
widgetData.reserve(Widget::MinVals); // use of MinVals

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
2
3
void f(std::size_t val);
f(Widget::MinVals); // fine, treated as "f(28)"
fwd(Widget::MinVals); // error! shouldn't link

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
2
3
4
5
6
7
8
9
10
11
12
void f(int (*pf)(int)); // pf = "processing function"
void f(int pf(int)); // declares same f as above

//overloaded function
int processVal(int value);
int processVal(int value, int priority);

//pass processVal to f
f(processVal); // fine

//pass processVal to fwd
fwd(processVal); // error! which processVal?

区别的原因:

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
2
3
4
5
template<typename T>
T workOnVal(T param) // template for processing values
{ … }

fwd(workOnVal); // error! which workOnVal instantiation?

解决办法: 显式指定类型

1
2
3
4
5
6
7
using ProcessFuncType = int (*)(int);// make typedef;

ProcessFuncType processValPtr = processVal; // specify needed signature for processVal

fwd(processValPtr); // fine

fwd(static_cast<ProcessFuncType>(workOnVal)); // also fine

因此这造成了对重载函数使用 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
2
3
4
5
6
7
8
9
10
11
12
13
14
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); // fine
fwd(h.totalLength); // error!

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
2
3
// 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

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 integral const static data members, template and overloaded function names, and bitfields.
作者

cx

发布于

2022-07-25

更新于

2022-11-23

许可协议