diff --git a/3.MovingToModernCpp/item9.md b/3.MovingToModernCpp/item9.md index 1adcddf..741f0f6 100644 --- a/3.MovingToModernCpp/item9.md +++ b/3.MovingToModernCpp/item9.md @@ -3,7 +3,7 @@ 我相信每个人都同意使用STL容器是个好主意,并且我希望Item18能说服你让你觉得使用**std:unique_ptr**也是个好主意,但我猜没有人喜欢写上几次 `std::unique_ptr>`这样的类型,它可能会让你患上腕管综合征的风险大大增加。 -避免上述医疗事故也很简单,引入**typedef**即可: +避免上述医疗悲剧也很简单,引入**typedef**即可: ````cpp typedef std::unique_ptr> UPtrMapSS; ```` @@ -11,7 +11,7 @@ typedef std::unique_ptr> UPtrMapS ````cpp using UPtrMapSS = std::unique_ptr>; ```` -这里给出的**typedef**和别名声明做的都是完全一样的事情,我们有理由想知道会不会出于一些技术上的原因两者有一个更好。 +由于这里给出的**typedef**和别名声明做的都是完全一样的事情,我们有理由想知道会不会出于一些技术上的原因两者有一个更好。 这里,在说它们之前我想提醒一下很多人都发现当声明一个函数指针时别名声明更容易理解: ````cpp @@ -43,7 +43,7 @@ struct MyAllocList { }; MyAllocList::type lw; ```` -它会变得很糟。如果你想使用在一个模板内使用**typedef**声明一个持有链表的对象,而这个对象又使用了模板参数,你就不得不在在**typedef**前面加上**typename** +更糟糕的是,如果你想使用在一个模板内使用**typedef**声明一个持有链表的对象,而这个对象又使用了模板参数,你就不得不在在**typedef**前面加上**typename** ````cpp template class Widget { @@ -54,7 +54,7 @@ private: ```` 这里**MyAllocList::type**使用了一个类型,这个类型依赖于模板参数**T**。 因此**MyAllocList::type**是一个依赖类型,在C++很多讨人喜欢的规则中的一个提到必须要在依赖类型名前加上**typename**。 -如果使用别名声明定义一个**MyAllocList**,就不需要使用**typename**(同事省略麻烦的**::type**后缀), +如果使用别名声明定义一个**MyAllocList**,就不需要使用**typename**(同时省略麻烦的::type后缀), ````cpp template using MyAllocList = std::list>; // as before @@ -84,24 +84,24 @@ private: … // 一个数据成员! }; ```` -就像你看到的,**MyAllocList::typ**不是一个类型。 -如果**Widget**使用**Wine**实例化,在**Widget**模板中的**MyAllocList::typ**将会是一个数据成员,不是一个类型。 -在**Widget**模板内,如果**MyAllocList::typ**表示的类型依赖于**T**,编译器就会坚持要求你在前面加上**typename**。 +就像你看到的,**MyAllocList::type**不是一个类型。 +如果**Widget**使用**Wine**实例化,在**Widget**模板中的**MyAllocList::type**将会是一个数据成员,不是一个类型。 +在**Widget**模板内,如果**MyAllocList::type**表示的类型依赖于**T**,编译器就会坚持要求你在前面加上**typename**。 如果你尝试过模板元编程(TMP), 你一定会碰到取模板类型参数然后基于它创建另一种类型的情况。 举个例子,给一个类型**T**,如果你想去掉**T**的常量修饰和引用修饰,比如你想把**const std::string&**变成**const std::string**。 又或者你想给一个类型加上**const**或左值引用,比如把**Widget**变成**const Widget**或**Widget&**。 (如果你没有用过玩过模板元编程,太遗憾了,因为如果你真的想成为一个高效C++程序员_[1]_,至少你需要熟悉C++的基础。你可以看看我在Item23,27提到的类型转换)。 -C++11在_type traits_中给了你一系列工具去实现类型转换,如果要使用这些模板请包含头文件。 -里面不全是类型转换的工具,也包含一些`predictable`接口的工具。给一个类型**T**,你想将它应用于转换中,结果类型就是**std::transformation ::type**,比如: +C++11在type traits中给了你一系列工具去实现类型转换,如果要使用这些模板请包含头文件。 +里面不全是类型转换的工具,也包含一些`predictable`接口的工具。给一个类型**T**,你想将它应用于转换中,结果类型就是**std::transformation\::type**,比如: ````cpp std::remove_const::type // 从const T中产出T std::remove_reference::type // 从T&和T&&中产出T std::add_lvalue_reference::type // 从T中产出T& ```` -注释仅仅简单的中介了类型转换做了什么,所以不要太随便的使用。 +注释仅仅简单的总结了类型转换做了什么,所以不要太随便的使用。 在你的项目使用它们之前,你最好看看它们的详细说明书。 -尽管写了一些,但我这里不是想给你一个关于type traits使用的教程。注意类型转换尾部的**::type**。 +尽管写了一些,但我这里不是想给你一个关于type traits使用的教程。注意类型转换尾部的::type。 如果你在一个模板内部使用类型参数,你也需要在它们前面加上**typename**。 至于为什么要这么做是因为这些type traits是通过在**struct**内嵌套**typedef**来实现的。 是的,它们使用类型别名_[2]_技术实现,而正如我之前所说这比别名声明要差。 @@ -137,7 +137,7 @@ using add_lvalue_reference_t = typename add_lvalue_reference::type; 记住 + typedef不支持模板化,但是别名声明支持。 -+ 别名模板避免了使用"::type"后缀,而且在模板中使用typedef还需要在前面加上typename ++ 别名模板避免了使用"::type"后缀,而且在模板中使用**typedef**还需要在前面加上**typename** + C++14提供了C++11所有类型转换的别名声明版本 ## 译注 diff --git a/5.RvalueReferences_MovingSemantics_And_PerfectForwarding/item23.md b/5.RvalueReferences_MovingSemantics_And_PerfectForwarding/item23.md index 78e37d9..d8969ce 100644 --- a/5.RvalueReferences_MovingSemantics_And_PerfectForwarding/item23.md +++ b/5.RvalueReferences_MovingSemantics_And_PerfectForwarding/item23.md @@ -19,7 +19,7 @@ void f(Widget&& w); ``` 参数`w`是一个左值,即使它的类型是一个**Widget**的右值引用(如果这里震惊到你了,请重新回顾从本书第二页开始的关于左值和右值的总览。) -## Item 20: 理解std::move和std::forward +## Item 23: 理解std::move和std::forward 为了了解`std::move`和`std::forward`,一种有用的方式是从*它们不做什么*这个角度来了解它们。`std::move`不移动(move)任何东西,`std::forward`也不转发(forward)任何东西。在运行期间(run-time),它们不做任何事情。它们不产生任何可执行代码,一字节也没有。 @@ -146,7 +146,7 @@ logAndProcess(std::move(w)); //call with rvalue 你也许会想知道`std::forward`是怎么知道它的参数是否是被一个右值初始化的。举个例子,在上述代码中,`std::forward`是怎么分辨参数`param`是被一个左值还是右值初始化的? 简短的说,该信息藏在函数`logAndProcess`的模板参数`T`中。该参数被传递给了函数`std::forward`,它解开了含在其中的信息。该机制工作的细节可以查询 Item 28. -考虑到`std::move`和`std::forward`都可以归结于**转换**,他们唯一的区别就是`std::move`总是执行转换,而`std::forward`偶尔为之。你可能会问是否我们可以免于使用`std::move`而在任何地方只使用`std::forward`。 从纯技术的角度,答案是yes: `std::forward`是可以完全胜任,`std::move`并非必须。当然,其实两者中没有哪一个函数是**真的必须**的,因为我们可以到处直接写转换代码,但是我希望我们能同意:这将相当的,嗯,让人恶心。 +考虑到`std::move`和`std::forward`都可以归结于**转换**,他们唯一的区别就是`std::move`总是执行转换,而`std::forward`偶尔为之。你可能会问是否我们可以免于使用`std::move`而在任何地方只使用`std::forward`。 从纯技术的角度,答案是yes: `std::forward`是可以完全胜任,`std::move`并非必须。当然,其实两者中没有哪一个函数是**真的必须**的,因为我们可以到处直接写转换代码,但是我希望我们能同意:这将相当的,嗯,让人恶心。 `std::move`的吸引力在于它的便利性: 减少了出错的可能性,增加了代码的清晰程度。考虑一个类,我们希望统计有多少次移动构造函数被调用了。我们只需要一个静态的计数器(static counter),它会在移动构造的时候自增。假设在这个类中,唯一一个非静态的数据成员是`std::string`,一种经典的移动构造函数(例如,使用std::move)可以被实现如下: @@ -178,7 +178,7 @@ public: } ``` -注意,第一,`std::move`只需要一个函数参数(rhs.s),而`std::forward`不但需要一个函数参数(rhs.s),还需要一个模板类型参数`std::string`。其次,我们转发给`std::forward`的参数类型应当是一个**非引用**(non-reference),因为传递的参数应该是一个右值(见 Item 28)。 同样,这意味着`std::move`比起`std::forward`来说需要打更少的字,并且免去了传递一个表示我们正在传递一个右值的类型参数。同样,它根绝了我们传递错误类型的可能性,(例如,`std::string&`可能导致数据成员`s`被复制而不是被移动构造)。 +注意,第一,`std::move`只需要一个函数参数(rhs.s),而`std::forward`不但需要一个函数参数(rhs.s),还需要一个模板类型参数`std::string`。其次,我们转发给`std::forward`的参数类型应当是一个**非引用**(non-reference),因为传递的参数应该是一个右值(见 Item 28)。 同样,这意味着`std::move`比起`std::forward`来说需要打更少的字,并且免去了传递一个表示我们正在传递一个右值的类型参数。同样,它根绝了我们传递错误类型的可能性,(例如,`std::string&`可能导致数据成员`s`被复制而不是被移动构造)。 更重要的是,`std::move`的使用代表着无条件向右值的转换,而使用`std::forward`只对绑定了右值的引用进行到右值转换。这是两种完全不同的动作。前者是典型地为了移动操作,而后者只是传递(亦作转发)一个对象到另外一个函数,保留它原有的左值属性或右值属性。因为这些动作实在是差异太大,所以我们拥有两个不同的函数(以及函数名)来区分这些动作。 diff --git a/6.Lambda Expressions/item34.md b/6.Lambda Expressions/item34.md new file mode 100644 index 0000000..797b504 --- /dev/null +++ b/6.Lambda Expressions/item34.md @@ -0,0 +1,277 @@ +# 考虑lambda表达式而非std::bind + +C++11中的`std::bind`是C++98的`std::bind1st`和`std::bind2nd`的后续,但在2005年已经成为了标准库的一部分。那时标准化委员采用了TR1的文档,其中包含了bind的规范。(在TR1中,`bind`位于不同的命名空间,因此它是`std::tr1::bind`,而不是`std::bind`,接口细节也有所不同)。这段历史意味着一些程序员有十年或更长时间的使用`std::bind`经验。如果您是其中之一,可能会不愿意放弃一个对您有用的工具。这是可以理解的,但是在这种情况下,改变是更好的,因为在C ++11中,`lambda`几乎是比`std :: bind`更好的选择。 从C++14开始,`lambda`的作用不仅强大,而且是完全值得使用的。 + +这个条目假设您熟悉`std::bind`。 如果不是这样,您将需要获得基本的了解,然后再继续。 无论如何,这样的理解都是值得的,因为您永远不知道何时会在必须阅读或维护的代码库中遇到`std::bind`的使用。 + +与第32项中一样,我们将从`std::bind`返回的函数对象称为绑定对象。 + +优先lambda而不是`std::bind`的最重要原因是lambda更易读。 例如,假设我们有一个设置闹钟的函数: + +```c++ +// typedef for a point in time (see Item 9 for syntax) +using Time = std::chrono::steady_clock::time_point; + +// see Item 10 for "enum class" +enum class Sound { Beep, Siren, Whistle }; + +// typedef for a length of time +using Duration = std::chrono::steady_clock::duration; +// at time t, make sound s for duration d void setAlarm(Time t, Sound s, Duration d); +``` + +进一步假设,在程序的某个时刻,我们已经确定需要设置一个小时后响30秒的闹钟。 但是,具体声音仍未确定。我们可以编写一个lambda来修改`setAlarm`的界面,以便仅需要指定声音: + +```c++ +// setSoundL ("L" for "lambda") is a function object allowing a // sound to be specified for a 30-sec alarm to go off an hour // after it's set +auto setSoundL = + [](Sound s) + { + // make std::chrono components available w/o qualification + using namespace std::chrono; + setAlarm(steady_clock::now() + hours(1), // alarm to go off + s, // in an hour for + seconds(30)); // 30 seconds + }; +``` + +我们在lambda中突出了对`setAlarm`的调用。这看来起是一个很正常的函数调用,即使是几乎没有lambda经验的读者也可以看到:传递给lambda的参数被传递给了`setAlarm`。 + +通过使用基于C++11对用户自定义常量的支持而建立的标准后缀,如秒(s),毫秒(ms)和小时(h)等,我们可以简化C++14中的代码。这些后缀在`std::literals`命名空间中实现,因此上述代码可以按照以下方式重写: + +```c++ +auto setSoundL = + [](Sound s) + { + using namespace std::chrono; + using namespace std::literals; // for C++14 suffixes + setAlarm(steady_clock::now() + 1h, // C++14, but + s, // same meaning + 30s); // as above + }; +``` + +下面是我们第一次编写对应的`std::bind`调用。这里存在一个我们后续会修复的错误,但正确的代码会更加复杂,即使是此简化版本也会带来一些重要问题: + +```c++ +using namespace std::chrono; // as above +using namespace std::literals; +using namespace std::placeholders; // needed for use of "_1" +auto setSoundB = std::bind(setAlarm, // "B" for "bind" + steady_clock::now() + 1h, // incorrect! see below + _1, + 30s); +``` + +我想像在lambda中一样突出显示对`setAlarm`的调用,但是没有这么做。这段代码的读者只需知道,调用`setSoundB`会使用在对`std :: bind`的调用中所指定的时间和持续时间来调用`setAlarm`。对于初学者来说,占位符**“ _1”**本质上是一个魔术,但即使是普通读者也必须从思维上将占位符中的数字映射到其在`std::bind`参数列表中的位置,以便明白调用`setSoundB`时的第一个参数会被传递进`setAlarm`,作为调用时的第二个参数。在对`std::bind`的调用中未标识此参数的类型,因此读者必须查阅`setAlarm`声明以确定将哪种参数传递给`setSoundB`。 + +但正如我所说,代码并不完全正确。在lambda中,表达式`steady_clock::now() + 1h`显然是是`setAlarm`的参数。调用`setAlarm`时将对其进行计算。这是合理的:我们希望在调用`setAlarm`后一小时发出警报。但是,在`std::bind`调用中,将`steady_clock::now() + 1h`作为参数传递给了`std::bind,而不是`setAlarm`。这意味着将在调用`std::bind`时对表达式进行求值,并且该表达式产生的时间将存储在结果绑定对象中。结果,闹钟将被设置为在调用`std::bind`后一小时发出声音,而不是在调用`setAlarm`一小时后发出。 + +要解决此问题,需要告诉`std::bind`推迟对表达式的求值,直到调用`setAlarm`为止,而这样做的方法是将对`std::bind`的第二个调用嵌套在第一个调用中: + +```c++ +auto setSoundB = + std::bind(setAlarm, + std::bind(std::plus<>(), steady_clock::now(), 1h), _1, + 30s); +``` + +如果您熟悉C++98的`std::plus`模板,您可能会惊讶地发现在此代码中,尖括号之间未指定任何类型,即该代码包含`std::plus<>`,而不是`std::plus`。 在C ++14中,通常可以省略标准运算符模板的模板类型参数,因此无需在此处提供。 C++11没有提供此类功能,因此等效于lambda的C ++11 `std::bind`使用为: + +```c++ +using namespace std::chrono; // as above +using namespace std::placeholders; +auto setSoundB = + std::bind(setAlarm, + std::bind(std::plus(), steady_clock::now(), hours(1)), + seconds(30)); +``` + +如果此时Lambda看起来不够吸引,那么应该检查一下视力了。 + +当setAlarm重载时,会出现一个新问题。 假设有一个重载函数,其中第四个参数指定了音量: + +```c++ +enum class Volume { Normal, Loud, LoudPlusPlus }; +void setAlarm(Time t, Sound s, Duration d, Volume v); +``` + +lambda能继续像以前一样使用,因为根据重载规则选择了`setAlarm`的三参数版本: + +```c++ +auto setSoundL = + [](Sound s) + { + using namespace std::chrono; + setAlarm(steady_clock::now() + 1h, s, + 30s); + }; +``` + +然而,`std::bind`的调用将会编译失败: + +```c++ +auto setSoundB = // error! which + std::bind(setAlarm, // setAlarm? + std::bind(std::plus<>(), + steady_clock::now(), + 1h), + _1, + 30s); +``` + +这里的问题是,编译器无法确定应将两个setAlarm函数中的哪一个传递给`std::bind`。 它们仅有的是一个函数名称,而这个函数名称是不确定的。 +要获得对`std::bind`的调用能进行编译,必须将`setAlarm`强制转换为适当的函数指针类型: + +```c++ +using SetAlarm3ParamType = void(*)(Time t, Sound s, Duration d); +auto setSoundB = // now + std::bind(static_cast(setAlarm), // okay + std::bind(std::plus<>(), + steady_clock::now(), + 1h), + _1, + 30s); +``` + +但这在`lambda`和`std::bind`的使用上带来了另一个区别。 在`setSoundL`的函数调用操作符(即lambda的闭包类对应的函数调用操作符)内部,对`setAlarm`的调用是正常的函数调用,编译器可以按常规方式进行内联: + +```c++ +setSoundL(Sound::Siren); // body of setAlarm may + // well be inlined here +``` + +但是,对`std::bind`的调用是将函数指针传递给`setAlarm`,这意味着在`setSoundB`的函数调用操作符(即绑定对象的函数调用操作符)内部,对`setAlarm`的调用是通过一个函数指针。 编译器不太可能通过函数指针内联函数,这意味着与通过`setSoundL`进行调用相比,通过`setSoundB`对`setAlarm的`调用,其函数不大可能被内联: + +```c++ +setSoundB(Sound::Siren); // body of setAlarm is less + // likely to be inlined here +``` + +因此,使用`lambda`可能会比使用`std::bind`能生成更快的代码。 +`setAlarm`示例仅涉及一个简单的函数调用。如果您想做更复杂的事情,使用lambda会更有利。 例如,考虑以下C++14的lambda使用,它返回其参数是否在最小值(`lowVal`)和最大值(`highVal`)之间的结果,其中`lowVal`和`highVal` 是局部变量: + +```c++ +auto betweenL = + [lowVal, highVal] + (const auto& val) // C++14 + { return lowVal <= val && val <= highVal; }; +``` + +使用`std::bind`可以表达相同的内容,但是该构造是一个通过晦涩难懂的代码来保证工作安全性的示例: + +```c++ +using namespace std::placeholders; // as above +auto betweenB = + std::bind(std::logical_and<>(), // C++14 + std::bind(std::less_equal<>(), lowVal, _1), + std::bind(std::less_equal<>(), _1, highVal)); +``` + +在C++11中,我们必须指定要比较的类型,然后`std::bind`调用将如下所示: + +```c++ +auto betweenB = // C++11 version + std::bind(std::logical_and(), + std::bind(std::less_equal(), lowVal, _1), + std::bind(std::less_equal(), _1, highVal)); +``` + +当然,在C++11中,lambda也不能采用`auto`参数,因此它也必须指定一个类型: + +```c++ +auto betweenL = // C++11 version + [lowVal, highVal] + (int val) + { return lowVal <= val && val <= highVal; }; +``` + +无论哪种方式,我希望我们都能同意,lambda版本不仅更短,而且更易于理解和维护。 +之前我就说过,对于那些没有`std::bind`使用经验的人,其占位符(例如\_1,\_2等)本质上都是magic。 但是,不仅仅占位符的行为是不透明的。 假设我们有一个函数可以创建Widget的压缩副本, + +```c++ +enum class CompLevel { Low, Normal, High }; // compression + // level +Widget compress(const Widget& w, // make compressed + CompLevel lev); // copy of w +``` + +并且我们想创建一个函数对象,该函数对象允许我们指定应将特定`w`的压缩级别。这种使用`std::bind`的话将创建一个这样的对象: + +```c++ +Widget w; +using namespace std::placeholders; +auto compressRateB = std::bind(compress, w, _1); +``` + +现在,当我们将`w`传递给`std::bind`时,必须将其存储起来,以便以后进行压缩。它存储在对象compressRateB中,但是这是如何存储的呢(是通过值还是引用)。之所以会有所不同,是因为如果在对`std::bind`的调用与对`compressRateB`的调用之间修改了`w`,则按引用捕获的`w`将反映其更改,而按值捕获则不会。 + +答案是它是按值捕获的,但唯一知道的方法是记住`std::bind`的工作方式;在对`std::bind`的调用中没有任何迹象。与lambda方法相反,其中`w`是通过值还是通过引用捕获是显式的: + +```c++ +auto compressRateL = // w is captured by + [w](CompLevel lev) // value; lev is + { return compress(w, lev); }; // passed by value +``` + +同样明确的是如何将参数传递给lambda。 在这里,很明显参数`lev`是通过值传递的。 因此: + +```c++ +compressRateL(CompLevel::High); // arg is passed + // by value +``` + +但是在对由`std::bind`生成的对象调用中,参数如何传递? + +```c++ +compressRateB(CompLevel::High); // how is arg + // passed? +``` + +同样,唯一的方法是记住`std::bind`的工作方式。(答案是传递给绑定对象的所有参数都是通过引用传递的,因为此类对象的函数调用运算符使用完美转发。) +与lambda相比,使用`std::bind`进行编码的代码可读性较低,表达能力较低,并且效率可能较低。 在C++14中,没有`std::bind`的合理用例。 但是,在C ++11中,可以在两个受约束的情况下证明使用`std::bind`是合理的: + +* 移动捕获。 C++11的lambda不提供移动捕获,但是可以通过结合lambda和`std::bind`来模拟。 有关详细信息,请参阅条款32,该条款还解释了在C ++ 14中,lambda对初始化捕获的支持将少了模拟的需求。 +* 多态函数对象。 因为绑定对象上的函数调用运算符使用完全转发,所以它可以接受任何类型的参数(以条款30中描述的完全转发的限制为例子)。当您要使用模板化函数调用运算符来绑定对象时,此功能很有用。 例如这个类, + +```c++ +class PolyWidget { + public: + template + void operator()(const T& param); ... +}; +``` + +`std::bind`可以如下绑定一个`PolyWidget`对象: + +```c++ +PolyWidget pw; +auto boundPW = std::bind(pw, _1); +``` + +`boundPW`可以接受任意类型的对象了: + +```c++ +boundPW(1930); // pass int to + // PolyWidget::operator() +boundPW(nullptr); // pass nullptr to + // PolyWidget::operator() +boundPW("Rosebud"); // pass string literal to + // PolyWidget::operator() +``` + +这一点无法使用C++11的lambda做到。 但是,在C++14中,可以通过带有`auto`参数的lambda轻松实现: + +```c++ +auto boundPW = [pw](const auto& param) // C++14 + { pw(param); }; +``` + +当然,这些是特殊情况,并且是暂时的特殊情况,因为支持C++14 lambda的编译器越来越普遍了。 +当`bind`在2005年被非正式地添加到C ++中时,与1998年的前身相比有了很大的改进。 在C ++11中增加了lambda支持,这使得`std::bind`几乎已经过时了,从C ++ 14开始,更是没有很好的用例了。 + +要谨记的是: + +* 与使用`std::bind`相比,Lambda更易读,更具表达力并且可能更高效。 +* 只有在C++11中,`std::bind`可能对实现移动捕获或使用模板化函数调用运算符来绑定对象时会很有用。 + diff --git a/README.md b/README.md index 74f54e2..cb95dcc 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ 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](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/6.Lambda%20Expressions/item33.md) __由 @LucienXian贡献__ - 4. Item 34:有限考虑lambda表达式而非std::bind + 4. [Item 34:优先考虑lambda表达式而非std::bind](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/6.Lambda%20Expressions/item34.md) __由 @LucienXian贡献__ 7. 并发API 1. [Item 35:优先考虑基于任务的编程而非基于线程的编程](https://github.com/kelthuzadx/EffectiveModernCppChinese/blob/master/7.The%20Concurrency%20API/item35.md) __由 @wendajiang贡献__ 2. Item 36:如果有异步的必要请指定std::launch::threads