Merge pull request #76 from Phreer/master

修改部分书写错误及格式.
This commit is contained in:
Yi Yang 2021-03-12 23:24:42 +08:00 committed by GitHub
commit bf8509cddc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 39 additions and 40 deletions

View File

@ -8,7 +8,7 @@
即使显式支持了移动操作结果可能也没有你希望的那么好。比如所有C++11的标准库都支持了移动操作但是认为移动所有容器的开销都非常小是个错误。对于某些容器来说压根就不存在开销小的方式来移动它所包含的内容。对另一些容器来说开销真正小的移动操作却使得容器元素移动含义事与愿违。
考虑一下`std::array`这是C++11中的新容器。`std::array`本质上是具有STL接口的内置数组。这与其他标准容器将内容存储在堆内存不同。存储具体数据在堆内存的容器本身只保存了只想堆内存数据的指针(真正实现当然更复杂一些,但是基本逻辑就是这样)。这种实现使得在常数时间移动整个容器成为可能的,只需要拷贝容器中保存的指针到目标容器,然后将原容器的指针置为空指针就可以了。
考虑一下`std::array`这是C++11中的新容器。`std::array`本质上是具有STL接口的内置数组。这与其他标准容器将内容存储在堆内存不同。存储具体数据在堆内存的容器本身只保存了指向堆内存数据的指针(真正实现当然更复杂一些,但是基本逻辑就是这样)。这种实现使得在常数时间移动整个容器成为可能的,只需要拷贝容器中保存的指针到目标容器,然后将原容器的指针置为空指针就可以了。
```cpp
std::vector<Widget> vm1;
@ -26,7 +26,7 @@ auto aw2 = std::move(aw1); // move aw1 into aw2. Runs in linear time. All elemen
注意`aw1`中的元素被移动到了`aw2`中,这里假定`Widget`类的移动操作比复制操作快。但是使用`std::array`的移动操作还是复制操作都将花费线性时间的开销,因为每个容器中的元素终归需要拷贝一次,这与“移动一个容器就像操作几个指针一样方便”的含义想去甚远。
另一方面,`std::strnig`提供了常数时间的移动操作和线性时间的复制操作。这听起来移动比复制快多了,但是可能不一定。许多字符串的实现采用了*small string optimization(SSO)*。"small"字符串比如长度小于15个字符的存储在了`std::string`的缓冲区中,并没有存储在堆内存,移动这种存储的字符串并不必复制操作更快。
另一方面,`std::strnig`提供了常数时间的移动操作和线性时间的复制操作。这听起来移动比复制快多了,但是可能不一定。许多字符串的实现采用了*small string optimization (SSO)*。"small"字符串比如长度小于15个字符的存储在了`std::string`的缓冲区中,并没有存储在堆内存,移动这种存储的字符串并不必复制操作更快。
SSO的动机是大量证据表明短字符串是大量应用使用的习惯。使用内存缓冲区存储而不分配堆内存空间是为了更好的效率。然而这种内存管理的效率导致移动的效率并不必复制操作高。

View File

@ -1,10 +1,10 @@
## Item30熟悉完美转发的失败case
## Item30熟悉完美转发的失败 case
C++11最显眼的功能之一就是完美转发功能。完美转发太棒了开始使用你就发现“完美”理想与现实还是有差距。C++11的完美转发是非常好用但是只有当你愿意忽略一些失败情况这个Item就是使你熟悉这些情形。
C++11最显眼的功能之一就是完美转发功能。完美转发太棒了开始使用你就发现“完美”理想与现实还是有差距。C++11 的完美转发是非常好用,但是只有当你愿意忽略一些失败情况,这个 Item 就是使你熟悉这些情形。
在我们开始epsilon探索之前有必要回顾一下“完美转发”的含义。“转发”仅表示将一个函数的参数传递给另一个函数。对于被传递的第二个函数目标是收到与第一个函数完全相同的对象。这就排除了按值传递参数因为它们是原始调用者传入内容的副本。我们希望被转发的函数能够可以与原始函数一起使用对象。指参数也被排除在外,因为我们不想强迫调用者传入指针。关于通用转发,我们将处理引用参数。
在我们开始 epsilon 探索之前,有必要回顾一下“完美转发”的含义。“转发”仅表示将一个函数的参数传递给另一个函数。对于被传递的第二个函数目标是收到与第一个函数完全相同的对象。这就排除了按值传递参数,因为它们是原始调用者传入内容的副本。我们希望被转发的函数能够可以与原始函数一起使用对象。指参数也被排除在外,因为我们不想强迫调用者传入指针。关于通用转发,我们将处理引用参数。
完美转发意味着我们不仅转发对象我们还转发显著的特征它们的类型是左值还是右值是const还是volatile。结合到我们会处理引用参数这意味着我们将使用通用引用参见Item24因为通用引用参数被传入参数时才确定是左值还是右值。
完美转发意味着我们不仅转发对象,我们还转发显著的特征:它们的类型,是左值还是右值,是 `const` 还是 `volatile`。结合到我们会处理引用参数这意味着我们将使用通用引用参见Item24因为通用引用参数被传入参数时才确定是左值还是右值。
假定我们有一些函数f然后想编写一个转发给它的函数就使用一个函数模板。我们需要的核心看起来像是这样
@ -16,7 +16,7 @@ void fwd(T&& param) // accept any argument
}
```
从本质上说,转发功能是通用的。例如fwd模板接受任何类型的采纳并转发得到的任何参数。这种通用性的逻辑扩展是转发函数不仅是模板而且是可变模板因此可以接受任何数量的参数。fwd的可变个如下:
从本质上说,转发功能是通用的。例如 `fwd` 模板,接受任何类型的参并转发得到的任何参数。这种通用性的逻辑扩展是转发函数不仅是模板而且是可变模板因此可以接受任何数量的参数。fwd的可变个如下:
```cpp
template<typename... Ts>
@ -26,7 +26,7 @@ void fwd(Ts&&... params) // accept any arguments
}
```
这种形式你会在标准化容器emplace中参见Item42和只能容器的工厂函数`std::make_unique和std::make_shared`中(参见Item21看到。
这种形式你会在标准化容器emplace中参见 Item42和智能指针的工厂函数`std::make_unique` 和 `std::make_shared`中(参见 Item21看到。
给定我们的目标函数f和被转发的函数fwd如果f使用特定参数做一件事但是fwd使用相同的参数做另一件事完美转发就会失败
@ -37,7 +37,7 @@ fwd(expression); // but this does something else, fwd fails to perfectly forward
导致这种失败的原因有很多。知道它们是什么以及如何解决它们很重要,因此让我们来看看那种参数无法做到完美转发。
### Braced initializers支撑初始化器)
### Braced initializers花括号初始化器)
假定f这样声明
@ -59,12 +59,12 @@ fwd({1,2,3}); // error! doesn't compile
这是因为这是完美转发失效的一种情况。
所有这种错误有相同的原因。在对f的直接调用例如f({1,2,3})),编译器看到传入的参数是声明中的类型。如果类型不匹配,就会执行隐式转换操作使得调用成功。在上面的例子中,从`{1,2,3}`生成了临时变量`std::vector<int>`对象因此f的参数会绑定到`std::vector<int>`对象上。
所有这种错误有相同的原因。在对f的直接调用例如 `f({1,2,3})`),编译器看到传入的参数是声明中的类型。如果类型不匹配,就会执行隐式转换操作使得调用成功。在上面的例子中,从`{1,2,3}`生成了临时变量`std::vector<int>`对象因此f的参数会绑定到`std::vector<int>`对象上。
当通过调用函数模板fwd调用f时编译器不再比较传入给fwd的参数和f的声明中参数的类型。代替的是推导传入给fwd的参数类型然后比较推导后的参数类型和f的声明类型。当下面情况任何一个发生时完美转发就会失败
当通过调用函数模板 `fwd` 调用f时编译器不再比较传入给 `fwd` 的参数和f的声明中参数的类型。代替的是推导传入给fwd的参数类型然后比较推导后的参数类型和f的声明类型。当下面情况任何一个发生时完美转发就会失败
- **编译器不能推导出一个或者多个fwd的参数类型**,编译器就会报错
- **编译器将一个或者多个fwd的参数类型推导错误**。在这里“错误”可能意味着fwd将无法使用推导出的类型进行编译但是也可能意味着调用者f使用fwd的推导类型对比直接传入参数类型表现出不一致的行为。这种不同行为的原因可能是因为f的函数重载定义并且由于是“不正确的”类型推导在fwd内部调用f和直接调用f将重载不同的函数。
- **编译器不能推导出一个或者多个 `fwd` 的参数类型**,编译器就会报错
- **编译器将一个或者多个 `fwd` 的参数类型推导错误**。在这里“错误”可能意味着fwd将无法使用推导出的类型进行编译但是也可能意味着调用者f使用fwd的推导类型对比直接传入参数类型表现出不一致的行为。这种不同行为的原因可能是因为f的函数重载定义并且由于是“不正确的”类型推导在fwd内部调用f和直接调用f将重载不同的函数。
在上面的`f({1,2,3})`例子中,问题在于,如标准所言,将括号初始化器传递给未声明为`std::initializer_list`的函数模板参数该标准规定为“非推导上下文”。简单来讲这意味着编译器在对fwd的调用中推导表达式`{1,2,3}`的类型因为fwd的参数没有声明为`std::initializer_list`。对于fwd参数的推导类型被阻止编译器只能拒绝该调用。
@ -75,13 +75,13 @@ auto il = {1,2,3}; // il's type deduced to be std::initializer_list<int>
fwd(il); // fine, perfect-forwards il to f
```
### 0或者NULL作为空指针
### 0 或者 `NULL` 作为空指针
Item8说明当你试图传递0或者NULL作为空指针给模板时类型推导会出错推导为一个整数类型而不是指针类型。结果就是不管是0还是NULL都不能被完美转发为空指针。解决方法非常简单使用nullptr就可以了具体的细节参考Item 8.
Item8说明当你试图传递 0 或者 `NULL` 作为空指针给模板时类型推导会出错推导为一个整数类型而不是指针类型。结果就是不管是0还是NULL都不能被完美转发为空指针。解决方法非常简单使用 `nullptr` 就可以了,具体的细节可参考Item 8.
### 仅声明的整数静态const数据成员
### 仅声明的整数静态 `const` 数据成员
通常无需在类中定义整数静态const数据成员声明就可以了。这是因为编译器会对此类成员
通常无需在类中定义整数静态const数据成员声明就可以了。这是因为编译器会对此类成员进行常量传播 (const propagation), 而不需要为它们开辟内存. 例如考虑下面的代码:
```cpp
class Widget {
@ -96,7 +96,7 @@ widgetData.reserve(Widget::MinVals); // use of MinVals
这里,我们使用`Widget::MinVals`或者简单点MinVals来确定`widgetData`的初始容量,即使`MinVals`缺少定义。编译器通过将值28放入所有位置来补充缺少的定义。没有为`MinVals`的值留存储空间是没有问题的。如果要使用`MinVals`的地址(例如,有人创建了`MinVals`的指针),则`MinVals`需要存储(因为指针总要有一个地址),尽管上面的代码仍然可以编译,但是链接时就会报错,直到为`MinVals`提供定义。
按照这个思路想象下f转发参数给fwd的函数这样声明
按照这个思路想象下f转发参数给 `fwd` 的函数)这样声明:
```cpp
void f(std::size_t val);
@ -155,9 +155,9 @@ int processVal(int value, int priority);
f(processVal); // fine
```
但是有一点要注意f要求一个函数指针但是`processVal`不是一个函数指针或者一个函数它是两个同名的函数。但是编译器可以知道它需要哪个通过参数类型和数量来匹配。因此选择了一个int参数的`processVal`地址传递给f
但是有一点要注意f要求一个函数指针但是`processVal`不是一个函数指针或者一个函数它是两个同名的函数。但是编译器可以知道它需要哪个通过参数类型和数量来匹配。因此选择了一个int参数的`processVal`地址传递给`f`。
工作的基本机制是让编译器帮选择f的声明选择一个需要的`processVal`。但是fwd是一个函数模板没有需要的类型信息使得编译器不可能帮助自动匹配一个合适的函数
工作的基本机制是让编译器帮选择f的声明选择一个需要的`processVal`。但是,`fwd`是一个函数模板,没有需要的类型信息,使得编译器不可能帮助自动匹配一个合适的函数:
```cpp
fwd(processVal); // error! which processVal?
@ -173,7 +173,7 @@ T workOnVal(T param) { ... } // template for processing values
fwd(workOnVal); // error! which workOnVal instantiation ?
```
获得像fwd的完美转发接受一个重载函数名或者模板函数名的方式是指定转发的类型。比如你可以创造与f相同参数类型的函数指针通过processVal或者workOnVal实例化这个函数指针可以引导生成代码时正确选择函数实例然后传递指针给f
获得像`fwd`的完美转发接受一个重载函数名或者模板函数名的方式是指定转发的类型。比如你可以创造与f相同参数类型的函数指针通过`processVal`或者`workOnVal`实例化这个函数指针可以引导生成代码时正确选择函数实例然后传递指针给f
```cpp
using ProcessFuncType = int (*)(int); // make typedef; see Item 9
@ -182,7 +182,7 @@ fwd(processValPtr); // fine
fwd(static_cast<ProcessFuncType>(workOnVal)); // alse fine
```
当然这要求你知道fwd转发的函数指针的类型。对于完美转发来说这一点并不合理毕竟完美转发被设计为转发任何内容如果没有文档告诉你转发的类型你如何知道译者注这里应该想表达这是解决重载函数名或者函数模板的解决方案但是这是完美转发本身的问题
当然这要求你知道fwd转发的函数指针的类型。对于完美转发来说这一点并不合理毕竟完美转发被设计为转发任何内容如果没有文档告诉你转发的类型你如何知道译者注这里应该想表达这是解决重载函数名或者函数模板的解决方案但是这是完美转发本身的问题
### 位域
@ -191,15 +191,15 @@ fwd(static_cast<ProcessFuncType>(workOnVal)); // alse fine
```cpp
struct IPv4Header {
std::uint32_t version:4,
IHL:4,
DSCP:6,
ECN:2,
totalLength:16;
IHL:4,
DSCP:6,
ECN:2,
totalLength:16;
...
};
```
如果声明我们的函数f转发函数fwd的目标为接收一个`std::size_t`的参数则使用IPv4Header对象的totalLength字段进行调用没有问题
如果声明我们的函数`f`转发函数fwd的目标为接收一个`std::size_t`的参数,则使用`IPv4Header`对象的`totalLength`字段进行调用没有问题:
```cpp
void f(std::size_t sz);
@ -208,17 +208,17 @@ IPv4Header h;
f(h.totalLength);// fine
```
如果通过fwd转发h.totalLength给f呢那就是一个不同的情况了
如果通过 `fwd` 转发 `h.totalLength` `f` 呢,那就是一个不同的情况了:
```cpp
fwd(h.totalLength); // error!
```
问题在于fwd的参数是引用而h.totalLength是非常量位域。听起来并不是那么糟糕但是C++标准非常清楚地谴责了这种组合非常量引用不应该绑定到位域。禁止的理由很充分。位域可能包含了机器字节的任意部分比如32位int的3-5位但是无法直接定位。我之前提到了在硬件层面引用和指针时一样的所以没有办法创建一个指向任意bit的指针C++规定你可以指向的最小单位是char所以就没有办法绑定引用到任意bit上。
问题在于 `fwd` 的参数是引用,而 `h.totalLength` 是非常量位域。听起来并不是那么糟糕但是C++标准非常清楚地谴责了这种组合:非常量引用不应该绑定到位域。禁止的理由很充分。位域可能包含了机器字节的任意部分(比如 32 `int` 3 - 5 但是无法直接定位。我之前提到了在硬件层面引用和指针时一样的所以没有办法创建一个指向任意bit的指针C++规定你可以指向的最小单位是`char`),所以就没有办法绑定引用到任意 bit 上。
一旦意识到接收位域作为参数的函数都将接收位域的副本,就可以轻松解决位域不能完美转发的问题。毕竟,没有函数可以绑定引用到位域,也没有函数可以接受指向位域的指针(不存在这种指针)。这种位域类型的参数只能按值传递,或者有趣的事,常量引用也可以。在按值传递时,被调用的函数接受了一个位域的副本,而且事实表明,位域的常量引用也是将其“复制”到普通对象再传递。
传递位域给完美转发的关键就是利用接收参数函数接受的是一个副本的事实。你可以自己创建副本然后利用副本调用完美转发。在IPv4Header的例子中可以如下写法
传递位域给完美转发的关键就是利用接收参数函数接受的是一个副本的事实。你可以自己创建副本然后利用副本调用完美转发。在 `IPv4Header` 的例子中,可以如下写法:
```cpp
// copy bitfield value; see Item6 for info on init. form
@ -228,10 +228,9 @@ fwd(length); // forward the copy
### 总结
在大多数情况下,完美转发工作的很好。你基本不用考虑其他问题。但是当其不工作时,当看起来合理的代码无法编译,或者更糟的是,无法按照预期运行时,了解完美转发的缺陷就很重要了。同样重要的是如何解决它们。在大多数情况下都很简单
在大多数情况下,完美转发工作的很好。你基本不用考虑其他问题。但是当其不工作时,当看起来合理的代码无法编译,或者更糟的是,无法按照预期运行时,了解完美转发的缺陷就很重要了。同样重要的是如何解决它们。在大多数情况下都很简单
### 需要记住的事
- 完美转发会失败当模板类型推导失败或者推导类型错误
- 导致完美转发失败的类型有braced initializers作为空指针的0或者NULL只声明的整型static const数据成员模板和重载的函数名位域
- 完美转发会失败当模板类型推导失败或者推导类型错误。
- 导致完美转发失败的类型有 braced initializers作为空指针的 0 或者 `NULL`,只声明 (而未定义) 的整型 static const 数据成员,模板和重载的函数名和位域。

View File

@ -1,6 +1,6 @@
C++11的伟大标志之一是将并发整合到语言和库中。熟悉其他线程API比如pthreads或者Windows threads的开发者有时可能会对C++提供的斯巴达式译者注应该是简陋和严谨的意思功能集感到惊讶这是因为C++对于并发的大量支持是在编译器的约束层面。由此产生的语言保证意味着在C++的历史中开发者首次通过标准库可以写出跨平台的多线程程序。这位构建表达库奠定了坚实的基础并发标准库tasks, futures, threads, mutexes, condition variables, atomic objects等仅仅是成为并发软件开发者丰富工具集的基础。
在接下来的Item中记住标准库有两个futures的模板`std::future和std::shared_future`。在许多情况下,区别不重要,所以我们经常简单的混于一谈为*futures*。
在接下来的Item中记住标准库有两个futures的模板`std::future` `std::shared_future`。在许多情况下,区别不重要,所以我们经常简单的混于一谈为*futures*。
# 优先基于任务编程而不是基于线程
@ -15,13 +15,13 @@ auto fut = std::async(doAsyncWork); // "fut" for "future"
```
这种方式中,函数对象作为一个任务传递给 `std::async`
基于任务的方法通常比基于线程的方法更优,原因之一上面的代码已经表明,基于任务的方法代码量更少。我们假设唤醒`doAsyncWork`的代码对于其提供的返回值是有需求的。基于线程的方法对此无能为力,而基于任务的方法可以简单地获取`std::async`返回的`future`提供的`get`函数获取这个返回值。如果`doAsycnWork`发生了异常,`get`函数就显得更为重要,因为`get`函数可以提供抛出异常的访问,而基于线程的方法,如果`doAsyncWork`抛出了异常,线程会直接终止(通过调用`std::terminate`)。
基于任务的方法通常比基于线程的方法更优,原因之一上面的代码已经表明,基于任务的方法代码量更少。我们假设唤醒 `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::thread` 是C++执行过程的对象,并作为软件线程的句柄 (handle)。`std::thread` 存在多种状态1. `null`表示空句柄,因为处于默认构造状态(即没有函数来执行),因此不对应任何软件线程。 2. moved from (moved-to的`std::thread` 就对应软件进程开始执行) 3. `joined`(连接唤醒与被唤醒的两个线程) 4. `detached`(将两个连接的线程分离)
软件线程是有限的资源。如果开发者试图创建大于系统支持的硬件线程数量,会抛出`std::system_error`异常。即使你编写了不抛出异常的代码,这仍然会发生,比如下面的代码,即使 `doAsyncWork``noexcept`
```cpp
@ -43,11 +43,11 @@ std::thread t(doAsyncWork); // throw if no more
```cpp
auto fut = std::async(doAsyncWork); // onus of thread mgmt is
// on implement of
// the Standard Library
// on implement of
// the Standard Library
```
这种调用方式将线程管理的职责转交给C++标准库的开发者。举个例子,这种调用方式会减少抛出资源超额的异常,为何这么说调用`std::async`并不保证开启一个新的线程,只是提供了执行函数的保证,具体是否创建新的线程来运行此函数,取决于具体实现,比如可以通过调度程序来将`AsyncWork`运行在等待此函数结果的线程上,调度程序的合理性决定了系统是否会抛出资源超额的异常,但是这是库开发者需要考虑的事情了。
这种调用方式将线程管理的职责转交给C++标准库的开发者。举个例子,这种调用方式会减少抛出资源超额的异常,为何这么说? 调用 `std::async` 并不保证开启一个新的线程,只是提供了执行函数的保证,具体是否创建新的线程来运行此函数,取决于具体实现,比如可以通过调度程序来将`AsyncWork`运行在等待此函数结果的线程上,调度程序的合理性决定了系统是否会抛出资源超额的异常,但是这是库开发者需要考虑的事情了。
如果考虑自己实现在等待结果的线程上运行输出结果的函数,之前提到了可能引出负载不均衡的问题,`std::async`运行时的调度程序显然比开发者更清楚调度策略的制定,因为运行时调度程序管理的是所有执行过程,而不仅仅个别开发者运行的代码。