现代模版元编程
前言模板元编程已成为 C++ 程序员工具包的重要组成部分。本文将学习并展示元编程工具和技术,并应用于每个标准库设施的代表实现。在此过程中,我们将研究 void_t,这是最近提出的一种极其简单的新 <type_traits> 候选项,一位专家将其描述为 “高度先进(且优雅),甚至对经验丰富的模板元编程人员来说都令人惊讶。”
注意:本文不适合 C++ 新手本文可能存在一些对计算机软件编程方法持有相当强烈的观点,这些观点不是所有程序员都度认同的。但他们应该确实如此
From the std:: library:
integral_constant, true_type, false_type;
is_same, is_void;
is_copy_assignable, is_move_assignable;
remove_const, remove_volatile, remove_cv;
conditional, enable_if;
is_intergal, is_floating_point;
Not int the std:: library:
abs, gcd ...
Item 35 优先基于任务编程而不是基于线程
前言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。
优先基于任务编程而不是基于线程如果开发者想要异步执⾏ doAsyncWork 函数,通常有两种⽅式。其⼀是通过创建 std::thread 执⾏ doAsyncWork, ⽐如
12int doAsyncWork( ...
Item 35 优先基于任务编程而不是基于线程
前言当你调⽤ std::async 执⾏函数时(或者其他可调⽤对象),你通常希望异步执⾏函数。但是这并不⼀定是你想要 std::async 执⾏的操作。你确实通过 std::async launch policy(译者注:这⾥没有翻译)要求执⾏函数,有两种标准policy,都通过 std::launch 域的枚举类型表⽰(参⻅ Item10 关于枚举的更多细节)。假定⼀个函数 f 传给 std::async 来执⾏:
std::launch::async 的 launch policy 意味着f必须异步执⾏,即在不同的线程
std::launch::deferred 的 launch policy 意味着f仅仅在当调⽤ get 或者 wait 要求 std::async 的返回值时才执⾏。这表⽰f推迟到被求值才延迟执⾏(译者注:异步与并发是两个不同概念,这⾥侧重于惰性求值)。当 get 或 wait 被调⽤,f 会同步执⾏,即调⽤⽅停⽌直到f运⾏结束。如果 get 和 wait 都没有被调⽤,f 将不会被执⾏
有趣的是,std::async 的默认 launch policy 是以 ...
Item 37 使 std::threads 在所有线程上不可连接路径
前言每个 std::thread 对象处于两个状态之⼀:joinable or unjoinable。joinable状态的 std::thread 对应于正在运⾏或者可能正在运⾏的异步执⾏线程。⽐如,⼀个blocked或者等待调度的 std::thread 是 joinable,已运⾏结束的 std::thread 也可以认为是 joinable.
unjoinable 的 std::thread 对象⽐如:
Default-constructed std::threads。这种 std::thread 没有函数执⾏,因此⽆法绑定到具体的线程上
已经被 moved 的 std::thread 对象。move 的结果就是将 std::thread 对应的线程所有权转移给另⼀个 std::thread
已经 joined 的 std::thread。在 join 之后,std::thread 执⾏结束,不再对应于具体的线程
已经 detached 的 std::thread。detach 断开了 std::thread 与线程之间的连接
(译者注:std::thread 可以视作状态 ...
Item 34 考虑lambda表达式而⾮std::bind
前言C++11 中的 std::bind 是 C++98 的 std::bind1st 和 std::bind 的后续,但在 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 返回的函数对象称为绑定对象。
优先 l ...
Item 33 对于 std::forward 的 auto&& 形参使⽤ decltype
泛型 lambda(generic lambdas) 是 C++14 中最值得期待的特性之⼀—,因为在 lambda 的参数中可以使⽤ auto 关键字。这个特性的实现是⾮常直截了当的:即在闭包类中的 operator() 函数是⼀个函数模版。例如存在这么⼀个 lambda:
1auto f = [](auto x){ return func(normalize(x)); };
对应的闭包类中的函数调用操作符看来就变成这样:
123456class SomeCompilerGeneratedClassName { public:template<typename T>auto operator()(T x) const{ return func(normalize(x)); }...};
在这个样例中,lambda 对变量 x 做的唯⼀⼀件事就是把它转发给函数 normalize。如果函数 normalize 对待左值右值的⽅式不⼀样,这个lambda的实现⽅式就不⼤合适了,因为即使传递到 lambda 的实参 ...
Item 32 使⽤初始化捕获来移动对象到闭包中
前言在某些场景下,按值捕获和按引用捕获都不是你所想要的。如果有一个只能被移动的对象(例如:std::unique_ptr 或 std::future)要进入到闭包里,使用 C++11 是无法实现的。如果你要复制的对象复制开销非常高,但移动的成本却不高(例如标准库中的⼤多数容器),并且你希望的是宁愿移动该对象到闭包而不是复制它。然而 C++11 却无法实现这一目标。
但如果你的编译器⽀持 C++14,那⼜是另⼀回事了,它能⽀持将对象移动道闭包中。如果你的兼容⽀持 C++14,那么请愉快地阅读下去。如果你仍然在使⽤仅⽀持 C++11 的编译器,也请愉快阅读,因为在 C++11 中有很多⽅法可以实现近似的移动捕获。
缺少移动捕获被认为是 C++11 的⼀个缺点,直接的补救措施是将该特性添加到 C++14 中,但标准化委员会选择了另⼀种⽅法。他们引⼊了⼀种新的捕获机制,该机制⾮常灵活,移动捕获是它执⾏的技术之⼀。新功能被称作初始化捕获,它⼏乎可以完成 C++11 捕获形式的所有⼯作,甚⾄能完成更多功能。默认的捕获模式使得你⽆法使⽤初始化捕获表⽰,但第31项说明提醒了你⽆论如何都应该远离这些捕获 ...
Item 31 避免使用默认捕获模式
lambdaLambda 表达式是 C++ 编程中的游戏规则改变者。这有点令⼈惊讶,因为它没有给语⾔带来新的表达能⼒。Lambda 可以做的所有事情都可以通过其他⽅式完成。但是 lambda 是创建函数对象相当便捷的⼀种⽅法,对于⽇常的 C++ 开发影响是巨⼤的。没有 lambda 时,标准库中的 _if 算法(⽐如,std::find_if, std::remove_if, std::count_if 等)通常需要繁琐的谓词,但是当有 lambda 可⽤时,这些算法使⽤起来就变得相当⽅便。⽐较函数(⽐如,std::sort, std::nth_element,std::lower_bound等)与算法函数也是相同的。在标准库外,lambda 可以快速创建 std::unique_ptr 和 std::shared_ptr 的⾃定义 deleter,并且使线程 API 中条件变量的条件设置变得同样简单(参⻅ Item 39)。除了标准库,lambda 有利于即时的回调函数,接口适配函数和特定上下⽂中的⼀次性函数。Lambda 确实使 C++ 成为更令⼈愉快的编程语⾔。
与 Lambda ...
Item 30 熟悉完美转发的失败 case
前言C++11 最显眼的功能之⼀就是完美转发功能。开始使⽤,你就发现“完美”,理想与现实还是有差距。C++11 的完美转发是⾮常好⽤,但是只有当你愿意忽略⼀些失败情况,这个章节就是使你熟悉这些情形。
在我们开始探索之前,有必要回顾⼀下“完美转发”的含义。
“转发”仅表⽰将⼀个函数的参数传递给另⼀个函数。对于被传递的第⼆个函数⽬标是收到与第⼀个函数完全相同的对象。这就排除了按值传递参数,因为它们是原始调⽤者传⼊内容的副本。我们希望被转发的函数能够可以与原始函数⼀起使⽤对象。指着参数也被排除在外,因为我们不想强迫调⽤者传⼊指针。关于通⽤转发,我们将处理引⽤参数。
完美转发意味着我们不仅转发对象,我们还转发显著的特征:它们的类型,是左值还是右值,是 const 还是 volatile。结合到我们会处理引⽤参数,这意味着我们将使⽤通⽤引⽤(参⻅Item24),因为通⽤引⽤参数被传⼊参数时才确定是左值还是右值
假定我们有⼀些函数f,然后想编写⼀个转发给它的函数(就使⽤⼀个函数模板)。我们需要的核⼼看起来像是这样:
12345template<typename T>void fwd ...
Item 29 认识移动操作的缺点
移动语义移动语义可以说是 C++11 最主要的特性。你可能会⻅过这些类似的描述“移动容器和拷⻉指针⼀样开销小”, “拷⻉临时对象现在如此⾼效,编码避免这种情况简直就是过早优化”这种情绪很容易理解。移动语义确实是这样重要的特性。它不仅允许编译器使⽤开销小的移动操作代替⼤开销的复制操作,而且默认这么做。以 C++98 的代码为基础,使⽤ C++11 重新编译你的代码,然后,哇,你的软件运⾏的更快了。移动语义确实令⼈振奋,但是有很多夸⼤的说法,这个 Item 的⽬的就是给你泼⼀瓢冷⽔,保持理智看待移动语义。
让我们从已知很多类型不⽀持移动操作开始这个过程。为了升级到 C++11,C++98 的很多标准库做了⼤修改,为很多类型提供了移动的能⼒,这些类型的移动实现⽐复制操作更快,并且对库的组件实现修改以利⽤移动操作。但是很有可能你⼯作中的代码没有完整地利⽤ C++11。对于你的应⽤中(或者代码库中),没有适配 C++11 的部分,编译器即使⽀持移动语义也是⽆能为⼒的。的确,C++11 倾向于为缺少移动操作定义的类默认⽣成,但是只有在没有声明复制操作,移动操作,或析构函数的类中才会⽣成移动操作(参 ...