Merge branch 'master' into fix-conflict

This commit is contained in:
LucienXian 2020-07-08 00:41:02 +08:00 committed by GitHub
commit 979c3596df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 428 additions and 10 deletions

View File

@ -82,6 +82,7 @@ dereUPLess = [](const std::unique_ptr<Widget> &p1,
const std::unique_ptr<Widget> &p2){return *p1<*p2;};
````
语法冗长不说,还需要重复写很多形参类型,使用`std::function`还不如使用auto。用auto声明的变量保存一个闭包这个变量将会得到和闭包一样的类型。
实例化`std::function`并声明一个对象这个对象将会有固定的大小。当使用这个对象保存一个闭包时它可能大小不足不能存储,这个时候`std::function`的构造函数将会在堆上面分配内存来存储,这就造成了使用`std::function`比auto会消耗更多的内存。并且通过具体实现我们得知通过std::function调用一个闭包几乎无疑比auto声明的对象调用要慢。
换句话说std::function方法比auto方法要更耗空间且更慢并且比起写一大堆类型使用auto要方便得多。在这场存储闭包的比赛中auto无疑取得了胜利也可以使用std::bind来生成一个闭包但在Item34我会尽我最大努力说服你使用lambda表达式代替std::bind)
@ -109,7 +110,7 @@ for(const std::pair<std::string,int>& p : m)
````
看起来好像很合情合理的表达,但是这里有一个问题,你看到了吗?
要想看到错误你就得知道std::unordered_map的key是一个常量所以std::pair的类型不是std::pair<std::string,int>而是std::pair<const std::string,int>。编译器会努力的找到一种方法把前者转换为后者。它会成功的因为它会创建一个临时对象这个临时对象的类型是p想绑定到的对象的类型即m中元素的类型然后把p的引用绑定到这个临时对象上。在每个循环迭代结束时临时对象将会销毁如果你写了这样的一个循环你可能会对它的一些行为感到非常惊讶因为你确信你只是让成为p指向m中各个元素的引用而已。
要想看到错误你就得知道`std::unordered_map`的key是一个常量所以`std::pair`的类型不是`std::pair<std::string,int>`而是`std::pair<const std::string,int>`。编译器会努力的找到一种方法把前者转换为后者。它会成功的因为它会创建一个临时对象这个临时对象的类型是p想绑定到的对象的类型即m中元素的类型然后把p的引用绑定到这个临时对象上。在每个循环迭代结束时临时对象将会销毁如果你写了这样的一个循环你可能会对它的一些行为感到非常惊讶因为你确信你只是让成为p指向m中各个元素的引用而已。
使用auto可以避免这些很难被意识到的类型不匹配的错误
````cpp
@ -123,8 +124,11 @@ for(const auto & p : m)
后面这两个例子说明了显式的指定类型可能会导致你不像看到的类型转换。如果你使用auto声明目标变量你就不必担心这个问题。
基于这些原因我建议你优先考虑auto而非显式类型声明。然而auto也不是完美的。每个auto变量都从初始化表达式中推导类型有一些表达式的类型和我们期望的大相径庭。关于在哪些情况下会发生这些问题以及你可以怎么解决这些问题我们在Item2和6讨论所以这里我不再赘述。我想把注意力放到你可能关心的另一点使用auto代替传统类型声明对源码可读性的影响。
首先深呼吸放松auto是**可选项**,不是**命令**在某些情况下如果你的专业判断告诉你使用显式类型声明比auto要更清晰更易维护那你就不必再坚持使用auto。牢记C++没有在其他众所周知的语言所拥有的类型接口上开辟新土地。
其他静态类型的过程式语言如C#,D,Sacla,Visual Basic等)或多或少的都有那些非静态类型的函数式语言如ML,Haskell,OCaml.F#等的特性。在某种程度上几乎没有显式类型使得动态类型语言Perl,Python,Ruby等取得了成功软件开发社区对于类型接口有丰富的经验他们展示了在维护大型工业强度的代码上使用这种技术没有任何争议。
一些开发者也担心使用auto就不能瞥一眼源代码便知道对象的类型然而IDE扛起了部分担子在很多情况下少量显示一个对象的类型对于知道对象的确切类型是有帮助的这通常已经足够了。举个例子要想知道一个对象是容器还是计数器还是智能指针不需要知道它的确切类型一个适当的变量名称就能告诉我们大量的抽象类型信息。
真正的问题是显式指定类型可以避免一些微妙的错误以及更具效率和正确性而且如果初始化表达式改变变量的类型也会改变这意味着使用auto可以帮助我们完成一些重构工作。举个例子如果一个函数返回类型被声明为int但是后来你认为将它声明为long会更好调用它作为初始化表达式的变量会自动改变类型但是如果你不使用auto你就不得不在源代码中挨个找到调用地点然后修改它们。

View File

@ -0,0 +1,54 @@
# Item29: Assume that move operations are not present, not cheap, and not used
移动语义可以说是C++11最主要的特性。你可能会见过这些类似的描述“移动容器和拷贝指针一样开销小” “拷贝临时对象现在如此高效编码避免这种情况简直就是过早优化”这种情绪很容易理解。移动语义确实是这样重要的特性。它不仅允许编译器使用开销小的移动操作代替大开销的复制操作而且默认这么做。以C++98的代码为基础使用C++11重新编译你的代码然后你的软件运行的更快了。
移动语义确实令人振奋但是有很多夸大的说法这个Item的目的就是给你泼一瓢冷水保持理智看待移动语义。
让我们从已知很多类型不支持移动操作开始这个过程。为了升级到C++11C++98的很多标准库做了大修改为很多类型提供了移动的能力这些类型的移动实现比复制操作更快并且对库的组件实现修改以利用移动操作。但是很有可能你工作中的代码没有完整地利用C++11。对于你的应用中或者代码库中没有适配C++11的部分编译器即使支持移动语义也是无能为力的。的确C++11倾向于为缺少移动操作定义的类默认生成但是只有在没有声明复制操作移动操作或析构函数的类中才会生成移动操作参考Item17。禁止移动操作的类中通过delete move operation 参考Item11编译器不生成移动操作的支持。对于没有明确支持移动操作的类型并且不符合编译器默认生成的条件的类没有理由期望C++11会比C++98进行任何性能上的提升。
即使显式支持了移动操作结果可能也没有你希望的那么好。比如所有C++11的标准库都支持了移动操作但是任务移动所有容器的开销都非常小是个错误。对于某些容器来说压根就不存在开销小的方式来移动它所包含的内容。对另一些容器来说开销真正小的移动操作却使得容器元素移动含义事与愿违。
考虑一下`std::array`这是C++11中的新容器。`std::array`本质上是具有STL接口的内置数组。这与其他标准容器将内容存储在堆内存不同。存储具体数据在堆内存的容器本身只保存了只想堆内存数据的指针真正实现当然更复杂一些但是基本逻辑就是这样。这种实现使得在常数时间移动整个容器成为可能的只需要拷贝容器中保存的指针到目标容器然后将原容器的指针置为空指针就可以了。
```cpp
std::vector<Widget> vm1;
auto vm2 = std::move(vm1); // move vm1 into vm2. Runs in constant time. Only ptrs in vm1 and vm2 are modified
```
`std::array`没有这种指针实现,数据就保存在`std::array`容器中
```cpp
std::array<Widget, 10000> aw1;
auto aw2 = std::move(aw1); // move aw1 into aw2. Runs in linear time. All elements in aw1 are moved into aw2.
```
注意`aw1`中的元素被移动到了`aw2`中,这里假定`Widget`类的移动操作比复制操作快。但是使用`std::array`的移动操作还是复制操作都将话费线性时间的开销,因为每个容器中的元素终归需要拷贝一次,这与“移动一个容器就像操作几个指针一样方便”的含义想去甚远。
另一方面,`std::strnig`提供了常数时间的移动操作和线性时间的复制操作。这听起来移动比复制快多了,但是可能不一定。许多字符串的实现采用了*small string optimization(SSO)*。"small"字符串比如长度小于15个字符的存储在了`std::string`的缓冲区中,并没有存储在堆内存,移动这种存储的字符串并不必复制操作更快。
SSO的动机是大量证据表明短字符串是大量应用使用的习惯。使用内存缓冲区存储而不分配堆内存空间是为了更好的效率。然而这种内存管理的效率导致移动的效率并不必复制操作高。
即使对于支持快速移动操作的类型某些看似可靠的移动操作最终也会导致复制。Item14解释了原因标准库中的某些容器操作提供了强大的异常安全保证确保C++98的代码直接升级C++11编译器不会不可运行仅仅确保移动操作不会抛出异常才会替换为移动操作。结果就是即使类提供了更具效率的移动操作编译器仍可能被迫使用复制操作来避免移动操作导致的异常。
因此存在几种情况C++11的移动语义并无优势
- **No move operations**:类没有提供移动操作,所以移动的写法也会变成复制操作
- **Move not faster**:类提供的移动操作并不必复制效率更高
- **Move not usable**:进行移动的上下文要求移动操作不会抛出异常,但是该操作没有被声明为`noexcept`
值得一提的是,还有另一个场景,会使得移动并没有那么有效率:
- **Source object is lvalue**:除了极少数的情况外(例如 Item25只有右值可以作为移动操作的来源
但是该Item的标题是假定不存在移动操作或者开销不小不使用移动操作。存在典型的场景就是编写模板代码因为你不清楚你处理的具体类型是什么。在这种情况下你必须像出现移动语义之前那样保守地考虑复制操作。不稳定的代码也是如此类的特性经常被修改导致可能移动操作会有问题。
但是通常你了解你代码里使用的类并且知道是否支持快速移动操作。这种情况你无需这个Item的假设只需要查找所用类的移动操作详细信息并且调用移动操作的上下文中可以安全的使用快速移动操作替换复制操作。
## 需要记住的事
- Assume that move operations are not present, not cheap, and not used.
- 完全了解的代码可以忽略本Item

View File

@ -44,7 +44,7 @@ move(T&& param)
该函数返回类型的`&&`部分表明`std::move`函数返回的是一个右值引用但是正如Item 28所解释的那样如果类型`T`恰好是一个左值引用,那么`T&&`将会成为一个左值引用。为了避免如此类型萃取器type trait见Item 9)`std::remove_reference`应用到了类型`T`上,因此确保了`&&`被正确的应用到了一个不是引用的类型上。这保证了`std::move`返回的真的是右值引用这很重要因为函数返回的右值引用是右值rvalues)。因此,`std::move`将它的参数转换为一个右值,这就是它的全部作用。
此外,`std;:move`在C++14中可以被更简单地实现。多亏了函数返回值类型推导见Item 3)和标准库的模板别名`std::remove_reference_t`见Item 9)`std::move`可以这样写:
此外,`std::move`在C++14中可以被更简单地实现。多亏了函数返回值类型推导见Item 3)和标准库的模板别名`std::remove_reference_t`见Item 9)`std::move`可以这样写:
```cpp
template <typename T>

View File

@ -0,0 +1,67 @@
# 优先基于任务编程而不是基于线程
如果开发者想要异步执行 `doAsyncWork` 函数,通常有两种方式。其一是通过创建 `std::thread` 执行 `doAsyncWork` 比如
```cpp
int doAsyncWork();
std::thread t(doAsyncWork);
```
其二是将 `doAsyncWork` 传递给 `std::async` 一种基于任务的策略:
```cpp
auto fut = std::async(doAsyncWork); // "fut" for "future"
```
这种方式中,函数对象作为一个任务传递给 `std::async`
基于任务的方法通常比基于线程的方法更优,原因之一上面的代码已经表明,基于任务的方法代码量更少。我们假设唤醒`doAsyncWork`的代码对于其提供的返回值是有需求的。基于线程的方法对此无能为力,而基于任务的方法可以简单地获取`std::async`返回的`future`提供的`get`函数获取这个返回值。如果`doAsycnWork`发生了异常,`get`函数就显得更为重要,因为`get`函数可以提供抛出异常的访问,而基于线程的方法,如果`doAsyncWork`抛出了异常,线程会直接终止(通过调用`std::terminate`)。
基于线程与基于任务最根本的区别在于抽象层次的高低。基于任务的方式使得开发者从线程管理的细节中解放出来对此在C++并发软件中总结了'thread'的三种含义:
- 硬件线程Hardware threads是真实执行计算的线程。现代计算机体系结构为每个CPU核心提供一个或者多个硬件线程。
- 软件线程Software threads也被称为系统线程是操作系统管理的在硬件线程上执行的线程。通常可以存在比硬件线程更多数量的软件线程因为当软件线程被比如 I/O、同步锁或者条件变量阻塞的时候操作系统可以调度其他未阻塞的软件线程执行提供吞吐量。
- `std::threads`是C++执行过程的对象并作为软件线程的handle(句柄)。`std::threads`存在多种状态1. `null`表示空句柄,因为处于默认构造状态(即没有函数来执行),因此不对应任何软件线程。 2. moved from (moved-to的`std::thread`就对应软件进程开始执行) 3. `joined`(连接唤醒与被唤醒的两个线程) 4. `detached`(将两个连接的线程分离)
软件线程是有限的资源。如果开发者试图创建大于系统支持的硬件线程数量,会抛出`std::system_error`异常。即使你编写了不抛出异常的代码,这仍然会发生,比如下面的代码,即使 `doAsyncWork``noexcept`
```cpp
int doAsyncWork() noexcept; // see Item 14 for noexcept
```
这段代码仍然会抛出异常。
```cpp
std::thread t(doAsyncWork); // throw if no more
// threads are available
```
设计良好的软件必须有效地处理这种可能性(软件线程资源耗尽),一种有效的方法是在当前线程执行`doAsyncWork`但是这可能会导致负载不均而且如果当前线程是GUI线程可能会导致响应时间过长的问题另一种方法是等待当前运行的线程结束之后再创建新的线程但是仍然有可能当前运行的线程在等待`doAsyncWork`的结果(例如操作得到的变量或者条件变量的通知)。
即使没有超出软件线程的限额仍然可能会遇到资源超额的麻烦。如果当前准备运行的软件线程大于硬件线程的数量系统的线程调度程序会将硬件核心的时间切片当一个软件线程的时间片执行结束会让给另一个软件线程即发生上下文切换。软件线程的上下文切换会增加系统的软件线程管理开销并且如果发生了硬件核心漂移这个开销会更高具体来说如果发生了硬件核心漂移1CPU cache中关于上次执行线程的数据很少需要重新加载指令2新线程的cache数据会覆盖老线程的数据如果将来会再次覆盖老线程的数据显然频繁覆盖增加很多切换开销。
避免资源超额是困难的因为软件线程之于硬件线程的最佳比例取决于软件线程的执行频率比如一个程序从IO密集型变成计算密集型执行频率是会改变的而且比例还依赖上下文切换的开销以及软件线程对于CPU cache的使用效率。此外硬件线程的数量和CPU cache的速度取决于机器的体系结构即使经过调校软件比例在某一种机器平台取得较好效果换一个其他类型的机器这个调校并不能提供较好效果的保证。
而使用`std::async`可以将调校最优比例这件事隐藏于标准库中,在应用层面不需过多考虑
```cpp
auto fut = std::async(doAsyncWork); // onus of thread mgmt is
// on implement of
// the Standard Library
```
这种调用方式将线程管理的职责转交给C++标准库的开发者。举个例子,这种调用方式会减少抛出资源超额的异常,为何这么说调用`std::async`并不保证开启一个新的线程,只是提供了执行函数的保证,具体是否创建新的线程来运行此函数,取决于具体实现,比如可以通过调度程序来将`AsyncWork`运行在等待此函数结果的线程上,调度程序的合理性决定了系统是否会抛出资源超额的异常,但是这是库开发者需要考虑的事情了。
如果考虑自己实现在等待结果的线程上运行输出结果的函数,之前提到了可能引出负载不均衡的问题,`std::async`运行时的调度程序显然比开发者更清楚调度策略的制定,因为运行时调度程序管理的是所有执行过程,而不仅仅个别开发者运行的代码。
如果在GUI程序中使用`std::async`会引起响应变慢的问题,还可以通过`std::launch::async`向`std::async`传递调度策略来保证运行函数在不同的线程上执行。
最前沿的线程调度算法使用线程池来避免资源超额的问题并且通过窃取算法来提升了跨硬件核心的负载均衡。C++标准实际上并不要求使用线程池或者`work-stealing`算法,而且这些技术的实现难度可能比你想象中更有挑战。不过,库开发者在标准库实现中采用了这些前沿的技术,这使得采用基于任务的方式编程的开发者在这些技术发展中持续获得回报,相反如果开发者直接使用`std::thread`编程,处理资源耗竭,负责均衡问题的责任就压在了应用开发者身上,更不说如何使得开发方案跨平台使用。
对比基于线程的开发方式,基于任务的设计为开发者避免了线程管理的痛苦,并且自然提供了一种获取异步执行的结果的方式。当然,仍然存在一些场景直接使用`std::thread`会更有优势:
- **需要访问非常基础的线程API**。C++并发API通常是通过操作系统提供的系统级API(pthreads 或者 windows threads)来实现的系统级API通常会提供更加灵活的操作方式举个例子C++并发API没有线程优先级和affinities的概念。为了提供对底层系统级线程API的访问`std::thread`对象提供了`native_handle`的成员函数,而在高层抽象的比如`std::futures`没有这种能力。
- **需要优化应用的线程使用**。举个例子只在特定系统平台运行的软件可以调教地比使用C++并行API更好的程序性能。
- **需要实现C++并发API之外的线程技术**。举例来说,自行实现线程池技术。
这些都是在应用开发中并不常见的例子,大多数情况,开发者应该优先采用基于任务的编程方式。
## 记住
- `std::thread`API不能直接访问异步执行的结果如果执行函数有异常抛出代码会终止执行
- 基于线程的编程方式关于解决资源超限,负载均衡的方案移植性不佳
- 基于任务的编程方式`std::async`会默认解决上面两条问题

292
8.Tweaks/Item41.md Normal file
View File

@ -0,0 +1,292 @@
# CHAPTER8 Tweaks
-------
对于C++中的通用技术总是存在适用场景。除了本章覆盖的两个例外描述什么场景使用哪种通用技术通常来说很容易。这两个例外是传值pass by value和 emplacement。决定何时使用这两种技术受到多种因素的影响本书提供的最佳建议是在使用它们的同时仔细考虑清除尽管它们都是高效的现代C++编程的重要角色。接下来的Items提供了是否使用它们来编写软件的所需信息。
## Item41.Consider pass by value for copyable parameters that are cheap to move and always copied 总是考虑直接按值传递,如果参数可拷贝并且移动操作开销很低
有些函数的参数是可复制的。比如说,`addName`成员函数可以拷贝自己的参数到一个私有容器。为了提高效率,应该拷贝左值,移动右值。
```cpp
class Widget {
public:
void addName(const std::string& newName) {
names.push_back(newName);
}
void addName(std::string&& newName) {
names.push_back(std::move(newName));
}
...
private:
std::vector<std::string> names;
};
```
这是可行的,但是需要编写两个同名异参函数,这有点让人难受:两个函数声明,两个函数实现,两个函数文档,两个函数的维护。唉。
此外你可能会担心程序的目标代码的空间占用当函数都内联inlined的时候会避免同时两个函数同时存在导致的代码膨胀问题但是一旦存在没有被内联inlined目标代码就是出现两个函数。
另一种方法是使`addName`函数成为具有通用引用的函数模板:参考Item24
```cpp
class Widget {
public:
template<typename T>
void addName(T&& newName) {
names.push_back(std::forward<T>(newName));
}
...
};
```
这减少了源代码的维护工作,但是通用引用会导致其他复杂性。作为模板,`addName`的实现必须放置在头文件中。在编译器展开的时候,可能会不止为左值和右值实例化为多个函数,也可能为`std::string`和可转换为`std::string`的类型分别实例化为多个函数参考Item25。同时有些参数类型不能通过通用引用传递参考Item30而且如果传递了不合法的参数类型编译器错误会令人生畏。参考Item27
是否存在一种编写`addName`的方法左值拷贝右值移动而且源代码和目标代码中都只有一个函数避免使用通用模板这种特性答案是是的。你要做的就是放弃你学习C++编程的第一条规则,就是用户定义的对象避免传值。像是`addName`函数中的`newName`参数,按值传递可能是一种完全合理的策略。
在我们讨论为什么对于`addName`中的`newName`参数按值传递非常合理之前,让我们来考虑如下实现:
```cpp
class Widget {
public:
void addName(std::string newName) {
names.push_back(std::move(newName));
}
...
}
```
该代码唯一可能令人困惑的部分就是`std::move`这里。`std::move`典型的应用场景是用在右值引用但是在这里我们了解到的信息1`newName`是完全复制的传递进来的对象换句话说改变不会影响原值2`newName`的最终用途就在这个函数里,不会再做他用,所以移动它不会影响其他代码。
事实就是我们只编写了一个`addName`函数避免了源代码和目标代码的重复。我们没有使用通用引用的特性不会导致头文件膨胀odd failure cases(这里不知道咋翻译),或者令人困惑的错误问题(编译)。但是这种设计的效率如何呢?按值传值会不会开销很大?
在C++98中可以肯定的是无论调用者如何调用参数`newName`都是拷贝传递。但是在C++11中`addName`就是左值拷贝,右值移动,来看如下例子:
```cpp
Widget w;
...
std::string name("Bart");
w.addName(name); // call addName with lvalue
...
w.addName(name + "Jenne"); // call addName with rvalue
```
第一处调用,`addName`的参数是左值因此是拷贝构造参数就像在C++98中一样。第二处调用参数是一个临时值是一个右值因此`newName`的参数是移动构造的。
就像我们想要的那样,左值拷贝,右值移动,优雅吧?
优雅,但是要牢记一些警示,回顾一下我们考虑过的三个版本的`addName`:
```cpp
class Widget { // Approach 1
public:
void addName(const std::string& newName) {
names.push_back(newName);
}
void addName(std::string&& newName) {
names.push_back(std::move(newName));
}
...
private:
std::vector<std::string> names;
};
class Widget { // Approach 2
public:
template<typename T>
void addName(T&& newName) {
names.push_back(std::forward<T>(newName));
}
...
};
class Widget { // Approach 3
public:
void addName(std::string newName) {
names.push_back(std::move(newName));
}
...
};
```
本书将前两个版本称为“按引用方法”,因为都是通过引用传递参数,仍然考虑这两种调用方式:
```cpp
Widget w;
...
std::string name("Bart");
w.addName(name); // call addName with lvalue
...
w.addName(name + "Jenne"); // call addName with rvalue
```
现在分别考虑三种实现中,两种调用方式,拷贝和移动操作的开销。会忽略编译器对于移动和拷贝操作的优化。
- **Overloading重载**:无论传递左值还是传递右值,调用都会绑定到一种`newName`的引用实现方式上。拷贝和复制零开销。左值重载中,`newName`拷贝到`Widget::names`中,右值重载中,移动进去。开销总结:左值一次拷贝,右值一次移动。
- **Using a universal reference通用模板方式**:同重载一样,调用也绑定到`addName`的引用实现上,没有开销。由于使用了`std::forward`,左值参数会复制到`Widget::names`,右值参数移动进去。开销总结同重载方式。
Item25 解释了如果调用者传递的参数不是`std::string`类型,将会转发到`std::string`的构造函数(几乎是零开销的拷贝或者移动操作)。因此通用引用的方式同样有同样效率,所以者不影响本次分析,简单分析`std::string`参数类型即可。
- **Passing by value按值传递**:无论传递左值还是右值,都必须构造`newName`参数。如果传递的是左值,需要拷贝的开销,如果传递的是右值,需要移动的开销。在函数的实现中,`newName`总是采用移动的方式到`Widget::names`。开销总结:左值参数,一次拷贝一次移动,右值参数两次移动。对比按引动传递的方法,对于左值或者右值,均多出一次移动操作。
再次回顾本Item的内容
```
总是考虑直接按值传递,如果参数可拷贝并且移动操作开销很低
```
这样措辞是有原因的:
1. 应该仅*consider using pass by value*。是的,因为只需要编写一个函数,同时只会在目标代码中生成一个函数。避免了通用引用方式的种种问题。但是毕竟开销会更高,而且下面还会讨论,还会存在一些目前我们并未讨论到的开销。
2. 仅考虑对于*copable parameters*按值传递。不符合此条件的的参数必须只有移动构造函数。回忆一下“重载”方案的问题,就是必须编写两个函数来分别处理左值和右值,如果参数没有拷贝构造函数,那么只需要编写右值参数的函数,重载方案就搞定了。
考虑一下`std::unique_ptr<std::string>`的数据成员和其`set`函数。因为`std::unique_ptr`是仅可移动的类型,所以考虑使用“重载”方式编写即可:
```cpp
class Widget {
public:
...
void setPtr(std::unique_ptr<std::string>&& ptr) {
p = std::move(ptr);
}
private:
std::unique_ptr<std::string> p;
};
```
调用者可能会这样写:
```cpp
Widget w;
...
w.setPtr(std::make_unique<std::string>("Modern C++"));
```
这样,传递给`setPtr`的参数就是右值,整体开销就是一次移动。如果使用传值方式编写:
```cpp
class Widget {
public:
...
void setPtr(std::unique_ptr<std::string> ptr) {
p = std::move(ptr);
}
private:
std::unique_ptr<std::string> p;
};
```
同样的调用就会先使用移动构造函数移动到参数`ptr`,然后再移动到`p`,整体开销就是两次移动。
3. 按值传递应该仅应用于哪些*cheap to move*的参数。当移动的开销较低,额外的一次移动才能被开发者接受,但是当移动的开销很大,执行不必要的移动类似不必要的复制时,这个规则就不适用了。
4. 你应该只对*always copied肯定复制*的参数考虑按值传递。为了看清楚为什么这很重要,假定在复制参数到`names`容器前,`addName`需要检查参数的长度是否过长或者过短,如果是,就忽略增加`name`的操作:
```cpp
class Widget { // Approach 3
public:
void addName(std::string newName) {
if ((newName.length() >= minLen) && (newName.length() <= maxLen)) {
names.push_back(std::move(newName));
}
}
...
private:
std::vector<std::string> names;
};
```
即使这个函数没有在`names`添加任何内容,也增加了构造和销毁`newName`的开销,而按引用传递会避免这笔开销。
即使你编写的函数是移动开销小的参数而且无条件复制,有时也可能不适合按值传递。这是因为函数复制参数存在两种方式:一种是通过构造函数(拷贝构造或者移动构造),还有一种是赋值(拷贝赋值或者移动赋值)。`addName`使用构造函数,它的参数传递给`vector::push_back`,在这个函数内部,`newName`是通过构造函数在`std::vector`创建一个新元素。对于使用构造函数拷贝参数的函数,上述分析已经可以给出最终结论:按值传递对于左值和右值均增加了一次移动操作的开销。
当参数通过赋值操作进行拷贝时,分析起来更加复杂。比如,我们有一个表征密码的类,因为密码可能会被修改,我们提供了`setter`函数`changeTo`。用按值传递的策略,我们实现一个密码类如下:
```cpp
class Password {
public:
explicit Password(std::string pwd) : text(std::move(pwd)) {}
void changeTo(std::string newPwd) {
text = std::move(newPwd);
}
...
private:
std::string text;
};
```
将密码存储为纯文本格式恐怕将使你的软件安全团队抓狂,但是先忽略这点考虑这段代码:
```cpp
std::string initPwd("Supercalifragilisticexpialidocious");
Password p(initPwd);
```
`p.text`被给定的密码构造,用按值传递的方式增加了一次移动操作的开销相对于重载或者通用引用,但是这无关紧要,一切看起来如此美好。
但是,该程序的用户可能对初始密码不太满意,因为这段密码`"Supercalifragilisticexpialidocious"`在许多字典中可以被发现。他或者她因此修改密码:
```cpp
std::string newPassword = "Beware the Jabberwock";
p.changeTo(newPassword);
```
不用关心新密码是不是比就密码更好,那是用户关心的问题。我们对于`changeTo`函数的按值传递实现方案会导致开销大大增加。
传递给`changeTo`的参数是一个左值(`newPassword`),所以`newPwd`参数需要被构造,`std::string`的拷贝构造函数会被调用,这个函数会分配新的存储空间给新密码。`newPwd`会移动赋值到`text`,这会导致释放旧密码的内存。所以`changeTo`存在两次动态内存管理的操作:一次是为新密码创建内存,一次是销毁旧密码的内存。
但是在这个例子中,旧密码比新密码长度更长,所以本来不需要分配新内存,销毁就内存的操作。如果使用重载的方式,两次动态内存管理操作可以避免:
```cpp
class Password {
public:
...
void changeTo(std::string& newPwd) {
text = newPwd;
}
...
private:
std::string text;
};
```
这种情况下,按值传递的开销(包括了内存分配和内存销毁)可能会比`std::string`的`move`操作高出几个数量级。
有趣的是,如果旧密码短于新密码,在赋值过程中就不可避免要重新分配内存,这种情况,按值传递跟按引用传递的效率是一样的。因此,参数的赋值操作开销取决于具体的参数的值,这种分析适用于动态分配内存的参数类型。
这种潜在的开销增加仅在传递左值参数时才适用,因为执行内存分配和释放通常发生在复制操作中。
结论是,使用按值传递的函数通过赋值复制一个参数的额外开销取决于传递的类型中左值和右值的比例,即这个值是否需要动态分配内存,以及赋值操作符的具体实现中对于内存的使用。对于`std::string`来说,取决于实现是否使用了小字符串优化(SSO 参考Item 29)如果是值是否匹配SSO缓冲区。
所以,正如我所说,当参数通过赋值进行拷贝时,分析按值传递的开销是复杂的。通常,最有效的经验就是“在证明没问题之前假设有问题”,就是除非已证明按值传递会为你需要的参数产生可接受开销的执行效率,否则使用重载或者通用引用的实现方式。
到此为止,对于需要运行尽可能快的软件来说,按值传递可能不是一个好策略,因为毕竟多了一次移动操作。此外,有时并不能知道是不是还多了其他开销。在`Widget::addName`例子中,按值传递仅多了一次移动操作,但是如果加入值的一些校验,可能按值传递就多了创建和销毁类型的开销相对于重载和通用引用的实现方式。
可以看到导致的方向,在调用链中,每次调用多了一次移动的开销,那么当调用链较长,总体就会产生无法忍受的开销,通过引用传递,调用链不会增加任何开销。
跟性能无关总是需要考虑的是按值传递不像按引用传递那样会收到切片问题的影响。这是C++98的问题在此不在详述但是如果要设计一个函数来处理这样的参数基类或者其派生类如果不想声明为按值传递因为你就是要分割派生类型
```cpp
class Widget{...};
class SpecialWidget: public Widget{...};
void processWidget(Widget w);
...
SecialWidget sw;
...
processWidget(sw);
```
如果不熟悉**slicing problem**可以先通过搜索引擎了解一下。这样你就知道切片问题是另一个C++98中默认按值传递名声不好的原因。有充分的理由来说明为什么你学习C++编程的第一件事就是避免用户自定义类型进行按值传递。
C++11没有从根本上改变C++98按值传递的基本盘通常按值传递仍然会带来你希望避免的性能下降而且按值传递会导致切片问题。C++11中新的功能是区分了左值和右值实现了可移动类型的移动语义尽管重载和通用引用都有其缺陷。对于特殊的场景复制参数总是会被拷贝而且移动开销小的函数可以按值传递这种场景通常也不会有切片问题这时按值传递就提供了一种简单的实现方式同时实现了接近引用传递的开销的效率。
## 需要记住的事
- 对于可复制,移动开销低,而且无条件复制的参数,按值传递效率基本与按引用传递效率一致,而且易于实现,生成更少的目标代码
- 通过构造函数拷贝参数可能比通过赋值拷贝开销大的多
- 按值传递会引起切片问题,所说不适合基类类型的参数

View File

@ -6,6 +6,7 @@
[![Sponsors on Open Collective](https://opencollective.com/EffectiveModernCppChinese/sponsors/badge.svg)](#sponsors)
> ! 2017.10开始更新<br>
> ! 标注“已修订”的章节表示已经没有大致的错误
> ! 我没有版权,我没有版权,我没有版权<br>
> 本书要求读者具有C++基础<br>
> !未翻译的条款名称现在直译,翻译时可能适当修改<br>
@ -37,30 +38,30 @@
2. [Item 19:对于共享资源使用std::shared_ptr](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item19.md) __已修订__
3. [Item 20:像std::shared_ptr一样使用std::weak_ptr可能造成dangle](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item20.md) __更新中__
4. Item 21:优先考虑使用std::make_unique和std::make_shared而非new
5. [Item 22:当使用Pimpl惯用法请在实现文件中定义特殊成员函数](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item22.md)
5. [Item 22:当使用Pimpl惯用法请在实现文件中定义特殊成员函数](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/4.SmartPointers/item22.md) __由 @BlurryLight贡献__
5. 右值引用,移动语意,完美转发
1. [Item 23:理解std::move和std::forward](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RvalueReferences_MovingSemantics_And_PerfectForwarding/item23.md)
2. [Item 24:区别通用引用和右值引用](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RvalueReferences_MovingSemantics_And_PerfectForwarding/item24.md)
1. [Item 23:理解std::move和std::forward](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RvalueReferences_MovingSemantics_And_PerfectForwarding/item23.md) __由 @BlurryLight贡献__
2. [Item 24:区别通用引用和右值引用](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RvalueReferences_MovingSemantics_And_PerfectForwarding/item24.md) __由 @BlurryLight贡献__
3. Item 25:对于右值引用使用std::move对于通用引用使用std::forward
4. Item 26:避免重载通用引用
5. Item 27:熟悉重载通用引用的替代品
6. Item 28:理解引用折叠
7. Item 29:认识移动操作的缺点
7. [Item 29:认识移动操作的缺点](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/5.RvalueReferences_MovingSemantics_And_PerfectForwarding/item29.md) __由 @wendajiang贡献__
8. Item 30:熟悉完美转发失败的情况
6. Lambda表达式
1. [Item 31:避免使用默认捕获模式](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/6.Lambda%20Expressions/item31.md)
2. [Item 32:使用初始化捕获来移动对象到闭包中](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/6.Lambda%20Expressions/item32.md)
1. [Item 31:避免使用默认捕获模式](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/6.Lambda%20Expressions/item31.md) __由 @LucienXian贡献__
2. [Item 32:使用初始化捕获来移动对象到闭包中](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/6.Lambda%20Expressions/item32.md) __由 @LucienXian贡献__
3. Item 33:对于std::forward的auto&&形参使用decltype
4. Item 34:有限考虑lambda表达式而非std::bind
7. 并发API
1. Item 35:优先考虑基于任务的编程而非基于线程的编程
1. [Item 35:优先考虑基于任务的编程而非基于线程的编程](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.The%20Concurrency%20API/Item35.md) __由 @wendajiang贡献__
2. Item 36:如果有异步的必要请指定std::launch::threads
3. Item 37:从各个方面使得std::threads unjoinable
4. Item 38:知道不同线程句柄析构行为
5. Item 39:考虑对于单次事件通信使用void
6. Item 40:对于并发使用std::atomicvolatile用于特殊内存区
8. 微调
1. Item 41:对于那些可移动总是被拷贝的形参使用传值方式
1. [Item 41:对于那些可移动总是被拷贝的形参使用传值方式](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/8.Tweaks/item41.md) __由 @wendajiang贡献__
2. Item 42:考虑就地创建而非插入
## 贡献者