mirror of
https://github.com/CnTransGroup/EffectiveModernCppChinese.git
synced 2024-12-28 05:40:43 +08:00
commit
6ade27f0aa
@ -91,13 +91,14 @@ private:
|
||||
|
||||
我希望这段代码是不言自明的,但是下面几点说明可能会有所帮助:
|
||||
|
||||
- 构造器只接受`std::thread`右值,因为我们想要move`std::thread`对象给`ThreadRAII`(再次强调,`std::thread`不可以复制)
|
||||
- 构造器只接受`std::thread`右值,因为我们想要move `std::thread`对象给`ThreadRAII`(再次强调,`std::thread`不可以复制)
|
||||
|
||||
- 构造器的参数顺序设计的符合调用者直觉(首先传递`std::thread`,然后选择析构执行的动作),但是成员初始化列表设计的匹配成员声明的顺序。将`std::thread`成员放在声明最后。在这个类中,这个顺序没什么特别之处,调整为其他顺序也没有问题,但是通常,可能一个成员的初始化依赖于另一个,因为`std::thread`对象可能会在初始化结束后就立即执行了,所以在最后声明是一个好习惯。这样就能保证一旦构造结束,所有数据成员都初始化完毕可以安全的异步绑定线程执行
|
||||
|
||||
- `ThreadRAII`提供了`get`函数访问内部的`std::thread`对象。这类似于标准智能指针提供的`get`函数,可以提供访问原始指针的入口。提供`get`函数避免了`ThreadRAII`复制完整`std::thread`接口的需要,因为着`ThreadRAII`可以在需要`std::thread`上下文的环境中使用
|
||||
|
||||
- 在`ThreadRAII`析构函数调用`std::thread`对象t的成员函数之前,检查t是否joinable。这是必须的,因为在unjoinbale的`std::thread`上调用`join or detach`会导致未定义行为。客户端可能会构造一个`std::thread`t,然后通过t构造一个`ThreadRAII`,使用`get`获取t,然后移动t,或者调用`join or detach`,每一个操作都使得t变为unjoinable
|
||||
- 在`ThreadRAII`析构函数调用`std::thread`对象t的成员函数之前,检查t是否joinable。这是必须的,因为在unjoinbale的`std::thread`上调用`join or detach`会导致未定义行为。客户端可能会构造一个`std::thread` t,然后通过t构造一个`ThreadRAII`,使用`get`获取t,然后移动t,或者调用`join or detach`,每一个操作都使得t变为unjoinable
|
||||
|
||||
如果你担心下面这段代码
|
||||
|
||||
```cpp
|
||||
|
@ -120,7 +120,7 @@ y = x; // read x again
|
||||
|
||||
编译器可通过忽略对y的一次赋值来优化代码,因为初始化和赋值是冗余的。
|
||||
|
||||
正常内存还有一个特征,就是如果你写入内存没就不会读,再次吸入,第一次写就可以被忽略,因为肯定会被覆盖。给出下面的代码:
|
||||
正常内存还有一个特征,就是如果你写入内存没有读取,再次写入,第一次写就可以被忽略,因为肯定会被覆盖。给出下面的代码:
|
||||
|
||||
```cpp
|
||||
x = 10; // write x
|
||||
@ -143,7 +143,7 @@ auto y = x;
|
||||
x = 20;
|
||||
```
|
||||
|
||||
可能你会想睡会写这种重复读写的代码(技术上称为redundant loads 和 dead stores),答案是开发者不会直接写,至少我们不希望开发者这样写。但是在编译器执行了模板实例化,内联和一系列重排序优化之后,结果会出现多余的操作和无效存储,所以编译器需要摆脱这样的情况并不少见。
|
||||
可能你会想谁会写这种重复读写的代码(技术上称为redundant loads 和 dead stores),答案是开发者不会直接写,至少我们不希望开发者这样写。但是在编译器执行了模板实例化,内联和一系列重排序优化之后,结果会出现多余的操作和无效存储,所以编译器需要摆脱这样的情况并不少见。
|
||||
|
||||
这种有话讲仅仅在内存表现正常时有效。“特殊”的内存不行。最常见的“特殊”内存是用来memory-mapped I/O的内存。这种内存实际上是与外围设备(比如外部传感器或者显示器,打印机,网络端口)通信,而不是读写(比如RAM)。这种情况下,再次考虑多余的代码:
|
||||
|
||||
|
@ -71,13 +71,13 @@ vs.push_back(queenOfDisco); // copy-construct queenOfDisco
|
||||
vs.emplace_back(queenOfDisco); // ditto
|
||||
```
|
||||
|
||||
因此,emplacement函数可以完成insertion函数的所有功能。并且有时效率更高,至上在理论上,不会更低效。那为什么不在所有场合使用它们?
|
||||
因此,emplacement函数可以完成insertion函数的所有功能。并且有时效率更高,至少在理论上,不会更低效。那为什么不在所有场合使用它们?
|
||||
|
||||
因为,就像说的那样,理论上,在理论和实际上没有什么区别,但是实际,区别还是有的。在当前标准库的实现下,有些场景,就像预期的那样,emplacement执行性能优于insertion,但是,有些场景反而insertion更快。这种场景不容易描述,因为依赖于传递的参数类型、容器类型、emplacement或insertion的容器位置、容器类型构造器的异常安全性和对于禁止重复值的容器(即`std::set,std::map,std::unorder_set,set::unorder_map`)要添加的值是否已经在容器中。因此,大致的调用建议是:通过benchmakr测试来确定emplacment和insertion哪种更快。
|
||||
|
||||
当然这个结论不是很令人满意,所以还有一种启发式的方法来帮助你确定是否应该使用emplacement。如果下列条件都能满足,emplacement会优于insertion:
|
||||
|
||||
- **值是通过构造器添加到容器,而不是直接赋值。**例子就像本Item刚开始的那样(添加"xyzzy"到`std::string的std::vector`中)。新值必须通过`std::string`的构造器添加到`std::vector`。如果我们回看这个例子,新值放到已经存在对象的位置,那情况就完全不一样了。考虑下:
|
||||
- **值是通过构造器添加到容器,而不是直接赋值。** 例子就像本Item刚开始的那样(添加"xyzzy"到`std::string的std::vector`中)。新值必须通过`std::string`的构造器添加到`std::vector`。如果我们回看这个例子,新值放到已经存在对象的位置,那情况就完全不一样了。考虑下:
|
||||
|
||||
```cpp
|
||||
std::vector<std::string> vs; // as before
|
||||
@ -89,9 +89,9 @@ vs.emplace_back(queenOfDisco); // ditto
|
||||
|
||||
而且,向容器添加元素是通过构造还是赋值通常取决于实现者。但是,启发式仍然是有帮助的。基于节点的容器实际上总是使用构造器添加新元素,大多数标准库容器都是基于节点的。例外的容器只有`std::vector, std::deque, std::string`(`std::array`也不是基于节点的,但是它不支持emplacement和insertion)。在不是基于节点的容器中,你可以依靠`emplace_back`来使用构造向容器添加元素,对于`std::deque`,`emplace_front`也是一样的。
|
||||
|
||||
- **传递的参数类型与容器的初始化类型不同。**再次强调,emplacement优于insertion通常基于以下事实:当传递的参数不是容器保存的类型时,接口不需要创建和销毁临时对象。当将类型为T的对象添加到container<T>时,没有理由期望emplacement比insertion运行的更快,因为不需要创建临时对象来满足insertion接口。
|
||||
- **传递的参数类型与容器的初始化类型不同。** 再次强调,emplacement优于insertion通常基于以下事实:当传递的参数不是容器保存的类型时,接口不需要创建和销毁临时对象。当将类型为T的对象添加到container<T>时,没有理由期望emplacement比insertion运行的更快,因为不需要创建临时对象来满足insertion接口。
|
||||
|
||||
- **容器不拒绝重复项作为新值。**这意味着容器要么允许添加重复值,要么你添加的元素都是不重复的。这样要求的原因是为了判断一个元素是否已经存在于容器中,emplacement实现通常会创建一个具有新值的节点,以便可以将该节点的值与现有容器中节点的值进行比较。如果要添加的值不在容器中,则链接该节点。然后,如果值已经存在,emplacement创建的节点就会被销毁,意味着构造和析构时浪费的开销。这样的创建就不会在insertion函数中出现。
|
||||
- **容器不拒绝重复项作为新值。** 这意味着容器要么允许添加重复值,要么你添加的元素都是不重复的。这样要求的原因是为了判断一个元素是否已经存在于容器中,emplacement实现通常会创建一个具有新值的节点,以便可以将该节点的值与现有容器中节点的值进行比较。如果要添加的值不在容器中,则链接该节点。然后,如果值已经存在,emplacement创建的节点就会被销毁,意味着构造和析构时浪费的开销。这样的创建就不会在insertion函数中出现。
|
||||
|
||||
本Item开始的例子中下面的调用满足上面的条件。所以调用比`push_back`运行更快。
|
||||
|
||||
@ -130,7 +130,7 @@ ptrs.push_back({new Widget, killWidget});
|
||||
|
||||
`std::shared_ptr`的临时对象创建应该可以避免,但是在这个场景下,临时对象值得被创建。考虑如下可能的时间序列:
|
||||
|
||||
1. 在上述的调用中,一个`std::shared_ptr<Widget> `的临时对象被创建来持有`new Widget`对象。称这个对象为*temp*。
|
||||
1. 在上述的调用中,一个`std::shared_ptr<Widget>`的临时对象被创建来持有`new Widget`对象。称这个对象为*temp*。
|
||||
2. `push_back`接受*temp*的引用。在节点的分配一个副本来复制*temp*的过程中,OOM异常被抛出
|
||||
3. 随着异常从`push_back`的传播,*temp*被销毁。作为唯一管理Widget的弱指针`std::shared_ptr`对象,会自动销毁`Widget`,在这里就是调用`killWidget`。
|
||||
|
||||
@ -221,7 +221,7 @@ std::regex r2(nullptr); // compiles
|
||||
|
||||
在标准的官方术语中,用于初始化r1的语法是所谓的复制初始化。相反,用于初始化r2的语法是(也被称为braces)被称为直接初始化。复制初始化不是显式调用构造器的,直接初始化是。这就是r2可以编译的原因。
|
||||
|
||||
然后回到`push_back和 emplace_back`,更一般来说,insertion函数对比emplacment函数。emplacement函数使用直接初始化,这意味着使用显式构造器。insertion函数使用复制初始化。因此:
|
||||
然后回到`push_back和emplace_back`,更一般来说,insertion函数对比emplacment函数。emplacement函数使用直接初始化,这意味着使用显式构造器。insertion函数使用复制初始化。因此:
|
||||
|
||||
```cpp
|
||||
regexes.emplace_back(nullptr); // compiles. Direct init permits use of explicit std::regex ctor taking a pointer
|
||||
|
Loading…
Reference in New Issue
Block a user