Boost-C---1-53-0-应用开发秘籍-全-
Boost C++ 1.53.0 应用开发秘籍(全)
原文:
zh.annas-archive.org/md5/f4df6da4b736d419f0c90ccdbb326567
译者:飞龙
前言
几年前,我的一个朋友在寻找关于 Boost 库的书。我问她:“你为什么不看看文档呢?”她的回答是:“我对这些了解不多,也不知道从哪里开始。Boost 太庞大了;我没有时间去读所有关于它的内容。”
好吧,这是一个不错的提示,但这样的书只会对初学者感兴趣。除非我加入一些 C++11 的内容,并将现有的 Boost 库与新的 C++标准进行比较,专业人士在这本书中找不到什么有趣的内容。
我还可以添加一些在 Boost 邮件列表中常见但难以找到或未在文档中涵盖的问题的答案。再加上性能说明,我们就能得到一本几乎对每个人都有趣的书。
本书将带你通过一系列清晰、实用的食谱,帮助你利用一些现成的解决方案。
Boost C++应用程序开发食谱集从教授 Boost 库的基础知识开始,这些知识现在大多已成为 C++11 的一部分,并且不会出现内存泄漏。资源管理将变得轻而易举。我们将看到编译时可以完成哪些工作,以及 Boost 容器能做什么。你认为多线程是一个负担吗?用 Boost 就不是了。你认为编写可移植且快速的服务器是不可能的吗?你会大吃一惊!编译器和操作系统差异太大吗?用 Boost 就不是了。从处理图像到图、目录、计时器、文件和字符串——每个人都会找到一个有趣的主题。
你将学习开发高质量、快速和可移植应用程序所需的一切。编写一次程序,你就可以在 Linux、Windows、Mac OS 和 Android 操作系统上使用它。
本书涵盖的内容
第一章,开始编写应用程序,涵盖了日常使用的一些食谱。我们将看到如何从不同的来源获取配置选项,以及可以使用 Boost 库作者引入的一些数据类型制作什么。
第二章,数据转换,解释了如何将字符串、数字和用户定义的类型相互转换,如何安全地转换多态类型,以及如何在 C++源文件中编写小型和大型解析器。
第三章,资源管理,提供了轻松管理资源的指导,以及如何使用一种能够存储任何功能对象、函数和 lambda 表达式的数据类型。阅读本章后,你的代码将变得更加可靠,内存泄漏将成为历史。
第四章,编译时技巧,通过一些基本示例指导你了解如何使用 Boost 库进行编译时检查、调整算法以及其他元编程任务。
第五章,多线程,讨论了线程及其相关内容。
第六章,操作任务,解释了我们可以将所有处理、计算和交互分解为函数(任务),并几乎独立地处理每个任务。此外,我们不需要在诸如从套接字接收数据或等待超时等慢速操作上阻塞,而是提供回调任务并继续处理其他任务。
第七章,操作字符串,涵盖了更改、搜索和表示字符串的不同方面。我们将看到如何使用 Boost 库轻松完成一些常见的字符串相关任务。
第八章,元编程,致力于一些酷且难以理解元编程方法。这些方法并不适用于日常使用,但它们对于开发通用库将非常有帮助。
第九章,容器,涵盖了 Boost 容器和与之直接相关的一切。本章提供了有关可以在日常编程中使用并使您的代码运行得更快、开发新应用程序更简单的 Boost 类的信息。
第十章,收集平台和编译器信息,提供了用于检测编译器、平台和 Boost 功能的不同辅助宏。这些宏在 Boost 库中被广泛使用,对于编写能够与任何编译器标志一起工作的可移植代码至关重要。
第十一章,与系统协同工作,更深入地探讨了文件系统以及创建和删除文件。我们将看到数据如何在不同的系统进程之间传递,如何以最大速度读取文件,以及如何进行其他技巧。
第十二章,探索冰山一角,致力于一些大型库,提供了开始的基础知识。一些 Boost 库很小,适用于日常使用,而其他库则需要单独的书籍来描述它们的所有功能。
本书所需内容
要运行本书中的示例,需要以下软件:
-
C++ 编译器:任何现代、流行的 C++ 编译器都适用。
-
集成开发环境(IDE):推荐使用 QtCreator 作为 IDE。
-
Boost:您应该有一个完整的 Boost 1.53 构建。
-
杂项工具:Graphviz(任何版本)和 libpng(最新版本)
注意,如果你使用的是 Linux 系统,除了 Boost 以外,所有必需的软件都可以在软件仓库中找到。
本书面向对象
这本书非常适合新接触 Boost 的开发者,他们希望提高对 Boost 的了解,并查看一些未记录的细节或技巧。假设您已经具备一些 C++经验,并且熟悉 STL 的基础知识。几章内容将需要一些关于多线程和网络的前期知识。您至少需要有一个好的 C++编译器和 Boost(推荐 1.53.0 或更高版本)的编译版本,这些将在本书的练习中使用。
规范
在这本书中,您将找到多种文本样式,用于区分不同类型的信息。以下是一些这些样式的示例,以及它们的意义解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名将如下所示:“这意味着您可以使用catch (const std::exception& e)
捕获几乎所有的 Boost 异常。”
代码块设置如下:
#include <boost/variant.hpp>
#include <iostream>
#include <vector>
#include <string>
int main()
{
typedef boost::variant<int, const char*, std::string> my_var_t;
std::vector<my_var_t> some_values;
some_values.push_back(10);
some_values.push_back("Hello there!");
some_values.push_back(std::string("Wow!"));
std::string& s = boost::get<std::string>(some_values.back());
s += " That is great!\n";
std::cout << s;
return 0;
}
新术语和重要词汇将以粗体显示。
注意
警告或重要注意事项将以如下框中的形式出现。
小贴士
小技巧和技巧将以如下形式出现。
读者反馈
我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢什么或可能不喜欢什么。读者反馈对我们开发您真正能从中获得最大收益的标题非常重要。
要发送一般性反馈,请简单地将电子邮件发送到<feedback@packtpub.com>
,并在邮件主题中提及书名。
如果您在某个主题领域有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
既然您已经自豪地拥有了一本 Packt 图书,我们有许多事情可以帮助您充分利用您的购买。
下载示例代码
您可以从您在www.packtpub.com
的账户下载您购买的所有 Packt 图书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support
并注册,以便将文件直接通过电子邮件发送给您。
错误清单
尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问 www.packtpub.com/submit-errata
,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站,或添加到该标题的现有勘误列表中,在“勘误”部分下。您可以通过选择您的标题从 www.packtpub.com/support
查看任何现有勘误。
盗版
在互联网上,版权材料的盗版问题是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过 <copyright@packtpub.com>
联系我们,并提供涉嫌盗版材料的链接。
我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面的帮助。
问题
如果您在本书的任何方面遇到问题,可以通过 <questions@packtpub.com>
联系我们,我们将尽力解决。
第一章. 开始编写你的应用程序
在本章中,我们将涵盖以下内容:
-
获取配置选项
-
在容器/变量中存储任何值
-
在容器/变量中存储多个选定的类型
-
使用一种更安全的方式来处理存储多个选定类型的数据容器
-
在没有值的情况下返回一个值或标志
-
从函数中返回一个数组
-
将多个值合并为一个
-
重新排序函数的参数
-
将值绑定为一个函数参数
-
使用 C++11 移动模拟
-
创建一个不可复制的类
-
创建一个不可复制但可移动的类
简介
Boost 是一组 C++ 库。每个库在被接受到 Boost 之前都经过了众多专业程序员的审查。库在多个平台上使用多个编译器和 C++ 标准库实现进行测试。在使用 Boost 时,你可以确信你正在使用一个最可移植、快速和可靠的解决方案,该解决方案适用于商业和开源项目。
Boost 的许多部分已经被包含在 C++11 中,甚至更多部分将被包含在下一个 C++ 标准中。你将在本书的每个菜谱中找到 C++11 特定的说明。
不进行长篇大论,让我们开始吧!
在本章中,我们将看到一些日常使用的菜谱。我们将了解如何从不同的来源获取配置选项,以及可以使用 Boost 库作者引入的一些数据类型制作什么。
获取配置选项
看一看一些控制台程序,例如 Linux 中的 cp
。它们都有花哨的帮助,它们的输入参数不依赖于任何位置,并且具有人类可读的语法,例如:
$ cp --help
Usage: cp [OPTION]... [-T] SOURCE DEST
-a, --archive same as -dR --preserve=all
-b like --backup but does not accept an argument
你可以在 10 分钟内为你的程序实现相同的功能。你所需要的只是 Boost.ProgramOptions
库。
准备工作
对于这个菜谱,你需要具备基本的 C++ 知识。记住,这个库不是仅头文件,所以你的程序需要链接到 libboost_program_options
库。
如何做到这一点...
让我们从一个小程序开始,该程序接受苹果和橙子的数量作为输入并计算水果的总数。我们希望达到以下结果:
$ our_program –apples=10 –oranges=20
Fruits count: 30
执行以下步骤:
-
首先,我们需要包含
program_options
头文件并为boost::program_options
命名空间创建一个别名(它太长了,难以输入!)我们还需要一个<iostream>
头文件:#include <boost/program_options.hpp> #include <iostream> namespace opt = boost::program_options;
-
现在,我们已经准备好描述我们的选项:
// Constructing an options describing variable and giving // it a textual description "All options" to it. opt::options_description desc("All options"); // When we are adding options, first parameter is a name // to be used in command line. Second parameter is a type // of that option, wrapped in value<> class. // Third parameter must be a short description of that // option desc.add_options() ("apples", opt::value<int>(), "how many apples do you have") ("oranges", opt::value<int>(), "how many oranges do you have") ;
-
我们将在稍后一点时间看到如何使用第三个参数,之后我们将处理解析命令行和输出结果:
// Variable to store our command line arguments opt::variables_map vm; // Parsing and storing arguments opt::store(opt::parse_command_line(argc, argv, desc), vm); opt::notify(vm); std::cout << "Fruits count: " << vm["apples"].as<int>() + vm["oranges"].as<int>() << std::endl;
这很简单,不是吗?
-
让我们向我们的选项描述中添加
--help
参数:("help", "produce help message")
-
现在在
opt::notify(vm);
之后添加以下行,你将为你的程序获得一个完全功能性的帮助:if (vm.count("help")) { std::cout << desc << "\n"; return 1; }
现在,如果我们用
--help
参数调用我们的程序,我们将得到以下输出:All options: --apples arg how many apples do you have --oranges arg how many oranges do you have --help produce help message
如你所见,我们没有为选项的值提供类型,因为我们不期望任何值传递给它。
-
一旦我们掌握了所有基础知识,让我们为一些选项添加简短名称,为苹果设置默认值,添加一些字符串输入,并从配置文件中获取缺失的选项:
#include <boost/program_options.hpp> // 'reading_file' exception class is declared in errors.hpp #include <boost/program_options/errors.hpp> #include <iostream> namespace opt = boost::program_options; int main(int argc, char *argv[]) { opt::options_description desc("All options"); // 'a' and 'o' are short option names for apples and // oranges 'name' option is not marked with // 'required()', so user may not support it desc.add_options() ("apples,a", opt::value<int>()->default_value(10), "apples that you have") ("oranges,o", opt::value<int>(), "oranges that you have") ("name", opt::value<std::string>(), "your name") ("help", "produce help message") ; opt::variables_map vm; // Parsing command line options and storing values to 'vm' opt::store(opt::parse_command_line(argc, argv, desc), vm); // We can also parse environment variables using // 'parse_environment' method opt::notify(vm); if (vm.count("help")) { std::cout << desc << "\n"; return 1; } // Adding missing options from "aples_oranges.cfg" // config file. // You can also provide an istreamable object as a // first parameter for 'parse_config_file' // 'char' template parameter will be passed to // underlying std::basic_istream object try { opt::store( opt::parse_config_file<char>("apples_oranges.cfg", desc), vm ); } catch (const opt::reading_file& e) { std::cout << "Failed to open file 'apples_oranges.cfg': " << e.what(); } opt::notify(vm); if (vm.count("name")) { std::cout << "Hi," << vm["name"].as<std::string>() << "!\n"; } std::cout << "Fruits count: " << vm["apples"].as<int>() + vm["oranges"].as<int>() << std::endl; return 0; }
注意
当使用配置文件时,我们需要记住,其语法与命令行语法不同。我们不需要在选项前放置减号。因此,我们的
apples_oranges.cfg
选项必须看起来像这样:oranges=20
它是如何工作的...
从代码和注释中理解这个例子非常简单。更有趣的是我们在执行时得到的结果:
$ ./our_program --help
All options:
-a [ --apples ] arg (=10) how many apples do you have
-o [ --oranges ] arg how many oranges do you have
--name arg your name
--help produce help message
$ ./our_program
Fruits count: 30
$ ./our_program -a 10 -o 10 --name="Reader"
Hi,Reader!
Fruits count: 20
还有更多...
C++11 标准采用了许多 Boost 库;然而,你不会在其中找到Boost.ProgramOptions
。
相关内容
-
Boost 的官方文档包含更多示例,并展示了
Boost.ProgramOptions
的更多高级特性,如位置相关的选项、非常规语法等。这可以在以下链接中找到:
小贴士
下载示例代码
你可以从你购买的所有 Packt 书籍的账户中下载示例代码文件。www.PacktPub.com
。如果你在其他地方购买了这本书,你可以访问www.PacktPub.com/support
并注册以直接将文件通过电子邮件发送给你。
在容器/变量中存储任何值
如果你一直在使用 Java、C#或 Delphi 编程,你肯定会怀念在 C++中创建具有Object
值类型的容器的能力。在这些语言中,Object
类是几乎所有类型的基本类,因此你可以在任何时候将其分配给(几乎)任何值。想象一下,如果 C++有这样一个特性会多么好:
#include <iostream>
#include <vector>
#include <string>
#include <auto_ptr.h>
int main()
{
typedef std::auto_ptr<Object> object_ptr;
std::vector<object_ptr> some_values;
some_values.push_back(new Object(10));
some_values.push_back(new Object("Hello there"));
some_values.push_back(new Object(std::string("Wow!")));
std::string* p =
dynamic_cast<std::string*>(some_values.back().get());
assert(p);
(*p) += " That is great!\n";
std::cout << *p;
return 0;
}
准备工作
我们将使用仅包含头文件的库。对于这个配方,你只需要具备基本的 C++知识。
如何实现...
在这种情况下,Boost 提供了一个解决方案,即Boost.Any
库,它具有更好的语法:
#include <boost/any.hpp>
#include <iostream>
#include <vector>
#include <string>
int main()
{
std::vector<boost::any> some_values;
some_values.push_back(10);
const char* c_str = "Hello there!";
some_values.push_back(c_str);
some_values.push_back(std::string("Wow!"));
std::string& s =
boost::any_cast<std::string&>(some_values.back());
s += " That is great!\n";
std::cout << s;
return 0;
}
太棒了,不是吗?顺便说一下,它有一个空状态,可以使用empty()
成员函数进行检查(就像在 STL 容器中一样)。
你可以使用两种方法从boost::any
获取值:
boost::any variable(std::string("Hello world!"));
//#1: Following method may throw a boost::bad_any_cast exception
// if actual value in variable is not a std::string
std::string s1 = boost::any_cast<std::string>(variable);
//#2: If actual value in variable is not a std::string
// will return an NULL pointer
std::string* s2 = boost::any_cast<std::string>(&variable);
它是如何工作的...
boost::any
类只是存储任何值。为了实现这一点,它使用类型擦除技术(类似于 Java 或 C#对其所有类型所做的那样)。要使用这个库,您实际上并不需要了解其内部实现,所以我们只需快速浏览一下类型擦除技术。当对类型为T
的某个变量进行赋值时,Boost.Any
构造一个类型(让我们称它为holder<T>
),它可以存储指定类型T
的值,并且是从某个内部基类型占位符派生出来的。占位符有用于获取存储类型的std::type_info
的虚拟函数和用于克隆存储类型的虚拟函数。当使用any_cast<T>()
时,boost::any
检查存储值的std::type_info
是否等于typeid(T)
(使用重载的占位符函数来获取std::type_info
)。
还有更多...
这样的灵活性从来都不是没有代价的。复制构造、值构造、复制赋值以及将值赋给boost::any
的实例将调用动态内存分配函数;所有的类型转换都需要获取运行时类型信息(RTTI);boost::any
大量使用虚拟函数。如果您对性能很感兴趣,请看下一个菜谱,它将给您一个在没有动态分配和 RTTI 使用的情况下实现几乎相同结果的想法。
Boost.Any
的另一个缺点是它不能与禁用 RTTI 一起使用。有可能使这个库即使在禁用 RTTI 的情况下也能使用,但目前还没有实现。
注意
几乎所有的 Boost 异常都源自std::exception
类或其派生类,例如,boost::bad_any_cast
是从std::bad_cast
派生出来的。这意味着您可以使用catch (const std::exception& e)
捕获几乎所有的 Boost 异常。
参见
-
Boost 的官方文档可能给您一些更多的例子,您可以在
www.boost.org/doc/libs/1_53_0/doc/html/any.html
找到它。 -
查看有关“使用更安全的方式来处理存储多个选定类型的容器”菜谱以获取更多关于该主题的信息
在变量/容器中存储多个选定的类型
您是否了解 C++11 中无限制联合的概念?让我简要地告诉您。C++03 联合只能存储称为 POD(普通旧数据)的非常简单的数据类型。因此,在 C++03 中,您不能在联合中存储std::string
或std::vector
。C++11 放宽了这一要求,但您必须自己管理这些类型的构造和析构,调用就地构造/析构,并记住存储在联合中的类型。这是一项巨大的工作,不是吗?
准备工作
我们将使用仅包含头文件的库进行工作,这个库使用起来很简单。您只需要具备基本的 C++知识就可以使用这个菜谱。
如何做到...
让我来向您介绍Boost.Variant
库。
-
Boost.Variant
库可以存储编译时指定的任何类型;它还管理就地构造/销毁,甚至不需要 C++11 标准:#include <boost/variant.hpp> #include <iostream> #include <vector> #include <string> int main() { typedef boost::variant<int, const char*, std::string> my_var_t; std::vector<my_var_t> some_values; some_values.push_back(10); some_values.push_back("Hello there!"); some_values.push_back(std::string("Wow!")); std::string& s = boost::get<std::string>(some_values.back()); s += " That is great!\n"; std::cout << s; return 0; }
太棒了,不是吗?
-
Boost.Variant
没有空状态,但它有一个empty()
函数,该函数始终返回false
。如果你确实需要表示一个空状态,只需在Boost.Variant
库支持的类型中的第一个位置添加一些平凡类型。当Boost.Variant
包含该类型时,将其解释为空状态。以下是一个示例,我们将使用boost::blank
类型来表示空状态:typedef boost::variant<boost::blank, int, const char*, std::string> my_var_t; // Default constructor will construct an // instance of boost::blank my_var_t var; // 'which()' method returns an index of a type, // currently held by variant. assert(var.which() == 0); // Empty state var = "Hello, dear reader"; assert(var.which() != 0);
-
你可以使用两种方法从变体中获取值:
boost::variant<int, std::string> variable(0); // Following method may throw a boost::bad_get // exception if actual value in variable is not an int int s1 = boost::get<int>(variable); // If actual value in variable is not an int // will return an NULL pointer int* s2 = boost::get<int>(&variable);
它是如何工作的...
boost::variant
类持有一个字符数组,并在该数组中存储值。数组的大小在编译时使用 sizeof()
和获取对齐的函数确定。在赋值或构造 boost::variant
时,之前的值就地销毁,并在字符数组上使用新的放置构造新值。
还有更多...
Boost.Variant
变量通常不会在堆上分配内存,并且它们不需要启用 RTTI。Boost.Variant
非常快,并且被其他 Boost 库广泛使用。为了达到最佳性能,请确保支持类型列表中有一个平凡类型,并且该类型位于第一个位置。
注意
Boost.Variant
不是 C++11 标准的一部分。
参见
-
使用更安全的方式来处理存储多个选定类型的容器 菜谱
-
Boost 的官方文档包含了更多关于
Boost.Variant
的示例和一些其他特性的描述,可以在以下位置找到:
使用更安全的方式来处理存储多个选定类型的容器
想象一下,你正在创建一个围绕某些 SQL 数据库接口的包装器。你决定 boost::any
将完美地满足数据库表单单元格的要求。其他程序员将使用你的类,他的任务是从数据库中获取一行并计算行中算术类型的总和。
这就是代码的样貌:
#include <boost/any.hpp>
#include <vector>
#include <string>
#include <typeinfo>
#include <algorithm>
#include <iostream>
// This typedefs and methods will be in our header,
// that wraps around native SQL interface
typedef boost::any cell_t;
typedef std::vector<cell_t> db_row_t;
// This is just an example, no actual work with database.
db_row_t get_row(const char* /*query*/) {
// In real application 'query' parameter shall have a 'const
// char*' or 'const std::string&' type? See recipe Using a
// reference to string type in Chapter 7, Manipulating Strings
// for an answer.
db_row_t row;
row.push_back(10);
row.push_back(10.1f);
row.push_back(std::string("hello again"));
return row;
}
// This is how a user will use your classes
struct db_sum: public std::unary_function<boost::any, void> {
private:
double& sum_;
public:
explicit db_sum(double& sum)
: sum_(sum)
{}
void operator()(const cell_t& value) {
const std::type_info& ti = value.type();
if (ti == typeid(int)) {
sum_ += boost::any_cast<int>(value);
} else if (ti == typeid(float)) {
sum_ += boost::any_cast<float>(value);
}
}
};
int main()
{
db_row_t row = get_row("Query: Give me some row, please.");
double res = 0.0;
std::for_each(row.begin(), row.end(), db_sum(res));
std::cout << "Sum of arithmetic types in database row is: " << res << std::endl;
return 0;
}
如果你编译并运行这个示例,它将输出正确的结果:
Sum of arithmetic types in database row is: 20.1
你还记得阅读 operator()
的实现时你的想法吗?我猜它们是,“那么 double、long、short、unsigned 以及其他类型怎么办?”。同样的想法也会出现在使用你的接口的程序员心中。所以你需要仔细记录你的 cell_t
存储的值,或者阅读以下章节中描述的更优雅的解决方案。
准备工作
如果你还不熟悉 Boost.Variant
和 Boost.Any
库,强烈建议你阅读前面的两个菜谱。
如何做到...
Boost.Variant
库实现了一种访问存储数据的访问者编程模式,这比通过 boost::get<>
获取值要安全得多。这种模式迫使程序员注意每个变体类型,否则代码将无法编译。您可以通过 boost::apply_visitor
函数使用此模式,该函数将访问者功能对象作为第一个参数,将变体作为第二个参数。访问者功能对象必须从 boost::static_visitor<T>
类派生,其中 T
是访问者返回的类型。访问者对象必须为变体存储的每个类型重载 operator()
。
让我们将 cell_t
类型更改为 boost::variant<int, float, string>
并修改我们的示例:
#include <boost/variant.hpp>
#include <vector>
#include <string>
#include <iostream>
// This typedefs and methods will be in header,
// that wraps around native SQL interface.
typedef boost::variant<int, float, std::string> cell_t;
typedef std::vector<cell_t> db_row_t;
// This is just an example, no actual work with database.
db_row_t get_row(const char* /*query*/) {
// See the recipe "Using a reference to string type"
// in Chapter 7, Manipulating Strings
// for a better type for 'query' parameter.
db_row_t row;
row.push_back(10);
row.push_back(10.1f);
row.push_back("hello again");
return row;
}
// This is how code required to sum values
// We can provide no template parameter
// to boost::static_visitor<> if our visitor returns nothing.
struct db_sum_visitor: public boost::static_visitor<double> {
double operator()(int value) const {
return value;
}
double operator()(float value) const {
return value;
}
double operator()(const std::string& /*value*/) const {
return 0.0;
}
};
int main()
{
db_row_t row = get_row("Query: Give me some row, please.");
double res = 0.0;
db_row_t::const_iterator it = row.begin(), end = row.end();
for (; it != end; ++it) {
res += boost::apply_visitor(db_sum_visitor(), *it);
}
std::cout << "Sum of arithmetic types in database row is: " << res << std::endl;
return 0;
}
它是如何工作的...
Boost.Variant
库将在编译时生成一个大的 switch
语句,每个 case
都将调用变体类型列表中的单个类型的访问者。在运行时,可以使用 which()
获取存储类型的索引,并跳转到 switch
语句中的正确 case
。对于 boost::variant<int, float, std::string>
,将生成类似以下的内容:
switch (which())
{
case 0: return visitor(*reinterpret_cast<int*>(address()));
case 1: return visitor(*reinterpret_cast<float*>(address()));
case 2: return visitor(*reinterpret_cast<std::string*>(address()));
default: assert(false);
}
在这里,address()
函数返回 boost::variant<int, float, std::string>
的内部存储指针。
还有更多...
如果我们将此示例与配方中的第一个示例进行比较,我们将看到 boost::variant
的以下优点:
-
我们知道一个变量可以存储哪些类型
-
如果 SQL 接口库的编写者向变体中添加或修改类型,我们将得到编译时错误而不是不正确的行为。
参见
-
在阅读了 第四章 中的部分内容后,编译时技巧,您将能够使访问者对象如此通用,即使底层类型发生变化,它也能正确工作。
-
Boost 的官方文档包含了更多示例和
Boost.Variant
的某些其他特性的描述,可在以下链接找到:
在没有值的情况下返回值或标志
假设我们有一个不抛出异常并返回值或指示发生错误的函数。在 Java 或 C# 编程语言中,这些情况通过比较函数返回值与空指针来处理;如果是空指针,则表示发生了错误。在 C++ 中,从函数返回指针会混淆库用户,并且通常需要动态内存分配(这很慢)。
准备工作
此配方只需要基本的 C++ 知识。
如何实现...
女士们,先生们,让我通过以下示例向您介绍 Boost.Optional
库:
try_lock_device()
函数尝试为设备获取锁,可能成功也可能不成功,这取决于不同的条件(在我们的例子中取决于rand()
函数调用)。该函数返回一个可选变量,可以转换为布尔变量。如果返回值等于布尔true
,则已获取锁,可以通过解引用返回的可选变量来获取用于处理设备的类的实例:
#include <boost/optional.hpp>
#include <iostream>
#include <stdlib.h>
class locked_device {
explicit locked_device(const char* /*param*/) {
// We have unique access to device
std::cout << "Device is locked\n";
}
public:
~locked_device () {
// Releasing device lock
}
void use() {
std::cout << "Success!\n";
}
static boost::optional<locked_device> try_lock_device() {
if (rand()%2) {
// Failed to lock device
return boost::none;
}
// Success!
return locked_device("device name");
}
};
int main()
{
// Boost has a library called Random. If you wonder why it was
// written when stdlib.h has rand() function, see the recipe
// "Using a true random number generator in Chapter 12,
// Scratching the Tip of the Iceberg
srandom(5);
for (unsigned i = 0; i < 10; ++i) {
boost::optional<locked_device> t = locked_device::try_lock_device();
// optional is convertible to bool
if (t) {
t->use();
return 0;
} else {
std::cout << "...trying again\n";
}
}
std::cout << "Failure!\n";
return -1;
}
这个程序将输出以下内容:
...trying again
...trying again
Device is locked
Success!
注意
默认构造的optional
变量可以转换为持有false
的布尔变量,并且不得解引用,因为它没有构造的底层类型。
它是如何工作的...
Boost.Optional
类与boost::variant
类非常相似,但只针对一种类型,boost::optional<T>
有一个chars
数组,其中类型为T
的对象可以是一个就地构造器。它还有一个布尔变量来记住对象的状态(是否已构造)。
还有更多...
Boost.Optional
类不使用动态分配,并且不需要底层类型的默认构造函数。它速度快,被认为将被纳入 C++的下一个标准。当前的boost::optional
实现不能与 C++11 右值引用一起工作;然而,已经提出了一些补丁来修复这个问题。
C++11 标准不包括Boost.Optional
类;然而,它目前正在被审查,以纳入下一个 C++标准或 C++14。
参见
-
Boost 的官方文档包含更多示例,并描述了
Boost.Optional
的先进特性(如使用工厂函数的就地构造)。文档可在以下链接找到:www.boost.org/doc/libs/1_53_0/libs/optional/doc/html/index.html
从函数返回数组
让我们玩一个猜谜游戏!你能从以下函数中了解到什么?
char* vector_advance(char* val);
应该由程序员来释放返回值吗?函数是否尝试释放输入参数?输入参数应该是以零结尾,还是函数应该假设输入参数具有指定的宽度?
现在,让我们使任务更难!看看以下行:
char ( &vector_advance( char (&val)[4] ) )[4];
请不要担心;在得到这里发生的事情的想法之前,我也一直在挠头半小时。vector_advance
是一个接受并返回四个元素数组的函数。有没有办法清楚地写出这样的函数?
准备工作
本食谱只需要基本的 C++知识。
如何做到...
我们可以像这样重写函数:
#include <boost/array.hpp>
typedef boost::array<char, 4> array4_t;array4_t& vector_advance(array4_t& val);
在这里,boost::array<char, 4>
只是围绕四个字符元素的数组的一个简单包装器。
这段代码回答了我们第一个示例中的所有问题,并且比第二个示例更易于阅读。
它是如何工作的...
boost::array
的第一个模板参数是元素类型,第二个是数组的大小。boost::array
是一个固定大小的数组;如果需要在运行时更改数组大小,请使用 std::vector
或 boost::container::vector
。
Boost.Array
库只包含一个数组。仅此而已。简单且高效。boost::array<>
类没有手写的构造函数,并且所有成员都是公共的,因此编译器会将其视为 POD 类型。
还有更多...
让我们看看 boost::array
的一些更多使用示例:
#include <boost/array.hpp>
#include <algorithm>
// Functional object to increment value by one
struct add_1 : public std::unary_function<char, void> {
void operator()(char& c) const {
++ c;
}
// If you're not in a mood to write functional objects,
// but don't know what does 'boost::bind(std::plus<char>(),
// _1, 1)' do, then read recipe 'Binding a value as a function
// parameter'.
};
typedef boost::array<char, 4> array4_t;
array4_t& vector_advance(array4_t& val) {
// boost::array has begin(), cbegin(), end(), cend(),
// rbegin(), size(), empty() and other functions that are
// common for STL containers.
std::for_each(val.begin(), val.end(), add_1());
return val;
}
int main() {
// We can initialize boost::array just like an array in C++11:
// array4_t val = {0, 1, 2, 3};
// but in C++03 additional pair of curly brackets is required.
array4_t val = {{0, 1, 2, 3}};
// boost::array works like a usual array:
array4_t val_res; // it can be default constructible and
val_res = vector_advance(val); // assignable
// if value type supports default construction and assignment
assert(val.size() == 4);
assert(val[0] == 1);
/*val[4];*/ // Will trigger an assert because max index is 3
// We can make this assert work at compile-time.
// Interested? See recipe 'Checking sizes at compile time'
// in Chapter 4, Compile-time Tricks.'
assert(sizeof(val) == sizeof(char) * array4_t::static_size);
return 0;
}
boost::array
的最大优点之一是它提供了与普通 C 数组完全相同的性能。C++ 标准委员会的人也喜欢它,所以它被纳入了 C++11 标准。有可能你的 STL 库已经包含了它(你可以尝试包含 <array>
头文件并检查 std::array<>
的可用性)。
参见
-
Boost 的官方文档提供了
Boost.Array
方法的完整列表,包括方法的复杂性和抛出行为描述,并可在以下链接找到: -
boost::array
函数在各个食谱中广泛使用;例如,可以参考 将值作为函数参数绑定 的食谱。
将多个值组合成一个
对于喜欢 std::pair
的人来说,有一个非常好的礼物。Boost 有一个名为 Boost.Tuple
的库,它就像 std::pair
一样,但它还可以与三元组、四元组以及更大的类型集合一起工作。
准备工作
本食谱只需要对 C++ 和 STL 有基本了解。
如何做到这一点...
执行以下步骤以将多个值组合成一个:
-
要开始使用元组,你需要包含适当的头文件并声明一个变量:
#include <boost/tuple/tuple.hpp> #include <string> boost::tuple<int, std::string> almost_a_pair(10, "Hello"); boost::tuple<int, float, double, int> quad(10, 1.0f, 10.0, 1);
-
通过
boost::get<N>()
函数实现获取特定值,其中N
是所需值的零基于索引:int i = boost::get<0>(almost_a_pair); const std::string& str = boost::get<1>(almost_a_pair); double d = boost::get<2>(quad);
boost::get<>
函数有许多重载,并在 Boost 中广泛使用。我们已经在 在容器/变量中存储多个选择类型 的食谱中看到了它是如何与其他库一起使用的。 -
你可以使用
boost::make_tuple()
函数来构造元组,这比完全限定元组类型要短,因为你不需要完全限定元组类型:using namespace boost; // Tuple comparison operators are // defined in header "boost/tuple/tuple_comparison.hpp" // Don't forget to include it! std::set<tuple<int, double, int> > s; s.insert(make_tuple(1, 1.0, 2)); s.insert(make_tuple(2, 10.0, 2)); s.insert(make_tuple(3, 100.0, 2)); // Requires C++11 auto t = make_tuple(0, -1.0, 2); assert(2 == get<2>(t)); // We can make a compile-time assert for type // of t. Interested? See chapter 'compile-time tricks'
-
另一个让生活变得容易的函数是
boost::tie()
。它几乎与make_tuple
一样工作,但为每个传递的类型添加了一个非 const 引用。这样的元组可以用来从另一个元组获取值。以下示例可以更好地理解它:boost::tuple<int, float, double, int> quad(10, 1.0f, 10.0, 1); int i; float f; double d; int i2; // Passing values from 'quad' variables // to variables 'i', 'f', 'd', 'i2' boost::tie(i, f, d, i2) = quad; assert(i == 10); assert(i2 == 1);
工作原理...
一些读者可能会 wonder 为什么我们需要元组,因为我们总是可以编写自己的结构体,例如,而不是编写 boost::tuple<int, std::string>
,我们可以创建一个结构体:
struct id_name_pair {
int id;
std::string name;
};
好吧,这个结构肯定比 boost::tuple<int, std::string>
更清晰。但假设这个结构在代码中只使用两次呢?
元组库背后的主要思想是简化模板编程。
还有更多...
一个元组的工作速度与 std::pair
相当(它不在堆上分配内存,也没有虚拟函数)。C++ 委员会认为这个类非常有用,并将其包含在 STL 中;你可以在 C++11 兼容的 STL 实现的 <tuple>
头文件中找到它(别忘了将所有 boost::
命名空间替换为 std::
)。
当前 Boost 的元组实现不使用变长模板;它只是由脚本生成的一组类。有一个实验版本使用 C++11 的右值和 C++03 编译器的它们仿真,所以有可能 Boost 1.54 将包含更快的元组实现。
参见
-
可以在以下链接找到元组的实验版本:
-
Boost 的官方文档包含了更多示例、关于性能和
Boost.Tuple
能力的信息。它可在以下链接找到:www.boost.org/doc/libs/1_53_0/libs/tuple/doc/tuple_users_guide.html
-
在 第八章 的 元编程 中,将所有元组元素转换为字符串 的菜谱展示了元组的某些高级用法
重新排序函数的参数
本菜谱和下一菜谱致力于一个非常有趣的库,其功能乍一看像某种魔法。这个库被称为 Boost.Bind
,它允许您轻松地从函数、成员函数和功能对象创建新的功能对象,同时也允许重新排序初始函数的输入参数,并将某些值或引用绑定为函数参数。
准备工作
此菜谱需要具备 C++、STL 算法和功能对象的了解。
如何做到这一点...
-
让我们从一个例子开始。你正在使用某个程序员提供的整数类型向量。这个整数类型只有一个操作符
+
,但你的任务是乘以一个值。没有bind
,这可以通过使用功能对象来实现:class Number{}; inline Number operator + (Number, Number); // Your code starts here struct mul_2_func_obj: public std::unary_function<Number, Number> { Number operator()(Number n1) const { return n1 + n1; } }; void mul_2_impl1(std::vector<Number>& values) { std::for_each(values.begin(), values.end(), mul_2_func_obj()); }
使用
Boost.Bind
,可以这样:#include <boost/bind.hpp> #include <functional> void mul_2_impl2(std::vector<Number>& values) { std::for_each(values.begin(), values.end(), boost::bind(std::plus<Number>(), _1, _1)); }
-
顺便说一下,我们可以轻松地使这个函数更通用:
template <class T> void mul_2_impl3(std::vector<T>& values) { std::for_each(values.begin(), values.end(), boost::bind(std::plus<T>(), _1, _1)); }
如何工作...
让我们更仔细地看看 mul_2
函数。我们向它提供一个值的向量,并为每个值应用 bind()
函数返回的函数对象。bind()
函数接受三个参数;第一个参数是 std::plus<Number>
类的实例(它是一个函数对象)。第二个和第三个参数是占位符。占位符 _1
用结果函数对象的第一个输入参数替换参数。正如你可能猜到的,有许多占位符;占位符 _2
表示用结果函数对象的第二个输入参数替换参数,同样也适用于占位符 _3
。嗯,看来你已经明白了这个概念。
还有更多...
为了确保你完全理解并知道 bind 可以在哪里使用,让我们看看另一个例子。
我们有两个类,它们与一些传感器设备一起工作。这些设备和类来自不同的供应商,因此它们提供了不同的 API。这两个类只有一个公共方法 watch
,它接受一个函数对象:
class Device1 {
private:
short temperature();
short wetness();
int illumination();
int atmospheric_pressure();
void wait_for_data();
public:
template <class FuncT>
void watch(const FuncT& f) {
for(;;) {
wait_for_data();
f(
temperature(),
wetness(),
illumination(),
atmospheric_pressure()
);
}
}
};
class Device2 {
private:
short temperature();
short wetness();
int illumination();
int atmospheric_pressure();
void wait_for_data();
public:
template <class FuncT>
void watch(const FuncT& f) {
for(;;) {
wait_for_data();
f(
wetness(),
temperature(),
atmospheric_pressure(),
illumination()
);
}
}
};
Device1::watch
和 Device2::watch
函数以不同的顺序将值传递给函数对象。
一些其他库提供了一个用于检测风暴的函数,当风暴风险足够高时,它会抛出一个异常:
void detect_storm(int wetness, int temperature, int atmospheric_pressure);
你的任务是为这两个设备提供一个风暴检测函数。以下是使用 bind
函数实现的方法:
Device1 d1;
// resulting functional object will silently ignore
// additional parameters passed to function call
d1.watch(boost::bind(&detect_storm, _2, _1, _4));
...
Device2 d2;
d2.watch(boost::bind(&detect_storm, _1, _2, _3));
Boost.Bind
库提供了良好的性能,因为它不使用动态分配和虚函数。即使 C++11 的 lambda 函数不可用,它也非常有用:
template <class FuncT>
void watch(const FuncT& f) {
f(10, std::string("String"));
f(10, "Char array");
f(10, 10);
}
struct templated_foo {
template <class T>
void operator()(T, int) const {
// No implementation, just showing that bound
// functions still can be used as templated
}
};
void check_templated_bind() {
// We can directly specify return type of a functional object
// when bind fails to do so
watch(boost::bind<void>(templated_foo(), _2, _1));
}
Bind 是 C++11 标准的一部分。它在 <functional>
头文件中定义,并且可能与 Boost.Bind
实现略有不同(然而,它至少与 Boost 的实现一样有效)。
参考以下内容
-
“将值绑定为函数参数”食谱对
Boost.Bind
的特性有更多介绍 -
Boost 的官方文档包含更多示例和高级特性的描述。它可在以下链接找到:
将值绑定为函数参数
如果你经常与 STL 库一起工作并使用 <algorithm>
头文件,你肯定会写很多函数对象。你可以使用一组 STL 适配器函数(如 bind1st
、bind2nd
、ptr_fun
、mem_fun
和 mem_fun_ref
)来构建它们,或者你可以手动编写它们(因为适配器函数看起来很吓人)。这里有一些好消息:Boost.Bind
可以替代所有这些函数,并提供更易读的语法。
准备工作
阅读前面的食谱以了解占位符的概念,或者确保你熟悉 C++11 占位符。了解 STL 函数和算法知识将受到欢迎。
如何做到这一点...
让我们看看 Boost.Bind
与传统 STL 类一起使用的示例:
-
按照以下代码计算大于或等于 5 的值:
boost::array<int, 12> values = {{1, 2, 3, 4, 5, 6, 7, 100, 99, 98, 97, 96}}; std::size_t count0 = std::count_if(values.begin(), values.end(), std::bind1st(std::less<int>(), 5)); std::size_t count1 = std::count_if(values.begin(), values.end(), boost::bind(std::less<int>(), 5, _1)); assert(count0 == count1);
-
这是我们如何计算空字符串的方法:
boost::array<std::string, 3> str_values = {{"We ", "are", " the champions!"}}; count0 = std::count_if(str_values.begin(), str_values.end(), std::mem_fun_ref(&std::string::empty)); count1 = std::count_if(str_values.begin(), str_values.end(), boost::bind(&std::string::empty, _1)); assert(count0 == count1);
-
现在让我们计算长度小于
5
的字符串:// That code won't compile! And it is hard to understand //count0 = std::count_if(str_values.begin(), //str_values.end(), //std::bind2nd( // std::bind1st( // std::less<std::size_t>(), // std::mem_fun_ref(&std::string::size) // ) //, 5 //)); // This will become much more readable, // when you get used to bind count1 = std::count_if(str_values.begin(), str_values.end(), boost::bind(std::less<std::size_t>(), boost::bind(&std::string::size, _1), 5)); assert(2 == count1);
-
比较字符串:
std::string s("Expensive copy constructor of std::string will be called when binding"); count0 = std::count_if(str_values.begin(), str_values.end(), std::bind2nd(std::less<std::string>(), s)); count1 = std::count_if(str_values.begin(), str_values.end(), boost::bind(std::less<std::string>(), _1, s)); assert(count0 == count1);
它是如何工作的...
boost::bind
函数返回一个功能对象,该对象存储了绑定值的副本和原始功能对象的副本。当实际执行 operator()
调用时,存储的参数会传递给原始功能对象,同时也会传递调用时传递的参数。
还有更多...
看一下前面的例子。当我们绑定值时,我们会将一个值复制到一个功能对象中。对于某些类,这个操作可能很昂贵。有没有一种方法可以绕过复制?
是的!而且 Boost.Ref
库将帮助我们!它包含两个函数,boost::ref()
和 boost::cref()
,前者允许我们将参数作为引用传递,后者将参数作为常量引用传递。ref()
和 cref()
函数只是构造了一个类型为 reference_wrapper<T>
或 reference_wrapper<const T>
的对象,它可以隐式转换为引用类型。让我们修改我们之前的例子:
#include <boost/ref.hpp>
...
std::string s("Expensive copy constructor of std::string now "
"won't be called when binding");
count0 = std::count_if(str_values.begin(), str_values.end(), std::bind2nd(std::less<std::string>(), boost::cref(s)));
count1 = std::count_if(str_values.begin(), str_values.end(), boost::bind(std::less<std::string>(), _1, boost::cref(s)));
assert(count0 == count1);
再举一个例子,展示如何使用 boost::ref
来连接字符串:
void wierd_appender(std::string& to, const std::string& from) {
to += from;
};
std::string result;
std::for_each(str_values.cbegin(), str_values.cend(), boost::bind(&wierd_appender, boost::ref(result), _1));
assert(result == "We are the champions!");
函数 ref
、cref
(以及 bind
)被接受到 C++11 标准中,并在 std::
命名空间中的 <functional>
头文件中定义。这些函数都不在堆上动态分配内存,也不使用虚函数。它们返回的对象易于优化,并且不会为好的编译器应用任何优化屏障。
这些函数的 STL 实现可能有一些额外的优化,以减少编译时间或只是针对特定编译器的优化,但遗憾的是,一些 STL 实现缺少 Boost 版本的某些功能。你可以使用任何 Boost 库中的 STL 版本,甚至混合 Boost 和 STL 版本。
参见
-
Boost.Bind
库在这本书中得到了广泛的应用;请参阅第六章 “处理任务” 和第五章 “多线程”,以获取更多示例 -
官方文档包含更多示例和高级特性的描述,请参阅
www.boost.org/doc/libs/1_53_0/libs/bind/bind.html
使用 C++11 移动模拟
C++11 标准最伟大的特性之一是右值引用。这个特性允许我们修改临时对象,从它们那里“窃取”资源。正如你所猜到的,C++03 标准没有右值引用,但使用 Boost.Move
库,你可以编写一些可移植的代码来使用它们,甚至更多,你实际上可以开始模拟移动语义。
准备工作
强烈建议至少了解 C++11 rvalue references 的基础知识。
如何做到这一点...
现在,让我们看看以下示例:
-
想象一下,你有一个具有多个字段(其中一些是 STL 容器)的类。
namespace other { // Its default construction is cheap/fast class characteristics{}; } // namespace other struct person_info { // Fields declared here // ... bool is_male_; std::string name_; std::string second_name_; other::characteristics characteristic_; };
-
是时候给它添加移动赋值和移动构造函数了!只需记住,在 C++03 中,STL 容器既没有移动操作符也没有移动构造函数。
-
正确的移动赋值实现与
swap
和clear
(如果允许空状态)相同。正确的移动构造函数实现接近默认构造和swap
。所以,让我们从swap
成员函数开始:#include <boost/swap.hpp> void swap(person_info& rhs) { std::swap(is_male_, rhs.is_male_); name_.swap(rhs.name_); second_name_.swap(rhs.second_name_); boost::swap(characteristic_, rhs.characteristic_); }
-
现在,将以下宏放在
private
部分:BOOST_COPYABLE_AND_MOVABLE(classname)
-
编写一个拷贝构造函数。
-
编写一个拷贝赋值,参数为
BOOST_COPY_ASSIGN_REF(classname)
。 -
编写一个移动构造函数和一个移动赋值,参数为
BOOST_RV_REF(classname)
:struct person_info { // Fields declared here // ... private: BOOST_COPYABLE_AND_MOVABLE(person_info) public: // For the simplicity of example we will assume that // person_info default constructor and swap are very // fast/cheap to call person_info() {} person_info(const person_info& p) : is_male_(p.is_male_) , name_(p.name_) , second_name_(p.second_name_) , characteristic_(p.characteristic_) {} person_info(BOOST_RV_REF(person_info) person) { swap(person); } person_info& operator=(BOOST_COPY_ASSIGN_REF(person_info) person) { if (this != &person) { person_info tmp(person); swap(tmp); } return *this; } person_info& operator=(BOOST_RV_REF(person_info) person) { if (this != &person) { swap(person); person_info tmp; tmp.swap(person); } return *this; } void swap(person_info& rhs) { // … } };
-
现在,我们有了
person_info
类的移动赋值和移动构造函数的可移植、快速实现。
它是如何工作的...
下面是一个如何使用移动赋值的例子:
person_info vasya;
vasya.name_ = "Vasya";
vasya.second_name_ = "Snow";
vasya.is_male_ = true;
person_info new_vasya(boost::move(vasya));
assert(new_vasya.name_ == "Vasya");
assert(new_vasya.second_name_ == "Snow");
assert(vasya.name_.empty());
assert(vasya.second_name_.empty());
vasya = boost::move(new_vasya);
assert(vasya.name_ == "Vasya");
assert(vasya.second_name_ == "Snow");
assert(new_vasya.name_.empty());
assert(new_vasya.second_name_.empty());
Boost.Move
库以非常高效的方式实现。当使用 C++11 编译器时,所有用于 rvalue 模拟的宏都将扩展为 C++11 特定功能,否则(在 C++03 编译器上)rvalue 将使用特定的数据类型和函数进行模拟,这些函数永远不会复制传递的值,也不会调用任何动态内存分配或虚拟函数。
还有更多...
你注意到 boost::swap
调用了吗?这是一个非常有用的实用函数,它将首先在变量的命名空间中(命名空间 other::
)搜索 swap
函数,如果没有为 characteristics
类提供 swap
函数,它将使用 STL 的 swap
实现。
参见
-
更多关于模拟实现的信息可以在 Boost 网站上找到,也可以在
Boost.Move
库的源代码中找到,链接为www.boost.org/doc/libs/1_53_0/doc/html/move.html
。 -
Boost.Utility
库是包含boost::utility
的库,它有许多有用的函数和类。请参阅其文档,链接为www.boost.org/doc/libs/1_53_0/libs/utility/utility.htm
。 -
在第三章,管理资源中,通过派生类的成员初始化基类的配方。
-
创建不可拷贝的类配方。
-
在创建不可拷贝但可移动的类配方中,有更多关于
Boost.Move
的信息,以及一些关于如何在容器中以可移植和高效的方式使用可移动对象的示例。
创建不可拷贝的类
你几乎肯定遇到过这样的情况,为类提供拷贝构造函数和移动赋值操作符需要做太多工作,或者类拥有一些由于技术原因不能复制的资源:
class descriptor_owner {
void* descriptor_;
public:
explicit descriptor_owner(const char* params);
~descriptor_owner() {
system_api_free_descriptor(descriptor_);
}
};
在前一个示例中,C++ 编译器将生成一个复制构造函数和赋值运算符,因此 descriptor_owner
类的潜在用户将能够创建以下糟糕的东西:
descriptor_owner d1("O_o");
descriptor_owner d2("^_^");
// Descriptor of d2 was not correctly freed
d2 = d1;
// destructor of d2 will free the descriptor
// destructor of d1 will try to free already freed descriptor
准备工作
对于这个配方,只需要非常基础的 C++ 知识。
如何做到...
为了避免这种情况,发明了 boost::noncopyable
类。如果你从它派生自己的类,C++ 编译器将不会生成复制构造函数和赋值运算符:
#include <boost/noncopyable.hpp>
class descriptor_owner_fixed : private boost::noncopyable {
…
现在,用户将无法做坏事:
descriptor_owner_fixed d1("O_o");
descriptor_owner_fixed d2("^_^");
// Won't compile
d2 = d1;
// Won't compile either
descriptor_owner_fixed d3(d1);
它是如何工作的...
精通读者会告诉我,我们可以通过将 descriptor_owning_fixed
的复制构造函数和赋值运算符设为私有,或者只是定义它们而不实现它们来达到完全相同的结果。是的,你是正确的。此外,这是 boost::noncopyable
类的当前实现。但 boost::noncopyable
也为你的类提供了良好的文档。它永远不会提出诸如“复制构造函数体是否在其他地方定义?”或“它是否有非标准的复制构造函数(带有非 const 引用的参数)?”等问题。
参见
-
创建一个不可复制但可移动的类 配方将给你一些想法,如何在 C++03 中通过移动来允许资源的唯一所有权。
-
你可以在
www.boost.org/doc/libs/1_53_0/libs/utility/utility.htm
的Boost.Utility
库的官方文档中找到很多有用的函数和类。 -
在 第三章 管理资源 中,通过派生类的成员初始化基类 的配方。
-
使用 C++11 移动仿真的配方
创建一个不可复制但可移动的类
现在想象以下情况:我们有一个不能复制的资源,它应该在析构函数中正确释放,并且我们希望从函数中返回它:
descriptor_owner construct_descriptor() {
return descriptor_owner("Construct using this string");
}
实际上,你可以使用 swap
方法来规避这种情况:
void construct_descriptor1(descriptor_owner& ret) {
descriptor_owner("Construct using this string").swap(ret);
}
但这样的规避方法不会允许我们在 STL 或 Boost 容器中使用 descriptor_owner
。顺便说一下,这看起来很糟糕!
准备工作
强烈建议至少了解 C++11 右值引用的基础知识。阅读 使用 C++11 移动仿真的配方 也是推荐的。
如何做到...
那些已经使用 C++11 的读者已经知道关于只移动类(如 std::unique_ptr
或 std::thread
)。使用这种方法,我们可以创建一个只移动的 descriptor_owner
类:
class descriptor_owner1 {
void* descriptor_;
public:
descriptor_owner1()
: descriptor_(NULL)
{}
explicit descriptor_owner1(const char* param)
: descriptor_(strdup(param))
{}
descriptor_owner1(descriptor_owner1&& param)
: descriptor_(param.descriptor_)
{
param.descriptor_ = NULL;
}
descriptor_owner1& operator=(descriptor_owner1&& param) {
clear();
std::swap(descriptor_, param.descriptor_);
return *this;
}
void clear() {
free(descriptor_);
descriptor_ = NULL;
}
bool empty() const {
return !descriptor_;
}
~descriptor_owner1() {
clear();
}
};
// GCC compiles the following in with -std=c++0x
descriptor_owner1 construct_descriptor2() {
return descriptor_owner1("Construct using this string");
}
void foo_rv() {
std::cout << "C++11\n";
descriptor_owner1 desc;
desc = construct_descriptor2();
assert(!desc.empty());
}
这只会在与 C++11 兼容的编译器上工作。这正是使用 Boost.Move
的正确时机!让我们修改我们的示例,使其可以在 C++03 编译器上使用。
根据文档,为了用可移植语法编写一个可移动但不可复制的类型,我们需要遵循以下简单的步骤:
-
在
private
部分 putBOOST_MOVABLE_BUT_NOT_COPYABLE(classname)
宏:class descriptor_owner_movable { void* descriptor_; BOOST_MOVABLE_BUT_NOT_COPYABLE(descriptor_owner_movable)
-
编写一个移动构造函数和移动赋值运算符,参数为
BOOST_RV_REF(classname)
:#include <boost/move/move.hpp> public: descriptor_owner_movable() : descriptor_(NULL) {} explicit descriptor_owner_movable(const char* param) : descriptor_(strdup(param)) {} descriptor_owner_movable( BOOST_RV_REF(descriptor_owner_movable) param) : descriptor_(param.descriptor_) { param.descriptor_ = NULL; } descriptor_owner_movable& operator=( BOOST_RV_REF(descriptor_owner_movable) param) { clear(); std::swap(descriptor_, param.descriptor_); return *this; } // ... }; descriptor_owner_movable construct_descriptor3() { return descriptor_owner_movable("Construct using this string"); }
它是如何工作的...
现在我们有一个可移动但不可复制的类,它甚至可以在 C++03 编译器和 Boost.Containers
中使用:
#include <boost/container/vector.hpp>
...
// Following code will work on C++11 and C++03 compilers
descriptor_owner_movable movable;
movable = construct_descriptor3();
boost::container::vector<descriptor_owner_movable> vec;
vec.resize(10);
vec.push_back(construct_descriptor3());
vec.back() = boost::move(vec.front());
但遗憾的是,C++03 STL 容器仍然无法使用它(这就是为什么我们在上一个示例中使用了 Boost.Containers
中的向量)。
还有更多...
如果你想在 C++03 编译器和 STL 容器中使用 Boost.Containers
,在 C++11 编译器上,你可以使用以下简单的技巧。将以下内容的头文件添加到你的项目中:
// your_project/vector.hpp
// Copyright and other stuff goes here
// include guards
#ifndef YOUR_PROJECT_VECTOR_HPP
#define YOUR_PROJECT_VECTOR_HPP
#include <boost/config.hpp>
// Those macro declared in boost/config.hpp header
// This is portable and can be used with any version of boost
// libraries
#if !defined(BOOST_NO_RVALUE_REFERENCES) && !defined(BOOST_NO_CXX11_RVALUE_REFERENCES)
// We do have rvalues
#include <vector>
namespace your_project_namespace {
using std::vector;
} // your_project_namespace
#else
// We do NOT have rvalues
#include <boost/container/vector.hpp>
namespace your_project_namespace {
using boost::container::vector;
} // your_project_namespace
#endif // !defined(BOOST_NO_RVALUE_REFERENCES) && !defined(BOOST_NO_CXX11_RVALUE_REFERENCES)
#endif // YOUR_PROJECT_VECTOR_HPP
现在,你可以包含 <your_project/vector.hpp>
并使用 your_project_namespace
命名空间中的向量:
your_project_namespace::vector<descriptor_owner_movable> v;
v.resize(10);
v.push_back(construct_descriptor3());
v.back() = boost::move(v.front());
但请注意编译器和 STL 实现特定的问题!例如,只有当你将移动构造函数、析构函数和移动赋值运算符标记为 noexcept
时,这段代码才会在 GCC 4.7 的 C++11 模式下编译。
参见
-
在第十章 Chapter 10. Gathering Platform and Compiler Information 的 Reducing code size and increasing performance of user-defined types (UDTs) in C++11 节中,可以找到更多关于
noexcept
的信息。 -
关于
Boost.Move
的更多信息可以在 Boost 网站上找到www.boost.org/doc/libs/1_53_0/doc/html/move.html
第二章. 转换数据
在本章中,我们将介绍:
-
将字符串转换为数字
-
将数字转换为字符串
-
将数字转换为数字
-
将用户定义的类型转换为/从字符串转换
-
转换多态对象
-
解析简单输入
-
解析输入
简介
现在我们已经了解了一些基本的 Boost 类型,是时候了解一些数据转换函数了。在本章中,我们将看到如何将字符串、数字和用户定义的类型相互转换,如何安全地转换多态类型,以及如何在 C++源文件中编写小型和大型解析器。
将字符串转换为数字
在 C++中将字符串转换为数字让很多人感到沮丧,因为其低效和用户不友好。让我们看看如何将字符串100
转换为int
:
#include <sstream>
std::istringstream iss("100");
int i;
iss >> i;
// And now, 'iss' variable will get in the way all the time,
// till end of the scope
// It is better not to think, how many unnecessary operations,
// virtual function calls and memory allocations occurred
// during those operations
C 方法并不好多少:
#include <cstdlib>
char * end;
int i = std::strtol ("100", &end, 10);
// Did it converted all the value to int, or stopped somewhere
// in the middle?
// And now we have 'end' variable will getting in the way
// By the way, we want an integer, but strtol returns long
// int... Did the converted value fit in int?
准备工作
只需要具备基本的 C++和 STL 知识即可使用此菜谱。
如何做...
Boost 库中有一个库可以帮助你应对字符串到数字转换的令人沮丧的难度。它被称为Boost.LexicalCast
,包括一个boost::bad_lexical_cast
异常类和一些boost::lexical_cast
函数:
#include <boost/lexical_cast.hpp>
int i = boost::lexical_cast<int>("100");
它甚至可以用于非空终止的字符串:
char chars[] = {'1', '0', '0' };
int i = boost::lexical_cast<int>(chars, 3);
assert(i == 100);
它是如何工作的...
boost::lexical_cast
函数接受字符串作为输入,并将其转换为尖括号中指定的类型。boost::lexical_cast
函数甚至会为您检查边界:
try {
// on x86 short usually may not store values greater than 32767
short s = boost::lexical_cast<short>("1000000");
assert(false); // Must not reach this
} catch (const boost::bad_lexical_cast& /*e*/) {}
还要检查输入的正确语法:
try {
int i = boost::lexical_cast<int>("This is not a number!");
assert(false); // Must not reach this
(void)i; // Suppressing warning about unused variable
} catch (const boost::bad_lexical_cast& /*e*/) {}
还有更多...
词法转换就像所有std::stringstreams
类一样使用std::locale
,可以转换本地化数字,但同时也有一套针对 C locale 和没有数字分组的 locale 的优化:
#include <locale>
std::locale::global(std::locale("ru_RU.UTF8"));
// In Russia coma sign is used as a decimal separator
float f = boost::lexical_cast<float>("1,0");
assert(f < 1.01 && f > 0.99);
这还不算完!你甚至可以简单地创建用于转换数字的模板函数。让我们创建一个将某些string
值容器转换为long int
值向量的函数:
#include <algorithm>
#include <vector>
#include <iterator>
#include <boost/lexical_cast.hpp>
template <class ContainerT>
std::vector<long int> container_to_longs(const ContainerT& container) {
typedef typename ContainerT::value_type value_type;
std::vector<long int> ret;
typedef long int (*func_t)(const value_type&);
func_t f = &boost::lexical_cast<long int, value_type>;
std::transform(container.begin(), container.end(), std::back_inserter(ret), f);
return ret;
}
// Somewhere in source file...
std::set<std::string> str_set;
str_set.insert("1");
assert(container_to_longs(str_set).front() == 1);
std::deque<const char*> char_deque;
char_deque.push_front("1");
char_deque.push_back("2");
assert(container_to_longs(char_deque).front() == 1);
assert(container_to_longs(char_deque).back() == 2);
// Obfuscating people with curly braces is fun!
typedef boost::array<unsigned char, 2> element_t;
boost::array<element_t, 2> arrays = {{ {{'1', '0'}}, {{'2', '0'}} }};
assert(container_to_longs(arrays).front() == 10);
assert(container_to_longs(arrays).back() == 20);
参见
-
请参阅“将数字转换为字符串”菜谱以获取有关
boost::lexical_cast
性能的信息。 -
Boost.LexicalCast
的官方文档包含一些示例、性能指标和常见问题的答案。它可在以下位置找到:www.boost.org/doc/libs/1_53_0/doc/html/boost_lexical_cast.html
将数字转换为字符串
在这个菜谱中,我们将继续讨论词法转换,但现在我们将使用Boost.LexicalCast
将数字转换为字符串。像往常一样,boost::lexical_cast
将提供一种非常简单的方法来转换数据。
准备工作
只需要具备基本的 C++和 STL 知识即可使用此菜谱。
如何做...
-
让我们使用
boost::lexical_cast
将整数100
转换为std::string
:#include <boost/lexical_cast.hpp> std::string s = boost::lexical_cast<std::string>(100); assert(s == "100");
-
将它与传统的 C++转换方法进行比较:
#include <sstream> // C++ way of converting to strings std::stringstream ss; ss << 100; std::string s; ss >> s; // Variable 'ss' will dangle all the way, till the end // of scope // Multiple virtual methods were called during // conversion assert(s == "100");
与 C 转换方法相反:
#include <cstdlib> // C way of converting to strings char buffer[100]; std::sprintf(buffer, "%i", 100); // You will need an unsigned long long int type to // count how many times errors were made in 'printf' // like functions all around the world. 'printf' // functions are a constant security threat! // But wait, we still need to construct a std::string std::string s(buffer); // And now we have an buffer variable that won't be // used assert(s == "100");
它是如何工作的...
boost::lexical_cast
函数也可以接受数字作为输入,并将它们转换为尖括号中指定的字符串类型。这与我们在上一个菜谱中所做的是非常相似的。
还有更多...
仔细的读者会注意到,在 lexical_cast
的情况下,我们有一个额外的调用到字符串拷贝构造函数,并且这样的调用将对性能产生影响。这是真的,但仅限于旧或差的编译器。现代编译器实现了命名返回值优化(NRVO),这将消除对拷贝构造函数和析构函数的不必要调用。即使与 C++11 兼容的编译器没有检测到 NRVO,它们也会使用 std::string
的移动拷贝构造函数,这是快速且高效的。Boost.LexicalCast
文档的 性能 部分显示了不同编译器对不同类型的转换速度,在大多数情况下 lexical_cast
比标准库的 std::stringstream
和 printf
函数更快。
如果将 boost::array
或 std::array
作为输出参数类型传递给 boost::lexical_cast
,将减少动态内存分配(或者根本不会进行内存分配;这取决于 std::locale
的实现)。
参见
-
Boost 的官方文档包含表格,比较
lexical_cast
的性能与其他转换方法。在大多数情况下,它都胜出。www.boost.org/doc/libs/1_53_0/doc/html/boost_lexical_cast.html
。它还有一些更多示例和一个常见问题解答部分。 -
将字符串转换为数字 的菜谱。
-
将用户定义类型转换为字符串或从字符串转换 的菜谱。
将数字转换为数字
你可能还记得你编写过如下代码的情况:
void some_function(unsigned short param);
int foo();
// Somewhere in code
// Some compilers may warn that int is being converted to
// unsigned short and that there is a possibility of losing
// data
some_function(foo());
通常,程序员只是通过隐式转换为无符号短整型数据类型来忽略这样的警告,如下面的代码片段所示:
// Warning suppressed. Looks like a correct code
some_function(
static_cast<unsigned short>(foo())
);
但这可能会使检测错误变得极其困难。这样的错误可能在代码中存在数年才被发现:
// Returns -1 if error occurred
int foo() {
if (some_extremely_rare_condition()) {
return -1;
} else if (another_extremely_rare_condition()) {
return 1000000;
}
return 65535;
}
准备工作
对于这个菜谱,只需要基本的 C++ 知识。
如何做到这一点...
-
库
Boost.NumericConversion
为此类情况提供了一个解决方案。并且很容易修改现有的代码以使用安全的转换,只需将static_cast
替换为boost::numeric_cast
。如果源值无法存储在目标中,它将抛出一个异常。让我们看看以下示例:#include <boost/numeric/conversion/cast.hpp> void correct_implementation() { // 100% correct some_function( boost::numeric_cast<unsigned short>(foo()) ); } void test_function() { for (unsigned int i = 0; i < 100; ++i) { try { correct_implementation(); } catch (const boost::numeric::bad_numeric_cast& e) { std::cout << '#' << i << ' ' << e.what() << std::endl; } } }
-
现在,如果我们运行
test_function()
,它将输出以下内容:#47 bad numeric conversion: negative overflow #58 bad numeric conversion: positive overflow
-
我们甚至可以检测特定的溢出类型:
void test_function1() { for (unsigned int i = 0; i < 100; ++i) { try { correct_implementation(); } catch (const boost::numeric::positive_overflow& e) { // Do something specific for positive overflow std::cout << "POS OVERFLOW in #" << i << ' ' << e.what() << std::endl; } catch (const boost::numeric::negative_overflow& e) { // Do something specific for negative overwlow std::cout <<"NEG OVERFLOW in #" << i << ' ' << e.what() << std::endl; } } }
test_function1()
函数将输出以下内容:NEG OVERFLOW in #47 bad numeric conversion: negative overflow POS OVERFLOW in #59 bad numeric conversion: positive overflow
它是如何工作的...
它检查输入参数的值是否可以适合新类型而不丢失数据,如果在转换过程中丢失了数据,它将抛出一个异常。
Boost.NumericConversion
库有一个非常快速的实现;它可以在编译时完成大量工作。例如,当转换为范围更广的类型时,源代码将直接调用 static_cast
方法。
还有更多...
boost::numeric_cast
函数是通过 boost::numeric::converter
实现的,它可以调整以使用不同的溢出、范围检查和舍入策略。但通常,numeric_cast
就是您所需要的。
这里有一个小例子,演示了如何为 boost::numeric::cast
创建我们自己的 mythrow_overflow_handler
过滤器:
template <class SourceT, class TargetT>
struct mythrow_overflow_handler {
void operator() (boost::numeric::range_check_result r) {
if (r != boost::numeric::cInRange) {
throw std::logic_error("Not in range!");
}
}
};
template <class TargetT, class SourceT>
TargetT my_numeric_cast(const SourceT& in) {
using namespace boost;
typedef numeric::conversion_traits<TargetT, SourceT> conv_traits;
typedef numeric::numeric_cast_traits<TargetT, SourceT> cast_traits;
typedef boost::numeric::converter
<
TargetT,
SourceT,
conv_traits,
mythrow_overflow_handler<SourceT, TargetT> // !!!
> converter;
return converter::convert(in);
}
// Somewhere in code
try {
my_numeric_cast<short>(100000);
} catch (const std::logic_error& e) {
std::cout << "It works! " << e.what() << std::endl;
}
这将输出以下内容:
It works! Not in range!
参见
-
Boost 的官方文档包含了所有数值转换模板参数的详细描述;它可以在以下链接找到:
www.boost.org/doc/libs/1_53_0/libs/numeric/conversion/doc/html/index.html
将用户定义类型转换为/从字符串
Boost.LexicalCast
有一个特性允许用户在 lexical_cast
中使用他们自己的类型。这个特性只需要用户为他们自己的类型编写正确的 std::ostream
和 std::istream
操作符。
如何做到这一点...
-
您只需要提供一个
operator<<
和operator>>
流操作符。如果您的类已经是可流的,则不需要做任何事情:#include <iosfwd> #include <stdexcept> // Somewhere in header file // Negative number, that does not store minus sign class negative_number { unsigned short number_; public: explicit negative_number(unsigned short number) : number_(number) {} // operators and functions defined lower // ... unsigned short value_without_sign() const { return number_; } }; std::ostream& operator<<(std::ostream& os, const negative_number& num) { os << '-' << num.value_without_sign(); return os; } std::istream& operator>>(std::istream& is, negative_number& num) { char ch; is >> ch; if (ch != '-') { throw std::logic_error("negative_number class designed " "to store ONLY negative values"); } unsigned short s; is >> s; num = negative_number(s); return os; }
-
现在我们可以使用
boost::lexical_cast
来将negative_number
类转换为其他类型,以及从其他类型转换为negative_number
类。以下是一个例子:#include <boost/lexical_cast.hpp> #include <assert.h> int main() { negative_number n = boost::lexical_cast<negative_number>("-100"); assert(n.value_without_sign() == 100); int i = boost::lexical_cast<int>(n); assert(i == -100); typedef boost::array<char, 10> arr_t; arr_t arr = boost::lexical_cast<arr_t>(n); assert(arr[0] == '-'); assert(arr[1] == '1'); assert(arr[2] == '0'); assert(arr[3] == '0'); assert(arr[4] == '\0'); }
它是如何工作的...
boost::lexical_cast
函数可以检测并使用流操作符来转换用户定义的类型。
Boost.LexicalCast
库为基本类型提供了许多优化,当用户定义类型被转换为基本类型或基本类型被转换为用户定义类型时,这些优化会被触发。
还有更多...
boost::lexical_cast
函数也可以转换为宽字符字符串,但需要正确的 basic_istream
和 basic_ostream
操作符重载:
template <class CharT>
std::basic_ostream<CharT>& operator<<(std::basic_ostream<CharT>& os,
const negative_number& num)
{
os << static_cast<CharT>('-') << num.value_without_sign();
return os;
}
template <class CharT>
std::basic_istream<CharT>& operator>>(std::basic_istream<CharT>& is, negative_number& num) {
CharT ch;
is >> ch;
if (ch != static_cast<CharT>('-')) {
throw std::logic_error("negative_number class designed to "
"store ONLY negative values");
}
unsigned short s;
is >> s;
num = negative_number(s);
return is;
}
int main() {
negative_number n = boost::lexical_cast<negative_number>(L"-1");
assert(n.value_without_sign() == 1);
typedef boost::array<wchar_t, 10> warr_t;
warr_t arr = boost::lexical_cast<warr_t>(n);
assert(arr[0] == L'-');
assert(arr[1] == L'1');
assert(arr[4] == L'\0');
}
Boost.LexicalCast
库不是 C++11 的一部分,但有一个提议将其添加到 C++ 标准中。许多 Boost 库都使用它,并希望它也能使您的生活更轻松。
参见
-
Boost.LexicalCast
文档包含了一些示例、性能指标和常见问题的答案;它可以在www.boost.org/doc/libs/1_53_0/doc/html/boost_lexical_cast.html
找到 -
将字符串转换为数字 的配方
-
将数字转换为字符串 的配方
转换多态对象
想象一下,某个程序员设计了一个糟糕的接口,如下所示(这是一个接口不应该如何编写的良好例子):
struct object {
virtual ~object() {}
};
struct banana: public object {
void eat() const {}
virtual ~banana(){}
};
struct pidgin: public object {
void fly() const {}
virtual ~pidgin(){}
};
object* try_produce_banana();
我们的任务是创建一个吃香蕉的函数,如果遇到的不是香蕉而是其他东西(吃洋泾浜语很恶心!),则抛出异常。如果我们尝试取消引用 try_produce_banana()
函数返回的值,我们就会处于取消引用空指针的危险之中。
准备工作
对于这个食谱,需要具备基本的 C++ 知识。
如何做到...
因此,我们需要编写以下代码:
void try_eat_banana_impl1() {
const object* obj = try_produce_banana();
if (!obj) {
throw std::bad_cast();
}
dynamic_cast<const banana&>(*obj).eat();
}
看起来很糟糕,不是吗?Boost.Conversion
提供了一个稍微好一点的解决方案:
#include <boost/cast.hpp>
void try_eat_banana_impl2() {
const object* obj = try_produce_banana();
boost::polymorphic_cast<const banana*>(obj)->eat();
}
它是如何工作的...
boost::polymorphic_cast
函数只是围绕第一个示例中的代码进行包装,仅此而已。它会检查输入是否为空,然后尝试进行动态转换。在这些操作过程中出现的任何错误都将抛出 std::bad_cast
异常。
还有更多...
Boost.Conversion
库还有一个 polymorphic_downcast
函数,它应该仅用于始终成功的向下转换。在调试模式(当 NDEBUG
未定义时)中,它将使用 dynamic_cast
检查正确的向下转换。当 NDEBUG
定义时,polymorphic_downcast
函数将仅执行 static_cast
操作。这是一个在性能关键部分使用的好函数,同时仍然能够在调试编译中检测到错误。
参见
-
polymorphic_cast
的想法最初是在书籍 《C++编程语言》 中提出的,作者是 Bjarne Stroustrup。有关更多信息以及不同主题的一些好主意,请参阅此书。 -
官方文档也可能很有帮助;它可在
www.boost.org/doc/libs/1_53_0/libs/conversion/cast.htm
找到。
解析简单输入
解析一小段文本是一个常见的任务。在这种情况下,我们常常面临一个困境:我们应该使用一些第三方专业工具进行解析,比如 Bison 或 ANTLR,还是尝试仅使用 C++ 和 STL 手动编写它?第三方工具在处理复杂文本的解析方面表现良好,使用它们编写解析器也很容易,但它们需要额外的工具来从它们的语法中创建 C++ 或 C 代码,并给项目添加更多的依赖。手写的解析器通常难以维护,但它们只需要 C++ 编译器。
让我们从一个非常简单的任务开始,解析 ISO 格式的日期,如下所示:
YYYY-MM-DD
以下是一些可能的输入示例:
2013-03-01
2012-12-31 // (woo-hoo, it almost a new year!)
让我们看看以下链接中的解析器语法 www.ietf.org/rfc/rfc3339.txt
:
date-fullyear = 4DIGIT
date-month = 2DIGIT ; 01-12
date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on
; month/year
full-date = date-fullyear "-" date-month "-" date-mday
准备工作
确保你熟悉占位符的概念,或者阅读 第一章 中的 重新排序函数参数 和 将值绑定为函数参数 食谱,开始编写您的应用程序。对解析工具的基本了解也会很有帮助。
如何做到...
让我向你介绍 Boost.Spirit
库。它允许直接在 C++ 代码格式中编写解析器(以及词法分析和生成器),它们可以立即执行(也就是说,不需要额外的工具来生成 C++ 代码)。Boost.Spirit
的语法非常接近 扩展巴科斯-诺尔范式(EBNF),它被许多标准用于表达语法,并且被其他流行的解析器所理解。本章开头的语法是 EBNF 格式。
-
我们需要包含以下头文件:
#include <boost/spirit/include/qi.hpp> #include <boost/spirit/include/phoenix_core.hpp> #include <boost/spirit/include/phoenix_operator.hpp> #include <assert.h>
-
现在是时候创建一个
date
结构来保存解析的数据了:struct date { unsigned short year; unsigned short month; unsigned short day; };
-
现在让我们看看解析器(关于它是如何一步步工作的详细描述可以在下一节找到):
// See recipe "Using a reference to string type" in Chapter 7, // Manipulating Strings for a better type // than std::string for parameter 's' date parse_date_time1(const std::string& s) { using boost::spirit::qi::_1; using boost::spirit::qi::ushort_; using boost::spirit::qi::char_; using boost::phoenix::ref; date res; const char* first = s.data(); const char* const end = first + s.size(); bool success = boost::spirit::qi::parse(first, end, ushort_[ ref(res.year) = 1 ] >> char('-') >> ushort_[ ref(res.month) = 1 ] >> char('-') >> ushort_[ ref(res.day) = _1 ] ); if (!success || first != end) { throw std::logic_error("Parsing failed"); } return res; }
-
现在我们可以在这个解析器想要使用的地方使用它:
int main() { date d = parse_date_time1("2012-12-31"); assert(d.year == 2012); assert(d.month == 12); assert(d.day == 31); }
它是如何工作的...
这是一个非常简单的实现;它不会检查数字的位数。解析发生在 boost::spirit::qi::parse
函数中。让我们稍微简化一下,移除成功的解析上的动作:
bool success = boost::spirit::qi::parse(first, end,
ushort_ >> char_('-') >> ushort_ >> char_('-') >> ushort_
);
first
参数指向要解析的数据的开始;它必须是一个可修改的(非常量)变量,因为 parse
函数将使用它来显示解析序列的结束。end
参数指向最后一个元素之后的元素。first
和 end
应该是迭代器。
函数的第三个参数是一个解析规则。它确实做了 EBNF 规则中写的那样的事情:
date-fullyear "-" date-month "-" date-md
我们只是用 >>
操作符替换了空白。
parse
函数在成功时返回 true。如果我们想确保整个字符串都被成功解析,我们需要检查解析器的返回值和输入迭代器的相等性。
现在我们需要处理成功的解析上的动作,这个配方就完成了。Boost.Spirit
中的语义动作写在 []
内,并且可以使用函数指针、函数对象、boost::bind
、std::bind
(或其他的 bind()
实现)或 C++11 lambda 函数来编写。
因此,你也可以用 C++11 lambda 为 YYYY
编写一个规则:
ushort_ [&res {res.year = s;} ]
现在,让我们更仔细地看看这个月的语义动作:
ushort_[ ref(res.month) = _1 ]
对于那些从开始就阅读这本书的人来说,这会让你想起 boost::bind
和占位符。ref(res.month)
表示将 res.month
作为可修改的引用传递,而 _1
表示第一个输入参数,它将是一个数字(ushort_
解析的结果)。
更多...
现在让我们修改我们的解析器,使其能够处理数字的位数。为此,我们将使用 unit_parser
模板类并设置正确的参数:
date parse_date_time2(const std::string& s) {
using boost::spirit::qi::_1;
using boost::spirit::qi::uint_parser;
using boost::spirit::qi::char_;
using boost::phoenix::ref;
// Use unsigned short as output type, require Radix 10, and from 2
// to 2 digits
uint_parser<unsigned short, 10, 2, 2> u2_;
// Use unsigned short as output type, require Radix 10, and from 4
// to 4 digits
uint_parser<unsigned short, 10, 4, 4> u4_;
date res;
const char* first = s.data();
const char* const end = first + s.size();
bool success = boost::spirit::qi::parse(first, end,
u4_ [ ref(res.year) = _1 ] >> char_('-')
>> u2_ [ ref(res.month) = _1 ] >> char_('-')
>> u2_ [ ref(res.day) = _1 ]
);
if (!success || first != end) {
throw std::logic_error("Parsing failed");
}
return res;
}
如果这些例子看起来很复杂,请不要担心。我第一次接触 Boost.Spirit
时也感到很害怕,但现在它真的简化了我的生活。如果你不害怕这段代码,你非常勇敢。
如果你想避免代码膨胀,尽量在源文件中编写解析器,而不是在头文件中。还要注意传递给 boost::spirit::parse
函数的迭代器类型,你使用的迭代器类型越少,二进制文件就越小。在源文件中编写解析器还有一个优点:它不会减慢项目的编译速度(正如你可能注意到的,Spirit
解析器编译速度较慢,所以最好在源文件中编译它们,而不是在头文件中定义它们并在整个项目中使用这个文件)。
如果你现在认为通过手动使用 STL 实现日期解析要简单一些...你是正确的!但仅限现在。看看下一个配方;它将给出更多关于 Boost.Spirit
使用的示例,并扩展这个示例,说明手动编写解析器比使用 Boost.Spirit
更困难的情况。
Boost.Spirit
库不是 C++11 的一部分,据我所知,它也没有被提议纳入即将到来的最近的 C++ 标准。
参见
-
第一章中的重新排列函数参数配方,开始编写您的应用程序。
-
将值绑定为函数参数配方。
-
Boost.Spirit
是一个巨大的仅头文件库。可能有一本单独的书来介绍它,所以请自由使用它的文档www.boost.org/doc/libs/1_53_0/libs/spirit/doc/html/index.html
。你还可以找到有关如何在 C++11 代码中直接编写词法分析和生成器的信息。
解析输入
在上一个示例中,我们编写了一个简单的日期解析器。想象一下,时间已经过去,任务已经改变。现在我们需要编写一个支持多种输入格式以及时区偏移的日期时间解析器。因此,现在我们的解析器应该能够理解以下输入:
2012-10-20T10:00:00Z // date time with zero zone offset
2012-10-20T10:00:00 // date time with unspecified zone offset
2012-10-20T10:00:00+09:15 // date time with zone offset
2012-10-20-09:15 // date time with zone offset
10:00:09+09:15 // time with zone offset
准备工作
我们将使用在 解析简单输入 配方中描述的 Spirit
库。在动手做这个配方之前,请先阅读它。
如何做到这一点...
-
让我们从编写一个将保存解析结果的日期时间结构开始:
struct datetime { enum zone_offsets_t { OFFSET_NOT_SET, OFFSET_Z, OFFSET_UTC_PLUS, OFFSET_UTC_MINUS }; private: unsigned short year_; unsigned short month_; unsigned short day_; unsigned short hours_; unsigned short minutes_; unsigned short seconds_; zone_offsets_t zone_offset_type_; unsigned int zone_offset_in_min_; static void dt_assert(bool v, const char* msg) { if (!v) { throw std::logic_error("Assertion failed: " + std::string(msg)); } } public: datetime() : year_(0), month_(0), day_(0) , hours_(0), minutes_(0), seconds_(0) , zone_offset_type_(OFFSET_NOT_SET), zone_offset_in_min_(0) {} // Getters: year(), month(), day(), hours(), minutes(), // seconds(), zone_offset_type(), zone_offset_in_min() // ... // Setters // void set_*(unsigned short val) { /*some assert and setting the *_ to val */ } // ... };
-
现在,让我们编写一个设置时区偏移的函数:
void set_zone_offset(datetime& dt, char sign, unsigned short hours, unsigned short minutes) { dt.set_zone_offset_type(sign == '+' ? datetime::OFFSET_UTC_PLUS : datetime::OFFSET_UTC_MINUS); dt.set_zone_offset_in_min(hours * 60 + minutes); }
-
编写解析器可以分解为编写几个简单的解析器,所以我们首先从编写时区偏移解析器开始。
//Default includes for Boost.Spirit #include <boost/spirit/include/qi.hpp> #include <boost/spirit/include/phoenix_core.hpp> #include <boost/spirit/include/phoenix_operator.hpp> // We'll use bind() function from Boost.Spirit, // because it iterates better with parsers #include <boost/spirit/include/phoenix_bind.hpp> datetime parse_datetime(const std::string& s) { using boost::spirit::qi::_1; using boost::spirit::qi::_2; using boost::spirit::qi::_3; using boost::spirit::qi::uint_parser; using boost::spirit::qi::char_; using boost::phoenix::bind; using boost::phoenix::ref; datetime ret; // Use unsigned short as output type, require Radix 10, and // from 2 to 2 digits uint_parser<unsigned short, 10, 2, 2> u2_; // Use unsigned short as output type, require Radix 10, and // from 4 to 4 digits uint_parser<unsigned short, 10, 4, 4> u4_; boost::spirit::qi::rule<const char*, void()> timezone_parser = -( // unary minus means optional rule // Zero offset char_('Z')[ bind(&datetime::set_zone_offset_type, &ret, datetime::OFFSET_Z) ] | // OR // Specific zone offset ((char_('+')|char_('-')) >> u2_ >> ':' >> u2_) [ bind(&set_zone_offset, ref(ret), _1, _2, _3) ] ); // ... return ret; }
-
让我们通过编写剩余的解析器来完成我们的示例:
boost::spirit::qi::rule<const char*, void()> date_parser = u4_ [ bind(&datetime::set_year, &ret, _1) ] >> char_('-') >> u2_ [ bind(&datetime::set_month, &ret, _1) ] >> char_('-') >> u2_ [ bind(&datetime::set_day, &ret, _1) ]; boost::spirit::qi::rule<const char*, void()> time_parser = u2_ [ bind(&datetime::set_hours, &ret, _1) ] >> char_(':') >> u2_ [ bind(&datetime::set_minutes, &ret, _1) ] >> char_(':') >> u2_ [ bind(&datetime::set_seconds, &ret, _1) ]; const char* first = s.data(); const char* const end = first + s.size(); bool success = boost::spirit::qi::parse(first, end, ((date_parser >> char_('T') >> time_parser) | date_parser | time_parser) >> timezone_parser ); if (!success || first != end) { throw std::logic_error("Parsing of '" + s + "' failed"); } return ret; } // end of parse_datetime() function
它是如何工作的...
这里有一个非常有趣的方法是 boost::spirit::qi::rule<const char*, void()>
。它消除了类型,并允许你在源文件中编写解析器并将它们导出到头文件中。例如:
// Somewhere in header file
class example_1 {
boost::spirit::qi::rule<const char*, void()> some_rule_;
public:
example_1();
};
// In source file
example_1::example_1() {
some_rule_ = /* ... */;
}
但请记住,这个类对编译器来说是一个优化障碍,所以当不需要时不要使用它。
还有更多...
我们可以通过移除执行类型擦除的 rule<>
对象来使我们的示例稍微快一点。对于我们的 C++11 示例,我们可以直接用 auto
关键字替换它们。
Boost.Spirit
库生成的解析器非常快;官方网站上有一些性能指标。还有一些关于如何使用 Boost.Spirit
库的建议;其中之一是只生成一次解析器,然后重复使用它(在我们的示例中未展示)。
在 timezone_parser
中解析特定时区偏移量的规则使用了 boost::phoenix::bind
调用,这不是强制性的。然而,没有它,我们将处理 boost::fusion::vector<char, unsigned short, unsigned short>
,这不如 bind(&set_zone_offset, ref(ret), _1, _2, _3)
用户友好。
在解析大文件时,请考虑阅读 第十一章 中关于 最快读取文件的方法 的菜谱,因为不正确地处理文件可能会比解析更大幅度地减慢您的程序。
编译使用库 Boost.Spirit
(或 Boost.Fusion
)的代码可能需要很长时间,因为模板实例化数量巨大。当在 Boost.Spirit
库上进行实验时,请使用现代编译器,它们提供更好的编译时间。
参见
Boost.Spirit
库值得专门写一本书来介绍。不可能在几个菜谱中描述其所有功能,因此参考文档将帮助您获取更多关于它的信息。它可在www.boost.org/doc/libs/1_53_0/libs/spirit/doc/html/index.html
找到。在那里您会发现更多示例、现成的解析器和有关如何直接在 C++11 代码中使用 Boost 编写词法分析和生成器的信息。
第三章。管理资源
本章我们将涵盖:
-
管理未离开作用域的类的指针
-
在方法间引用计数类指针
-
管理未离开作用域的数组指针
-
在方法间引用计数数组指针
-
在变量中存储任何函数对象
-
在变量中传递函数指针
-
在变量中传递 C++11 lambda 函数
-
指针容器
-
在作用域退出时执行某些操作
-
通过派生类的成员初始化基类
简介
在本章中,我们将继续处理由 Boost 库引入的数据类型,主要关注指针的使用。我们将了解如何轻松管理资源,以及如何使用一种能够存储任何函数对象、函数和 lambda 表达式的数据类型。阅读本章后,你的代码将变得更加可靠,内存泄漏将成为历史。
管理未离开作用域的类的指针
有时候我们需要在内存中动态分配内存并构造一个类,麻烦就从这里开始了。看看下面的代码:
void foo1() {
foo_class* p = new foo_class("Some initialization data");
bool something_else_happened = some_function1(p);
if (something_else_happened) {
delete p;
return false;
}
some_function2(p);
delete p;
return true;
}
这段代码乍一看似乎是正确的。但是,如果 some_function1()
或 some_function2()
抛出异常怎么办?在这种情况下,p
不会被删除。让我们以下面的方式修复它:
void foo2() {
foo_class* p = new foo_class("Some initialization data");
try {
bool something_else_happened = some_function1(p);
if (something_else_happened) {
delete p;
return false;
}
some_function2(p);
} catch (...) {
delete p;
throw;
}
delete p;
return true;
}
现在代码看起来很丑陋且难以阅读,但却是正确的。也许我们可以做得更好。
准备工作
需要具备基本的 C++ 知识和异常期间代码的行为。
如何做到...
让我们看看 Boost.SmartPtr
库。这里有一个 boost::scoped_ptr
类,可能对你有所帮助:
#include <boost/scoped_ptr.hpp>
bool foo3() {
boost::scoped_ptr<foo_class> p(new foo_class(
"Some initialization data"));
bool something_else_happened = some_function1(p.get());
if (something_else_happened) {
return false;
}
some_function2(p.get());
return true;
}
现在,资源泄漏的可能性已经不存在了,源代码也变得更加清晰。
注意
如果你控制 some_function1()
和 some_function2()
,你可能希望重新编写它们,以便它们接受 scoped_ptr<foo_class>
(或只是一个引用)的引用,而不是 foo_class
的指针。这样的接口将更加直观。
它是如何工作的...
在析构函数中,boost::scoped_ptr<T>
将为其存储的指针调用 delete
。当抛出异常时,堆栈回溯,并调用 scoped_ptr
的析构函数。
scoped_ptr<T>
类模板是不可复制的;它只存储指向类的指针,并且不需要 T
是一个完整类型(它可以被前置声明)。一些编译器在删除不完整类型时不会发出警告,这可能导致难以检测的错误,但 scoped_ptr
(以及 Boost.SmartPtr
中的所有类)具有针对此类情况的特定编译时断言。这使得 scoped_ptr
完美地实现了 Pimpl
习惯用法。
boost::scoped_ptr<T>
函数等同于 const std::auto_ptr<T>
,但它还有一个 reset()
函数。
还有更多...
这个类非常快。在大多数情况下,编译器会将使用 scoped_ptr
的代码优化成接近我们手写的机器代码(如果编译器检测到某些函数不抛出异常,有时甚至更好)。
参见
Boost.SmartPtr
库的文档包含了许多示例以及关于所有智能指针类的其他有用信息。您可以在www.boost.org/doc/libs/1_53_0/libs/smart_ptr/smart_ptr.htm
上阅读它。
在方法间使用类指针的引用计数
想象一下,你有一些包含数据的动态分配的结构,你想要在不同的执行线程中处理它。执行此操作的代码如下:
#include <boost/thread.hpp>
#include <boost/bind.hpp>
void process1(const foo_class* p);
void process2(const foo_class* p);
void process3(const foo_class* p);
void foo1() {
while (foo_class* p = get_data()) // C way
{
// There will be too many threads soon, see
// recipe 'Executing different tasks in parallel'
// for a good way to avoid uncontrolled growth of threads
boost::thread(boost::bind(&process1, p))
.detach();
boost::thread(boost::bind(&process2, p))
.detach();
boost::thread(boost::bind(&process3, p))
.detach();
// delete p; Oops!!!!
}
}
我们不能在 while
循环的末尾释放 p
,因为它可能仍然被运行进程函数的线程使用。进程函数不能删除 p
,因为它们不知道其他线程已经不再使用它了。
准备工作
这个配方使用了 Boost.Thread
库,它不是一个仅包含头文件的库,因此你的程序需要链接到 libboost_thread
和 libboost_system
库。在继续阅读之前,请确保你理解了线程的概念。有关使用线程的配方,请参阅 参见 部分。
你还需要一些关于 boost::bind
或 std::bind
的基本知识,它们几乎是相同的。
如何做...
如你所猜,在 Boost(和 C++11)中有一个类可以帮助你处理这个问题。它被称为 boost::shared_ptr
,它可以被用作:
#include <boost/shared_ptr.hpp>
void process_sp1(const boost::shared_ptr<foo_class>& p);
void process_sp2(const boost::shared_ptr<foo_class>& p);
void process_sp3(const boost::shared_ptr<foo_class>& p);
void foo2() {
typedef boost::shared_ptr<foo_class> ptr_t;
ptr_t p;
while (p = ptr_t(get_data())) // C way
{
boost::thread(boost::bind(&process_sp1, p))
.detach();
boost::thread(boost::bind(&process_sp2, p))
.detach();
boost::thread(boost::bind(&process_sp3, p))
.detach();
// no need to anything
}
}
这方面的另一个例子如下:
#include <string>
#include <boost/smart_ptr/make_shared.hpp>
void process_str1(boost::shared_ptr<std::string> p);
void process_str2(const boost::shared_ptr<std::string>& p);
void foo3() {
boost::shared_ptr<std::string> ps = boost::make_shared<std::string>(
"Guess why make_shared<std::string> "
"is faster than shared_ptr<std::string> "
"ps(new std::string('this string'))"
);
boost::thread(boost::bind(&process_str1, ps))
.detach();
boost::thread(boost::bind(&process_str2, ps))
.detach();
}
它是如何工作的...
shared_ptr
类内部有一个原子引用计数器。当你复制它时,引用计数器会增加,当其析构函数被调用时,引用计数器会减少。当引用计数器等于零时,delete
会调用 shared_ptr
指向的对象。
现在,让我们找出在 boost::thread
(boost::bind(&process_sp1, p)
) 的情况下发生了什么。process_sp1
函数接受一个引用作为参数,那么为什么我们在退出 while
循环时它没有被释放呢?答案是简单的。bind()
返回的功能对象包含共享指针的一个副本,这意味着指向 p
的数据不会在功能对象被销毁之前被释放。
回到 boost::make_shared
,让我们看看 shared_ptr<std::string> ps(new int(0))
。在这种情况下,我们有两个 new
调用:首先是在构造一个指向整数的指针时,其次是在构造 shared_ptr
类(它使用 new
调用在堆上分配一个原子计数器)。但是,当我们使用 make_shared
构造 shared_ptr
时,只有一个 new
调用会被执行。它将分配一块内存,并在其中构造一个原子计数器和 int
对象。
还有更多...
原子引用计数器保证了shared_ptr
在多线程中的正确行为,但您必须记住,原子操作并不像非原子操作那样快。在 C++11 兼容的编译器上,您可以使用std::move
(以这种方式移动共享指针的构造函数,使得原子计数器既不增加也不减少)来减少原子操作的次数。
shared_ptr
和make_shared
类是 C++11 的一部分,并在std::
命名空间中的头文件<memory>
中声明。
参考以下内容
-
请参考第五章,多线程,以获取有关
Boost.Thread
和原子操作更多信息。 -
请参考第一章中的重新排序函数参数配方,开始编写您的应用程序,以获取有关
Boost.Bind
更多信息。 -
请参考第一章中的将值绑定为函数参数配方,开始编写您的应用程序,以获取有关
Boost.Bind
更多信息。 -
Boost.SmartPtr
库的文档包含了许多关于所有智能指针类的示例和其他有用信息。您可以在www.boost.org/doc/libs/1_53_0/libs/smart_ptr/smart_ptr.htm
上阅读它。
管理未离开作用域的数组指针
我们已经在管理未离开作用域的类的指针配方中看到了如何管理资源指针。但是,当我们处理数组时,我们需要调用delete[]
而不是简单的delete
,否则将会有内存泄漏。请看以下代码:
void may_throw1(const char* buffer);
void may_throw2(const char* buffer);
void foo() {
// we cannot allocate 10MB of memory on stack,
// so we allocate it on heap
char* buffer = new char[1024 * 1024 * 10];
// Here comes some code, that may throw
may_throw1(buffer);
may_throw2(buffer);
delete[] buffer;
}
准备工作
对于这个配方,需要了解 C++异常和模板。
如何做...
Boost.SmartPointer
库不仅包含scoped_ptr<>
类,还包含scoped_array<>
类。
#include <boost/scoped_array.hpp>
void foo_fixed() {
// so we allocate it on heap
boost::scoped_array<char> buffer(new char[1024 * 1024 * 10]);
// Here comes some code, that may throw,
// but now exception won't cause a memory leak
may_throw1(buffer.get());
may_throw2(buffer.get());
// destructor of 'buffer' variable will call delete[]
}
它是如何工作的...
它的工作方式就像一个scoped_ptr<>
类,但在析构函数中调用delete[]
而不是delete
。
还有更多...
scoped_array<>
类具有与scoped_ptr<>
相同的安全性和设计。它没有额外的内存分配,也没有虚拟函数调用。它不能被复制,也不是 C++11 的一部分。
参考以下内容
Boost.SmartPtr
库的文档包含了许多关于所有智能指针类的示例和其他有用信息。您可以在www.boost.org/doc/libs/1_53_0/libs/smart_ptr/smart_ptr.htm
上阅读它。
在方法间使用数组指针的引用计数
我们继续处理指针,我们的下一个任务是引用计数一个数组。让我们看看一个从流中获取一些数据并在不同线程中处理它的程序。执行此操作的代码如下:
#include <cstring>
#include <boost/thread.hpp>
#include <boost/bind.hpp>
void do_process(const char* data, std::size_t size);
void do_process_in_background(const char* data, std::size_t size) {
// We need to copy data, because we do not know,
// when it will be deallocated by the caller
char* data_cpy = new char[size];
std::memcpy(data_cpy, data, size);
// Starting thread of execution to process data
boost::thread(boost::bind(&do_process, data_cpy, size))
.detach();
// We cannot delete[] data_cpy, because
// do_process1 or do_process2 may still work with it
}
与在方法间使用指针的引用计数配方中出现的相同问题。
准备工作
此配方使用 Boost.Thread
库,它不是一个仅包含头文件的库,因此您的程序需要链接到 libboost_thread
和 libboost_system
库。在继续阅读之前,请确保您理解线程的概念。
您还需要了解一些关于 boost::bind
或 std::bind
的基础知识,它们几乎相同。
如何操作...
有三种解决方案。它们之间的主要区别在于 data_cpy
变量的类型和构造。这些解决方案都做了与配方开头描述的完全相同的事情,但没有内存泄漏。解决方案如下:
-
第一种解决方案:
#include <boost/shared_array.hpp> void do_process(const boost::shared_array<char>& data, std::size_t size) { do_process(data.get(), size); } void do_process_in_background_v1(const char* data, std::size_t size) { // We need to copy data, because we do not know, when // it will be deallocated by the caller boost::shared_array<char> data_cpy(new char[size]); std::memcpy(data_cpy.get(), data, size); // Starting threads of execution to process data boost::thread(boost::bind(&do_process1, data_cpy)) .detach(); // no need to call delete[] for data_cpy, because // data_cpy destructor will deallocate data when // reference count will be zero }
-
第二种解决方案:
自从 Boost 1.53 以来,
shared_ptr
本身就可以处理数组:#include <boost/shared_ptr.hpp> #include <boost/make_shared.hpp> void do_process_shared_ptr( const boost::shared_ptr<char[]>& data, std::size_t size) { do_process(data.get(), size); } void do_process_in_background_v2(const char* data, std::size_t size) { // Faster than 'First solution' boost::shared_ptr<char[]> data_cpy = boost::make_shared<char[]>(size); std::memcpy(data_cpy.get(), data, size); // Starting thread of execution to process data boost::thread(boost::bind( &do_process_shared_ptr, data_cpy, size )).detach(); // data_cpy destructor will deallocate data when // reference count will be zero }
-
第三种解决方案:
void do_process_shared_ptr2( const boost::shared_ptr<char>& data, std::size_t size) { do_process(data.get(), size); } void do_process_in_background_v3(const char* data, std::size_t size) { // Same speed as in First solution boost::shared_ptr<char> data_cpy( new char[size], boost::checked_array_deleter<char>() ); std::memcpy(data_cpy.get(), data, size); // Starting threads of execution to process data boost::thread(boost::bind( &do_process_shared_ptr2, data_cpy, size )).detach(); // data_cpy destructor will deallocate data when // reference count will be zero }
它是如何工作的...
在这些示例中的每一个,共享类都会计算引用数,并在引用数变为零时调用 delete[]
。前两个示例是微不足道的。在第三个示例中,我们为共享指针提供了一个 deleter
对象。这个 deleter
对象将代替默认的 delete
调用。这个 deleter
与 C++11 中的 std::unique_ptr
和 std::shared_ptr
中使用的相同。
还有更多...
第一种解决方案是传统的 Boost;在 Boost 1.53 之前,第二种解决方案的功能并未在 shared_ptr
中实现。
第二种解决方案是最快的(它使用的 new
调用较少),但它只能与 Boost 1.53 及更高版本一起使用。
第三种解决方案是最便携的。它可以与较老的 Boost 版本以及 C++11 STL 的 shared_ptr<>
(只需别忘了将 boost::checked_array_deleter<T>()
改为 std::default_delete<T[]>()
)一起使用。
另请参阅
Boost.SmartPtr
库的文档包含了许多示例和其他关于所有智能指针类的有用信息。您可以在www.boost.org/doc/libs/1_53_0/libs/smart_ptr/smart_ptr.htm
中了解它。
在变量中存储任何功能对象
C++ 有一种语法可以处理函数指针和成员函数指针。而且,这很好!然而,这个机制很难与功能对象一起使用。考虑当你正在开发一个库,其 API 在头文件中声明,实现则在源文件中。这个库应该有一个接受任何功能对象的函数。你将如何传递一个功能对象给它?看看下面的代码:
// Required for std::unary_function<> template
#include <functional>
// making a typedef for function pointer accepting int
// and returning nothing
typedef void (*func_t)(int);
// Function that accepts pointer to function and
// calls accepted function for each integer that it has
// It cannot work with functional objects :(
void process_integers(func_t f);
// Functional object
class int_processor: public std::unary_function<int, void> {
const int min_;
const int max_;
bool& triggered_;
public:
int_processor(int min, int max, bool& triggered)
: min_(min)
, max_(max)
, triggered_(triggered)
{}
void operator()(int i) const {
if (i < min_ || i > max_) {
triggered_ = true;
}
}
};
准备工作
在开始此配方之前,建议阅读 第一章 中关于 在容器/变量中存储任何值 的配方。
您还需要了解一些关于 boost::bind
或 std::bind
的基础知识,它们几乎相同。
如何操作...
让我们看看如何修复示例,并使 process_integers
接受功能对象:
-
有一个解决方案,它被称为
Boost.Function
库。它允许你存储任何函数、成员函数或功能对象,如果其签名与模板参数中描述的匹配:#include <boost/function.hpp> typedef boost::function<void(int)> fobject_t; // Now this function may accept functional objects void process_integers(const fobject_t& f); int main() { bool is_triggered = false; int_processor fo(0, 200, is_triggered); process_integers(fo); assert(is_triggered); }
boost::function
类有一个默认构造函数,并且处于空状态。 -
检查空/默认构造状态可以这样做:
void foo(const fobject_t& f) { // boost::function is convertible to bool if (f) { // we have value in 'f' // ... } else { // 'f' is empty // ... } }
它是如何工作的...
fobject_t
方法在其自身中存储功能对象的 数据并擦除它们的精确类型。使用以下代码中的 boost::function
对象是安全的:
bool g_is_triggered = false;
void set_functional_object(fobject_t& f) {
int_processor fo( 100, 200, g_is_triggered);
f = fo;
// fo leavs scope and will be destroyed,
// but 'f' will be usable eve inouter scope
}
这让你想起了 boost::any
类吗?它使用相同的技巧——类型擦除来存储任何函数对象。
还有更多...
Boost.Function
库有大量的优化;它可能在不进行额外内存分配的情况下存储小的函数对象,并且有优化的移动赋值运算符。它被视为 C++11 STL 库的一部分,并在 std::
命名空间中的 <functional>
头文件中定义。
但是,记住 boost::function
对编译器意味着一个优化障碍。这意味着:
std::for_each(v.begin(), v.end(),
boost::bind(std::plus<int>(), 10, _1));
将被编译器优化得更好
fobject_t f(boost::bind(std::plus<int>(), 10, _1));
std::for_each(v.begin(), v.end(), f);
这就是为什么你应该尽量避免在实际上不需要时使用 Boost.Function
。在某些情况下,C++11 的 auto
关键字可能更方便:
auto f = boost::bind(std::plus<int>(), 10, _1);
std::for_each(v.begin(), v.end(), f);
参见
-
Boost.Function
的官方文档包含更多示例、性能指标和类参考文档。你可以在www.boost.org/doc/libs/1_53_0/doc/html/function.html
了解相关信息。 -
在变量中传递函数指针 的配方。
-
在变量中传递 C++11 lambda 函数 的配方。
在变量中传递函数指针
我们正在继续之前的示例,现在我们想在 process_integeres()
方法中传递一个函数指针。我们应该只为函数指针添加重载,还是有一个更优雅的方法?
准备工作
这个配方是继续之前的配方。你必须先阅读之前的配方。
如何做到这一点...
由于 boost::function<>
也可以从函数指针构造,因此无需进行任何操作:
void my_ints_function(int i);
int main() {
process_integeres(&my_ints_function);
}
它是如何工作的...
将 my_ints_function
的指针存储在 boost::function
类中,并且对 boost::function
的调用将被转发到存储的指针。
还有更多...
Boost.Function
库为函数指针提供了良好的性能,并且它不会在堆上分配内存。然而,无论你在 boost::function
中存储什么,它都会使用 RTTI。如果你禁用 RTTI,它将继续工作,但会显著增加编译二进制文件的大小。
参见
-
Boost.Function
的官方文档包含更多示例、性能指标和类参考文档。你可以在www.boost.org/doc/libs/1_53_0/doc/html/function.html
了解相关信息。 -
*在变量中传递 C++11 lambda 函数*
配方。
在变量中传递 C++11 lambda 函数
我们将继续使用上一个示例,现在我们想要在我们的 process_integers()
方法中使用一个 lambda 函数。
准备工作
这个配方是延续前两个配方的系列。您必须先阅读它们。您还需要一个兼容 C++11 的编译器或至少一个具有 C++11 lambda 支持的编译器。
如何实现...
由于 boost::function<>
也可以与任何难度的 lambda 函数一起使用,因此无需进行任何操作:
// lambda function with no parameters that does nothing
process_integeres([](int /*i*/){});
// lambda function that stores a reference
std::deque<int> ints;
process_integeres(&ints{
ints.push_back(i);
});
// lambda function that modifies its content
std::size_t match_count = 0;
process_integeres(ints, &match_count mutable {
if (ints.front() == i) {
++ match_count;
}
ints.pop_front();
});
还有更多...
Boost.Functional
中 lambda 函数存储的性能与其他情况相同。当 lambda 表达式产生的功能对象足够小,可以放入 boost::function
的实例中时,不会执行动态内存分配。调用存储在 boost::function
中的对象的速度接近通过指针调用函数的速度。对象的复制速度接近构造 boost::function
的速度,并在类似情况下将精确地使用动态内存分配。移动对象不会分配和释放内存。
参考信息
- 关于性能和
Boost.Function
的更多信息可以在官方文档页面上找到:www.boost.org/doc/libs/1_53_0/doc/html/function.html
指针容器
有时候我们需要在容器中存储指针。例如:在容器中存储多态数据,强制容器中数据的快速复制,以及容器中操作数据的严格异常要求。在这种情况下,C++程序员有以下选择:
-
在容器中存储指针并使用运算符
delete
处理它们的销毁:#include <set> #include <algorithm> #include <boost/bind.hpp> #include <boost/type_traits/remove_pointer.hpp> #include <cassert> template <class T> struct ptr_cmp: public std::binary_function<T, T, bool> { template <class T1> bool operator()(const T1& v1, const T1& v2) const { return operator ()(*v1, *v2); } bool operator()(const T& v1, const T& v2) const { return std::less<T>()(v1, v2); } }; void example1() { std::set<int*, ptr_cmp<int> > s; s.insert(new int(1)); s.insert(new int(0)); // ... assert(**s.begin() == 0); // ... // Deallocating resources // Any exception in this code will lead to // memory leak std::for_each(s.begin(), s.end(), boost::bind(::operator delete, _1)); }
这种方法容易出错,并且需要大量编写
-
在容器中存储智能指针:
对于 C++03 版本:
void example2_a() { typedef std::auto_ptr<int> int_aptr_t; std::set<int_aptr_t, ptr_cmp<int> > s; s.insert(int_aptr_t(new int(1))); s.insert(int_aptr_t(new int(0))); // ... assert(**s.begin() == 0); // ... // resources will be deallocated by auto_ptr<> }
std::auto_ptr
方法已被弃用,不建议在容器中使用。此外,此示例在 C++11 中无法编译。对于 C++11 版本:
void example2_b() { typedef std::unique_ptr<int> int_uptr_t; std::set<int_uptr_t, ptr_cmp<int> > s; s.insert(int_uptr_t(new int(1))); s.insert(int_uptr_t(new int(0))); // ... assert(**s.begin() == 0); // ... // resources will be deallocated by unique_ptr<> }
这种解决方案是一个好方案,但不能用于 C++03,并且您仍然需要编写一个比较器功能对象
-
在容器中使用
Boost.SmartPtr
:#include <boost/shared_ptr.hpp> void example3() { typedef boost::shared_ptr<int> int_sptr_t; std::set<int_sptr_t, ptr_cmp<int> > s; s.insert(int_sptr_t(new int(1))); s.insert(int_sptr_t(new int(0))); // ... assert(**s.begin() == 0); // ... // resources will be deallocated by shared_ptr<> }
这种解决方案是可移植的,但您仍然需要编写比较器,并且它增加了性能惩罚(原子计数器需要额外的内存,其增加/减少操作不如非原子操作快)
准备工作
为了更好地理解这个配方,需要了解 STL 容器。
如何实现...
Boost.PointerContainer
库提供了一个良好且可移植的解决方案:
#include <boost/ptr_container/ptr_set.hpp>
void correct_impl() {
boost::ptr_set<int> s;
s.insert(new int(1));
s.insert(new int(0));
// ...
assert(*s.begin() == 0);
// ...
// resources will be deallocated by container itself
}
它是如何工作的...
Boost.PointerContainer
库包含 ptr_array
、ptr_vector
、ptr_set
、ptr_multimap
等类。所有这些容器都能简化你的生活。当处理指针时,它们将在析构函数中释放指针,并简化对指针所指向数据的访问(无需在 assert(*s.begin() == 0);
中进行额外的解引用)。
更多内容...
之前的示例并没有克隆指针数据,但当我们想要克隆一些数据时,我们只需要在要克隆的对象的命名空间中定义一个独立的函数,例如 new_clone()
。此外,如果你包含了 <boost/ptr_container/clone_allocator.hpp>
头文件,你可以使用默认的 T* new_clone( const T& r )
实现,如下面的代码所示:
#include <boost/ptr_container/clone_allocator.hpp>
#include <boost/ptr_container/ptr_vector.hpp>
// Creating vector of 10 elements with values 100
boost::ptr_vector<int> v;
v.resize(10, new int(100));
assert(v.size() == 10);
assert(v.back() == 100);
参见
-
官方文档包含了每个类的详细参考,你可以在
www.boost.org/doc/libs/1_53_0/libs/ptr_container/doc/ptr_container.html
上阅读相关信息。 -
本章的前四个示例将为你提供一些智能指针使用的例子
在作用域退出时执行某些操作
如果你处理的是像 Java、C# 或 Delphi 这样的语言,你显然使用了 try{} finally{}
构造或 D 语言的 scope(exit)
。让我简要地描述一下这些语言构造的功能。
当程序通过返回或异常离开当前作用域时,finally
或 scope(exit)
块中的代码将被执行。这种机制非常适合实现如以下代码片段所示的 RAII 模式:
// Some pseudo code (suspiciously similar to Java code)
try {
FileWriter f = new FileWriter("example_file.txt");
// Some code that may trow or return
// …
} finally {
// Whatever happened in scope, this code will be executed
// and file will be correctly closed
if (f != null) {
f.close()
}
}
在 C++ 中有办法做这样的事情吗?
准备工作
需要基本的 C++ 知识来完成这个示例。了解抛出异常时的代码行为将很有用。
如何实现...
Boost.ScopeExit
库被设计用来解决这类问题:
#include <boost/scope_exit.hpp>
#include <cstdlib>
#include <cstdio>
#include <cassert>
int main() {
std::FILE* f = std::fopen("example_file.txt", "w");
assert(f);
BOOST_SCOPE_EXIT(f) {
// Whatever happened in scope, this code will be
// executed and file will be correctly closed.
std::fclose(f);
} BOOST_SCOPE_EXIT_END
// Some code that may throw or return.
// ...
}
它是如何工作的...
变量 f
通过 BOOST_SCOPE_EXIT(f)
以值的方式传递。当程序离开执行范围时,BOOST_SCOPE_EXIT(f) {
和 } BOOST_SCOPE_EXIT_END
之间的代码将被执行。如果我们希望通过引用传递值,请在 BOOST_SCOPE_EXIT
宏中使用 &
符号。如果我们希望传递多个值,只需用逗号将它们分开。
注意
在某些编译器上,对指针的传递引用效果不佳。BOOST_SCOPE_EXIT(&f)
宏无法在那里编译,这就是为什么我们在示例中没有通过引用捕获它的原因。
更多内容...
要在成员函数内部捕获它,我们使用一个特殊的符号 this_
:
class theres_more_example {
public:
void close(std::FILE*);
void theres_more_example_func() {
std::FILE* f = 0;
BOOST_SCOPE_EXIT(f, this_) { // Capture object `this_`.
this_->close(f);
} BOOST_SCOPE_EXIT_END
}
};
Boost.ScopeExit
库在堆上不分配额外的内存,也不使用虚函数。使用默认语法,不要定义 BOOST_SCOPE_EXIT_CONFIG_USE_LAMBDAS
,因为否则作用域退出将使用 boost::function
实现,这可能会分配额外的内存并引入优化屏障。
参见
- 官方文档包含了更多示例和用例。你可以在
www.boost.org/doc/libs/1_53_0/libs/scope_exit/doc/html/index.html
了解相关信息。
通过派生类的成员初始化基类
让我们看看以下示例。我们有一个具有虚函数并且必须使用对 std::ostream
对象的引用进行初始化的基类:
#include <boost/noncopyable.hpp>
#include <sstream>
class tasks_processor: boost::noncopyable {
std::ostream& log_;
protected:
virtual void do_process() = 0;
public:
explicit tasks_processor(std::ostream& log)
: log_(log)
{}
void process() {
log_ << "Starting data processing";
do_process();
}
};
我们还有一个具有 std::ostream
对象并实现 do_process()
函数的派生类:
class fake_tasks_processor: public tasks_processor {
std::ostringstream logger_;
virtual void do_process() {
logger_ << "Fake processor processed!";
}
public:
fake_tasks_processor()
: tasks_processor(logger_) // Oops! logger_ does
// not exist here
, logger_()
{}
};
在编程中,这种情况并不常见,但当这种错误发生时,并不总是简单就能想到绕过它的方法。有些人试图通过改变 logger_
和基类初始化的顺序来绕过它:
fake_tasks_processor()
: logger_() // Oops! logger_ still will be constructed
// AFTER tasks_processor
, tasks_processor(logger_)
{}
它不会像他们预期的那样工作,因为直接基类在非静态数据成员之前初始化,无论成员初始化器的顺序如何。
准备工作
需要具备基本的 C++ 知识才能使用此配方。
如何做到这一点...
Boost.Utility
库为这类情况提供了一个快速解决方案;它被称为 boost::base_from_member
模板。要使用它,你需要执行以下步骤:
-
包含
base_from_member.hpp
头文件:#include <boost/utility/base_from_member.hpp>
-
从
boost::base_from_member<T>
派生你的类,其中T
是必须在基类之前初始化的类型(注意基类的顺序;boost::base_from_member<T>
必须放在使用T
的类之前):class fake_tasks_processor_fixed : boost::base_from_member<std::ostringstream> , public tasks_processor
-
正确编写构造函数如下:
{ typedef boost::base_from_member<std::ostringstream> logger_t; // ... public: fake_tasks_processor_fixed() : logger_t() , tasks_processor(logger_t::member) {} };
它是如何工作的...
如果直接基类在非静态数据成员之前初始化,并且如果直接基类会按照它们在基类指定列表中出现的声明顺序初始化,我们需要以某种方式使基类成为我们的非静态数据成员。或者创建一个具有所需成员的成员字段的基类:
template < typename MemberType, int UniqueID = 0 >class base_from_member{protected: MemberType member; // Constructors go there...};
还有更多...
正如你所见,base_from_member
有一个整数作为第二个模板参数。这是为了处理我们需要多个相同类型的 base_from_member
类的情况:
class fake_tasks_processor2
: boost::base_from_member<std::ostringstream, 0>
, boost::base_from_member<std::ostringstream, 1>
, public tasks_processor
{
typedef boost::base_from_member<std::ostringstream, 0> logger0_t;
typedef boost::base_from_member<std::ostringstream, 1> logger1_t;
virtual void do_process() {
logger0_t::member << "0: Fake processor2 processed!";
logger1_t::member << "1: Fake processor2 processed!";
}
public:
fake_tasks_processor2()
: logger0_t()
, logger1_t()
, tasks_processor(logger0_t::member)
{}
};
boost::base_from_member
类既不应用额外的动态内存分配,也没有虚函数。当前的实现不支持 C++11 特性(如完美转发和变长模板),但在 Boost 的 trunk 分支中,有一个可以充分利用 C++11 优势的实现。它可能将在最近的未来合并到发布分支中。
参见
-
Boost.Utility
库包含了许多有用的类和方法;有关获取更多信息,请参阅www.boost.org/doc/libs/1_53_0/libs/utility/utility.htm
。 -
在 第一章 的 Making a noncopyable class 配方中,Starting to Write Your Application,包含了
Boost.Utility
中类的更多示例。 -
此外,第一章中的使用 C++11 移动模拟配方包含来自
Boost.Utility
类的更多示例。
第四章. 编译时技巧
在本章中,我们将涵盖:
-
在编译时检查大小
-
启用模板函数对整型类型的用法
-
禁用模板函数对真实类型的用法
-
从数字创建一个类型
-
实现一个类型特性
-
选择模板参数的最佳运算符
-
在 C++03 中获取表达式的类型
简介
在本章中,我们将看到一些基本示例,说明如何使用 Boost 库在编译时检查、调整算法以及在其他元编程任务中。
一些读者可能会问,“我们为什么要关心编译时的事情?”这是因为程序的发布版本只编译一次,但运行多次。我们在编译时做得越多,运行时的工作就越少,从而产生更快、更可靠的程序。只有在代码中包含检查的部分被执行时,才会执行运行时检查。编译时检查不会让你编译出一个有错误的程序。
这章可能是最重要的章节之一。没有它,理解 Boost 源和其他类似 Boost 的库是不可能的。
在编译时检查大小
让我们想象我们正在编写一些序列化函数,该函数将值存储在指定大小的缓冲区中:
#include <cstring>
#include <boost/array.hpp>
template <class T, std::size_t BufSizeV>
void serialize(const T& value, boost::array<unsigned char, BufSizeV>& buffer) {
// TODO: fixme
std::memcpy(&buffer[0], &value, sizeof(value));
}
这段代码有以下问题:
-
缓冲区的大小没有被检查,所以它可能会溢出
-
这个函数可以与非平凡旧数据(POD)类型一起使用,这可能导致不正确的行为
我们可以通过添加一些断言来部分修复它,例如:
template <class T, std::size_t BufSizeV>
void serialize(const T& value, boost::array<unsigned char, BufSizeV>& buffer) {
assert(BufSizeV >= sizeof(value));
// TODO: fixme
std::memcpy(&buffer[0], &value, sizeof(value));
}
但是,这是一个不好的解决方案。BufSizeV
和sizeof(value)
的值在编译时是已知的,因此我们可以潜在地使代码在缓冲区太小的情况下无法编译,而不是有运行时断言(如果在调试期间没有调用该函数,它可能不会触发,甚至在发布模式下可能被优化掉,所以会发生非常糟糕的事情)。
准备工作
这个配方需要一些关于 C++模板和Boost.Array
库的知识。
如何做...
让我们使用Boost.StaticAssert
和Boost.TypeTraits
库来纠正解决方案,输出将如下所示:
#include <boost/static_assert.hpp>
#include <boost/type_traits/is_pod.hpp>
template <class T, std::size_t BufSizeV>
void serialize(const T& value, boost::array<unsigned char, BufSizeV>& buffer) {
BOOST_STATIC_ASSERT(BufSizeV >= sizeof(value));
BOOST_STATIC_ASSERT(boost::is_pod<T>::value);
std::memcpy(&buffer[0], &value, sizeof(value));
}
它是如何工作的...
BOOST_STATIC_ASSERT
宏只能在断言表达式可以在编译时评估并且隐式转换为bool
的情况下使用。这意味着你只能在其中使用sizeof()
、静态常量和其他常量表达式。如果断言表达式评估为false
,BOOST_STATIC_ASSERT
将停止我们的程序编译。在serialization()
函数的情况下,如果第一个静态断言失败,这意味着有人为非常小的缓冲区使用了该函数,并且该代码必须由程序员修复。C++11 标准有一个与 Boost 版本等效的static_assert
关键字。
这里有一些更多的例子:
BOOST_STATIC_ASSERT(3 >= 1);
struct some_struct { enum enum_t { value = 1}; };
BOOST_STATIC_ASSERT(some_struct::value);
template <class T1, class T2>
struct some_templated_struct {
enum enum_t { value = (sizeof(T1) == sizeof(T2))};
};
BOOST_STATIC_ASSERT((some_templated_struct<int, unsigned int>::value));
注意
如果BOOST_STATIC_ASSERT
宏的断言表达式中有逗号符号,我们必须将整个表达式用额外的括号括起来。
最后一个例子非常接近我们在serialize()
函数的第二行看到的。所以现在是我们更多地了解Boost.TypeTraits
库的时候了。这个库提供大量编译时元函数,允许我们获取类型信息并修改类型。元函数的使用看起来像boost::function_name<parameters>::value
或boost::function_name<parameters>::type
。元函数boost::is_pod<T>::value
只有在T
是 POD 类型时才会返回true
。
让我们看看更多的例子:
#include <iostream>
#include <boost/type_traits/is_unsigned.hpp>
#include <boost/type_traits/is_same.hpp>
#include <boost/type_traits/remove_const.hpp>
template <class T1, class T2>
void type_traits_examples(T1& /*v1*/, T2& /*v2*/) {
// Returns true if T1 is an unsigned number
std::cout << boost::is_unsigned<T1>::value;
// Returns true if T1 has exactly the same type, as T2
std::cout << boost::is_same<T1, T2>::value;
// This line removes const modifier from type of T1.
// Here is what will happen with T1 type if T1 is:
// const int => int
// int => int
// int const volatile => int volatile
// const int& => const int&
typedef typename boost::remove_const<T1>::type t1_nonconst_t;
}
注意
一些编译器甚至可能在没有typename
关键字的情况下编译此代码,但这种行为违反了 C++标准,因此强烈建议使用typename
。
更多内容...
BOOST_STATIC_ASSSERT
宏有一个更详细的变体,称为BOOST_STATIC_ASSSERT_MSG
,如果断言失败,它将在编译器日志(或 IDE 窗口)中输出错误消息。看看下面的代码:
template <class T, std::size_t BufSizeV>
void serialize2(const T& value, boost::array<unsigned char, BufSizeV>& buf) {
BOOST_STATIC_ASSERT_MSG(boost::is_pod<T>::value,
"This serialize2 function may be used only "
"with POD types."
);
BOOST_STATIC_ASSERT_MSG(BufSizeV >= sizeof(value),
"Can not fit value to buffer. "
"Make buffer bigger."
);
std::memcpy(&buf[0], &value, sizeof(value));
}
// Somewhere in code:
boost::array<unsigned char, 1> buf;
serialize2(std::string("Hello word"), buf);
在 C++11 模式下,使用 g++编译器编译前面的代码将给出以下结果:
../../../BoostBook/Chapter4/static_assert/main.cpp: In instantiation of 'void serialize2(const T&, boost::array<unsigned char, BufSizeV>&) [with T = std::basic_string<char>; long unsigned int BufSizeV = 1ul]':
../../../BoostBook/Chapter4/static_assert/main.cpp:77:46: required from here
../../../BoostBook/Chapter4/static_assert/main.cpp:58:5: error: static assertion failed: This serialize2 function may be used only with POD types.
../../../BoostBook/Chapter4/static_assert/main.cpp:63:5: error: static assertion failed: Can not fit value to buffer. Make buffer bigger.
BOOST_STATIC_ASSSERT
、BOOST_STATIC_ASSSERT_MSG
以及类型特性库中的任何函数都不会产生运行时惩罚。所有这些函数都是在编译时执行的,不会在二进制文件中添加任何汇编指令。
Boost.TypeTraits
库部分被纳入 C++11 标准;因此,你可能会在std::
命名空间中的<type_traits>
头文件中找到特性。C++11 <type_traits>
有一些函数在Boost.TypeTraits
中不存在,但一些元函数只在 Boost 中存在。当 Boost 和 STL 中有类似函数时,STL 版本(在罕见情况下)可能因为编译器特定的内建函数使用而稍微好一些。
如我们之前提到的,BOOST_STATIC_ASSERT_MSG
宏也被纳入 C++11(甚至 C11)作为static_assert(expression, message)
关键字。
如果你需要跨编译器的可移植性或 STL <type_traits>
中不存在的元函数,请使用这些库的 Boost 版本。
参见
-
本章接下来的食谱将给出更多关于如何使用静态断言和类型特性的例子和想法。
-
请阅读
Boost.StaticAssert
的官方文档,以获取更多示例,链接为www.boost.org/doc/libs/1_53_0/doc/html/boost_sta
ticassert.html
启用对整型模板函数的使用
这是一种常见的情况,当我们有一个实现了某些功能的模板类。看看下面的代码片段:
// Generic implementation
template <class T>
class data_processor {
double process(const T& v1, const T& v2, const T& v3);
};
在执行前面的代码之后,我们有了该类的两个额外的优化版本,一个用于整型,另一个用于实型:
// Integral types optimized version
template <class T>
class data_processor {
typedef int fast_int_t;
double process(fast_int_t v1, fast_int_t v2, fast_int_t v3);
};
// SSE optimized version for float types
template <class T>
class data_processor {
double process(double v1, double v2, double v3);
};
现在的问题是,如何让编译器自动为指定的类型选择正确的类。
准备工作
此配方需要了解 C++模板的知识。
如何做到这一点...
我们将使用 Boost.Utility
和 Boost.TypeTraits
来解决这个问题:
-
让我们从包含头文件开始:
#include <boost/utility/enable_if.hpp> #include <boost/type_traits/is_integral.hpp> #include <boost/type_traits/is_float.hpp>
-
让我们在通用实现中添加一个具有默认值的额外模板参数:
// Generic implementation template <class T, class Enable = void> class data_processor { // ... };
-
按照以下方式修改优化版本,这样编译器现在会将它们视为模板部分特化:
// Integral types optimized version template <class T> class data_processor<T, typename boost::enable_if_c< boost::is_integral<T>::value >::type> { /* ... */ }; // SSE optimized version for float types template <class T> class data_processor<T, typename boost::enable_if_c< boost::is_float<T>::value >::type> { /* ... */ };
-
就这样!现在编译器将自动选择正确的类:
template <class T> double example_func(T v1, T v2, T v3) { data_processor<T> proc; return proc.process(v1, v2, v3); } int main () { // Integral types optimized version // will be called example_func(1, 2, 3); short s = 0; example_func(s, s, s); // Real types version will be called example_func(1.0, 2.0, 3.0); example_func(1.0f, 2.0f, 3.0f); // Generic version will be called example_func("Hello", "word", "processing"); }
它是如何工作的...
boost::enable_if_c
模板是一个有点棘手的模板。它利用了 SFINAE(Substitution Failure Is Not An Error) 原则,该原则在模板实例化过程中被使用。以下是该原则的工作方式:如果在函数或类模板的实例化过程中形成了无效的参数或返回类型,则实例化将从重载解析集中移除,并且不会导致编译错误。现在让我们回到解决方案,看看它是如何与传递给 data_processor
类的 T
参数的不同类型一起工作的。
如果我们将 int
作为 T
类型传递,编译器首先会尝试实例化模板部分特化,然后再使用我们的非特化(通用)版本。当它尝试实例化一个 float
版本时,boost::is_float<T>::value
元函数将返回 false
。boost::enable_if_c<false>::type
元函数无法正确实例化(因为 boost::enable_if_c<false>
没有提供 ::type
),这就是 SFINAE 发挥作用的地方。由于类模板无法实例化,这必须被解释为不是错误,编译器将跳过这个模板特化。接下来,部分特化是针对整型类型进行优化的。boost::is_integral<T>::value
元函数将返回 true
,boost::enable_if_c<true>::type
可以实例化,这使得可以实例化整个 data_processor
特化。编译器找到了匹配的部分特化,因此它不需要尝试实例化非特化方法。
现在,让我们尝试传递一些非算术类型(例如,const char *
),看看编译器会做什么。首先编译器会尝试实例化模板部分特化。具有 is_float<T>::value
和 is_integral<T>::value
的特化将无法实例化,因此编译器将尝试实例化我们的通用版本,并且会成功。
没有使用 boost::enable_if_c<>
,对于任何类型,所有部分特化的版本都可能同时实例化,这会导致歧义和编译失败。
注意
如果你使用模板并且编译器报告无法在两个模板类的方法之间进行选择,你可能需要 boost::enable_if_c<>
。
还有更多...
这种方法的另一种版本被称为 boost::enable_if
(末尾没有 _c
)。它们之间的区别在于 enable_if_c
接受常量作为模板参数;然而,简短版本接受一个具有 value
静态成员的对象。例如,boost::enable_if_c<boost::is_integral<T>::value >::type
等于 boost::enable_if<boost::is_integral<T> >::type>
。
C++11 在 <type_traits>
头文件中定义了 std::enable_if
,其行为与 boost::enable_if_c
完全相同。它们之间没有区别,除了 Boost 的版本可以在非 C++11 编译器上工作,提供更好的可移植性。
所有启用函数仅在编译时执行,不会在运行时增加性能开销。然而,添加一个额外的模板参数可能会在 typeid(T).name()
中产生更大的类名,并且在某些平台上比较两个 typeid()
结果时可能会增加极小的性能开销。
参见
-
下一个示例将给出更多关于
enable_if
用法的示例。 -
你还可以查阅
Boost.Utility
的官方文档。它包含许多示例和许多有用的类(这些类在这本书中得到了广泛的应用)。请参阅www.boost.org/doc/libs/1_53_0/libs/utility/utility.htm
。 -
你也可以阅读一些关于模板部分特殊化的文章,请参阅
msdn.microsoft.com/en-us/library/3967w96f%28v=vs.110%29.aspx
。
禁用模板函数对真实类型的用法
我们继续使用 Boost 元编程库。在前一个示例中,我们看到了如何使用 enable_if_c
与类一起使用,现在该看看它在模板函数中的用法了。考虑以下示例。
最初,我们有一个适用于所有可用类型的模板函数:
template <class T>
T process_data(const T& v1, const T& v2, const T& v3);
现在我们使用 process_data
函数编写代码时,我们为具有 operator +=
函数的类型使用优化的 process_data
版本:
template <class T>
T process_data_plus_assign(const T& v1, const T& v2, const T& v3);
但是,我们不想改变已经编写的代码;相反,只要可能,我们希望强制编译器自动使用优化函数来替代默认函数。
准备工作
阅读前一个示例以了解 boost::enable_if_c
的作用,并理解 SFINAE 的概念。然而,仍然需要了解模板知识。
如何做到这一点...
使用 Boost 库可以完成模板魔法。让我们看看如何做:
-
我们将需要
boost::has_plus_assign<T>
元函数和<boost/enable_if.hpp>
头文件:#include <boost/utility/enable_if.hpp> #include <boost/type_traits/has_plus_assign.hpp>
-
现在我们将禁用具有加法赋值运算符的类型的默认实现:
// Modified generic version of process_data template <class T> typename boost::disable_if_c<boost::has_plus_assign<T>::value,T>::type process_data(const T& v1, const T& v2, const T& v3);
-
为具有加法赋值运算符的类型启用优化版本:
// This process_data will call a process_data_plus_assign template <class T> typename boost::enable_if_c<boost::has_plus_assign<T>::value, T>::type process_data(const T& v1, const T& v2, const T& v3) { return process_data_plus_assign(v1, v2, v3); }
-
现在,用户不会感觉到差异,但优化版本将在可能的情况下被使用:
int main() { int i = 1; // Optimized version process_data(i, i, i); // Default version // Explicitly specifing template parameter process_data<const char*>("Testing", "example", "function"); }
它是如何工作的...
boost::disable_if_c<bool_value>::type
元函数在 bool_value
等于 true
时禁用方法(与 boost::enable_if_c<!bool_value>::type
的工作方式相同)。
如果我们将一个类作为 boost::enable_if_c
或 boost::disable_if_c
的第二个参数传递,在成功评估的情况下,它将通过 ::type
返回。
让我们逐步了解模板实例化的过程。如果我们传递 int
作为 T
类型,首先编译器将搜索具有所需签名的函数重载。因为没有这样的函数,下一步将是实例化这个函数的模板版本。例如,编译器从我们的第二个(优化)版本开始;在这种情况下,它将成功评估 typename boost::enable_if_c<boost::has_plus_assign<T>::value, T>::type
表达式,并将得到 T
返回类型。但是,编译器不会停止;它将继续实例化尝试。它将尝试实例化我们的第一个函数版本,但在评估 typename boost::disable_if_c<boost::has_plus_assign<T>::value>
时将失败。这个失败不会被当作错误处理(参考 SFINAE)。正如你所看到的,没有 enable_if_c
和 disable_if_c
,将会有歧义。
还有更多...
与 enable_if_c
和 enable_if
一样,禁用函数也有 disable_if
版本:
// First version
template <class T>
typename boost::disable_if<boost::has_plus_assign<T>, T>::type
process_data2(const T& v1, const T& v2, const T& v3);
// process_data_plus_assign
template <class T>
typename boost::enable_if<boost::has_plus_assign<T>, T>::type
process_data2(const T& v1, const T& v2, const T& v3);
C++11 既没有 disable_if_c
,也没有 disable_if
(你可以使用 std::enable_if<!bool_value>::type
代替)。
如前一个食谱中提到的,所有启用和禁用函数都仅在编译时执行,不会在运行时增加性能开销。
参见
-
从头开始阅读这一章,以获取更多编译时技巧的示例。
-
考虑阅读
Boost.TypeTraits
的官方文档,以获取更多示例和元函数的完整列表。www.boost.org/doc/libs/1_53_0/libs/type_traits/doc/html/index.html
。 -
Boost.Utility
库可能为你提供了更多boost::enable_if
的使用示例。更多信息请参阅www.boost.org/doc/libs/1_53_0/libs/utility/utility.htm
。
从数字创建类型
我们已经看到了如何在没有使用 boost::enable_if_c
的情况下选择函数的例子。让我们考虑以下例子,其中我们有一个用于处理 POD 数据类型的泛型方法:
#include <boost/static_assert.hpp>
#include <boost/type_traits/is_pod.hpp>
// Generic implementation
template <class T>
T process(const T& val) {
BOOST_STATIC_ASSERT((boost::is_pod<T>::value));
// ...
}
此外,我们还有一个针对 1、4 和 8 字节大小的相同函数的优化版本。我们如何重写 process 函数,以便它可以调用优化版本?
准备工作
高度推荐至少阅读这一章的第一个食谱,这样你就不会因为这里发生的事情而感到困惑。模板和元编程不应该让你感到害怕(或者准备好看到很多它们)。
如何做到这一点...
我们将看到如何将模板类型的尺寸转换为某种类型的变量,以及如何使用该变量来推断正确的函数重载。
-
让我们定义
process_impl
函数的通用和优化版本:#include <boost/mpl/int.hpp> namespace detail { // Generic implementation template <class T, class Tag> T process_impl(const T& val, Tag /*ignore*/) { // ... } // 1 byte optimized implementation template <class T> T process_impl(const T& val, boost::mpl::int_<1> /*ignore*/) { // ... } // 4 bytes optimized implementation template <class T> T process_impl(const T& val, boost::mpl::int_<4> /*ignore*/) { // ... } // 8 bytes optimized implementation template <class T> T process_impl(const T& val, boost::mpl::int_<8> /*ignore*/) { // ... } } // namespace detail
-
现在,我们已经准备好编写过程函数:
// will be only dispatching calls template <class T> T process(const T& val) { BOOST_STATIC_ASSERT((boost::is_pod<T>::value)); return detail::process_impl( val, boost::mpl::int_<sizeof(T)>()); }
它是如何工作的...
这里最有趣的部分是 boost::mpl::int_<sizeof(T)>(). sizeof(T)
在编译时执行,因此其输出可以用作模板参数。boost::mpl::int_<>
类只是一个空的类,它持有整型类型的编译时值(在 Boost.MPL
库中,这样的类被称为积分常量)。它可以像以下代码所示实现:
template <int Value>
struct int_ {
static const int value = Value;
typedef int_<Value> type;
typedef int value_type;
};
我们需要一个此类实例,这就是为什么我们在 boost::mpl::int_<sizeof(T)>()
的末尾有一个圆括号。
现在,让我们更详细地看看编译器将如何决定使用哪个 process_impl
函数。首先,编译器将尝试匹配具有第二个参数而不是模板的函数。如果 sizeof(T)
是 4,编译器将尝试搜索具有类似 process_impl(T, boost::mpl::int_<8>)
签名的函数,并找到来自 detail
命名空间的 4 字节优化版本。如果 sizeof(T)
是 34,编译器将找不到具有类似 process_impl(T, boost::mpl::int_<34>)
签名的函数,并将使用模板变体 process_impl(const T& val, Tag /*ignore*/)
。
还有更多...
Boost.MPL
库有几个元编程的数据结构。在这个菜谱中,我们只是触及了冰山一角。您可能会发现以下来自 MPL 的积分常量类很有用:
-
bool_
-
int_
-
long_
-
size_t
-
char_
所有的 Boost.MPL
函数(除了 for_each
运行时函数)都是在编译时执行的,不会增加运行时开销。Boost.MPL
库不是 C++11 的一部分,但许多 STL 库为了满足自己的需求,实现了它的一些函数。
参见
-
第八章 Metaprogramming 中的菜谱将为您提供更多
Boost.MPL
库使用的示例。如果您有信心,您也可以尝试阅读其文档,www.boost.org/doc/libs/1_53_0/libs/mpl/doc/index.html
。 -
在
www.boost.org/doc/libs/1_53_0/libs/type_traits/doc/html/boost_typetraits/examples/fill.html
和www.boost.org/doc/libs/1_53_0/libs/type_traits/doc/html/boost_typetraits/examples/copy.html
上阅读更多关于标签使用的示例。
实现类型特性
我们需要实现一个类型特性,当它作为模板参数传递 std::vector
类型时,返回 true。
准备工作
需要一些关于 Boost.TypeTrait
或 STL 类型特性的基本知识。
如何实现...
让我们看看如何实现一个类型特性:
#include <vector>
#include <boost/type_traits/integral_constant.hpp>
template <class T>
struct is_stdvector: boost::false_type {};
template <class T, class Allocator>
struct is_stdvector<std::vector<T, Allocator> >: boost::true_type {};
它是如何工作的...
几乎所有的工作都是由 boost::true_type
和 boost::false_type
类完成的。boost::true_type
类中有一个布尔 ::value
静态常量,其值等于 true
,而 boost::false_type
类中有一个布尔 ::value
静态常量,其值等于 false
。它们还有一些 typedef,通常是从 boost::mpl::integral_c
派生出来的,这使得使用从 true_type/false_type
派生的类型与 Boost.MPL
一起使用变得容易。
我们第一个 is_stdvector
结构是一个通用的结构,当找不到此类结构的模板特化版本时,总是会被使用。我们的第二个 is_stdvector
结构是 std::vector
类型的模板特化(注意,它是从 true_type
派生出来的!)所以,当我们向 is_stdvector
结构传递向量类型时,将使用模板特化版本,否则将使用通用版本,它是从 false_type
派生出来的。
注意
3 行 在我们的特性中,boost::false_type
和 boost::true_type
前面没有公共关键字,因为我们使用了 struct
关键字,并且默认使用公共继承。
更多...
那些使用与 C++11 兼容的编译器的读者可以使用 std::
命名空间中声明的 <type_traits>
头文件中的 true_type
和 false_type
类型来创建他们自己的类型特性。
如同往常,Boost 版本更具有可移植性,因为它可以在 C++03 编译器上使用。
参见
- 本章中的几乎所有配方都使用了类型特性。有关更多示例和信息,请参阅
www.boost.org/doc/libs/1_53_0/libs/type_traits/doc/html/i
ndex.html。
选择模板参数的最佳运算符
想象一下,我们正在使用来自不同供应商的类,这些类实现了不同数量的算术运算,并且具有从整数构造函数。我们确实想编写一个函数,当传递任何类给它时,它会增加一个。我们还希望这个函数是有效的!看看下面的代码:
template <class T>
void inc(T& value) {
// call ++value
// or call value ++
// or value += T(1);
// or value = value + T(1);
}
准备工作
需要一些关于 C++ 模板和 Boost.TypeTrait
或 STL 类型特性的基本知识。
如何做...
所有选择都可以在编译时完成。这可以通过使用 Boost.TypeTraits
库来实现,如下面的步骤所示:
-
让我们从制作正确的函数对象开始:
namespace detail { struct pre_inc_functor { template <class T> void operator()(T& value) const { ++ value; } }; struct post_inc_functor { template <class T> void operator()(T& value) const { value++; } }; struct plus_assignable_functor { template <class T> void operator()(T& value) const { value += T(1); } }; struct plus_functor { template <class T> void operator()(T& value) const { value = value + T(1); } }; }
-
之后我们将需要一系列类型特性:
#include <boost/type_traits/conditional.hpp> #include <boost/type_traits/has_plus_assign.hpp> #include <boost/type_traits/has_plus.hpp> #include <boost/type_traits/has_post_increment.hpp> #include <boost/type_traits/has_pre_increment.hpp>
-
然后,我们就准备好推导出正确的函数对象并使用它:
template <class T> void inc(T& value) { typedef detail::plus_functor step_0_t; typedef typename boost::conditional< boost::has_plus_assign<T>::value, detail::plus_assignable_functor, step_0_t >::type step_1_t; typedef typename boost::conditional< boost::has_post_increment<T>::value, detail::post_inc_functor, step_1_t >::type step_2_t; typedef typename boost::conditional< boost::has_pre_increment<T>::value, detail::pre_inc_functor, step_2_t >::type step_3_t; step_3_t() // default constructing functor (value); // calling operator() of a functor }
它是如何工作的...
所有魔法都是通过 conditional<bool Condition, class T1, class T2>
元函数完成的。当这个元函数接受 true
作为第一个参数时,它通过 ::type
typedef 返回 T1
。当 boost::conditional
元函数接受 false
作为第一个参数时,它通过 ::type
typedef 返回 T2
。它就像某种编译时 if
语句。
因此,step0_t
包含一个detail::plus_functor
元函数,而step1_t
将包含step0_t
或detail::plus_assignable_functor
。step2_t
类型将包含step1_t
或detail::post_inc_functor
。step3_t
类型将包含step2_t
或detail::pre_inc_functor
。每个step*_t
类型定义包含的内容是通过类型特性推导得出的。
还有更多...
这个函数有一个 C++11 版本,可以在std::
命名空间中的<type_traits>
头文件中找到。Boost 在不同的库中有这个函数的多个版本,例如,Boost.MPL
有boost::mpl::if_c
函数,它的工作方式与boost::conditional
完全相同。它还有一个版本boost::mpl::if_
(没有c
结尾),它将为第一个模板参数调用::type
;如果它派生自boost::true_type
(或是一个boost::true_type
类型),在::type
调用期间将返回其第二个参数,否则将返回最后一个模板参数。我们可以将我们的inc()
函数重写为使用Boost.MPL
,如下面的代码所示:
#include <boost/mpl/if.hpp>
template <class T>
void inc_mpl(T& value) {
typedef detail::plus_functor step_0_t;
typedef typename boost::mpl::if_<
boost::has_plus_assign<T>,
detail::plus_assignable_functor,
step_0_t
>::type step_1_t;
typedef typename boost::mpl::if_<
boost::has_post_increment<T>,
detail::post_inc_functor,
step_1_t
>::type step_2_t;
typedef typename boost::mpl::if_<
boost::has_pre_increment<T>,
detail::pre_inc_functor,
step_2_t
>::type step_3_t;
step_3_t() // default constructing functor
(value); // calling operator() of a functor
}
参考阅读
-
食谱启用模板函数对整型类型的用法
-
食谱禁用模板函数对真实类型的用法
-
Boost.TypeTraits
文档有一个完整的可用元函数列表。请阅读它,网址为www.boost.org/doc/libs/1_53_0/libs/type_traits/doc/html/index.html
。 -
第八章中的元编程食谱将为你提供更多
Boost.MPL
库使用的示例。如果你感到自信,你也可以尝试阅读其文档,网址为www.boost.org/doc/libs/1_53_0/libs/mpl/doc/index.html
。 -
有一个提议要为 C++添加类型切换,你可能对此感兴趣。请阅读它,网址为
www.stroustrup.com/OOPSLA-ty
peswitch-draft.pdf。
在 C++03 中获取表达式的类型
在之前的食谱中,我们看到了一些关于boost::bind
使用的示例。这是一个好用的工具,但有一个小缺点;在 C++03 中很难将boost::bind
元函数的仿函数作为变量存储。
#include <functional>
#include <boost/bind.hpp>
const ??? var = boost::bind(std::plus<int>(), _1, _1);
在 C++11 中,我们可以使用auto
关键字代替???
,并且这会起作用。在 C++03 中有没有办法做到这一点?
准备工作
C++11 的auto
和decltype
关键字的知识可能有助于你理解这个食谱。
如何做到这一点...
我们需要一个Boost.Typeof
库来获取表达式的返回类型:
#include <boost/typeof/typeof.hpp>
BOOST_AUTO(var, boost::bind(std::plus<int>(), _1, _1));
它是如何工作的...
它只是创建了一个名为var
的变量,并将表达式的值作为第二个参数传递。var
的类型由表达式的类型检测得出。
还有更多...
经验丰富的 C++11 读者会注意到,新标准中有更多关键字用于检测表达式类型。也许Boost.Typeof
也有针对它们的宏。让我们看看以下 C++11 代码:
typedef decltype(0.5 + 0.5f) type;
使用 Boost.Typeof
,前面的代码可以写成以下形式:
typedef BOOST_TYPEOF(0.5 + 0.5f) type;
C++11 版本的 decltype(expr)
会推导并返回 expr
的类型。
template<class T1, class T2>
auto add(const T1& t1, const T2& t2) ->decltype(t1 + t2) {
return t1 + t2;
};
使用 Boost.Typeof
,前面的代码可以写成以下形式:
template<class T1, class T2>
BOOST_TYPEOF_TPL(T1() + T2()) add(const T1& t1, const T2& t2) {
return t1 + t2;
};
注意
C++11 在函数声明末尾有特殊的语法来指定返回类型。不幸的是,这在 C++03 中无法模拟,所以我们不能在宏中使用 t1
和 t2
变量。
你可以在模板和任何其他编译时表达式中自由使用 BOOST_TYPEOF()
函数的结果:
#include <boost/static_assert.hpp>
#include <boost/type_traits/is_same.hpp>
BOOST_STATIC_ASSERT((boost::is_same<BOOST_TYPEOF(add(1, 1)), int>::value));
但不幸的是,这种魔法并不总是不需要帮助就能工作。例如,用户定义的类并不总是会被检测到,因此以下代码在某些编译器上可能会失败:
namespace readers_project {
template <class T1, class T2, class T3>
struct readers_template_class{};
}
#include <boost/tuple/tuple.hpp>
typedef
readers_project::readers_template_class<int, int, float>
readers_template_class_1;
typedef BOOST_TYPEOF(boost::get<0>(
boost::make_tuple(readers_template_class_1(), 1)
)) readers_template_class_deduced;
BOOST_STATIC_ASSERT((
boost::is_same<
readers_template_class_1,
readers_template_class_deduced
>::value
));
在这种情况下,你可以给 Boost.Typeof
提供帮助,并注册一个模板:
BOOST_TYPEOF_REGISTER_TEMPLATE(
readers_project::readers_template_class /*class name*/,
3 /*number of template classes*/
)
然而,三个最受欢迎的编译器即使在没有 BOOST_TYPEOF_REGISTER_TEMPLATE
和没有 C++11 的情况下也能正确检测类型。
参见
-
Boost.Typeof
的官方文档中有更多示例。有关信息请参阅www.boost.org/doc/libs/1_53_0/doc/html/typeof.html
。 -
Bjarne Stroustrup 可能会向你介绍一些 C++11 的特性。有关信息请参阅
www.stroustrup.com/C++11FAQ.html
。
第五章:多线程
在本章中,我们将涵盖:
-
创建执行线程
-
同步访问公共资源
-
使用原子操作快速访问公共资源
-
创建工作队列类
-
多读单写锁
-
创建每个线程唯一的变量
-
中断线程
-
操作线程组
简介
在本章中,我们将处理线程及其相关内容。鼓励读者具备基本的多线程知识。
多线程意味着在单个进程中存在多个执行线程。线程可以共享进程资源并拥有自己的资源。这些执行线程可以在不同的 CPU 上独立运行,从而实现更快和更响应的程序。
Boost.Thread
库为操作系统接口提供了跨平台的统一性,用于处理线程。它不是一个仅包含头文件的库,因此本章中的所有示例都需要链接到 libboost_thread
和 libboost_system
库。
创建执行线程
在现代多核编译器上,为了实现最大性能(或仅仅提供良好的用户体验),程序通常必须使用多个执行线程。以下是一个激励示例,其中我们需要在绘制用户界面的线程中创建和填充一个大文件:
#include <algorithm>
#include <fstream>
#include <iterator>
void set_not_first_run();
bool is_first_run();
// Function, that executes for a long time
void fill_file_with_data(char fill_char, std::size_t size, const char* filename){
std::ofstream ofs(filename);
std::fill_n(std::ostreambuf_iterator<char>(ofs), size, fill_char);
set_not_first_run();
}
// ...
// Somewhere in thread that draws a user interface
if (is_first_run()) {
// This will be executing for a long time during which
// users interface will freeze..
fill_file_with_data(0, 8 * 1024 * 1024, "save_file.txt");
}
准备工作
此配方需要了解 boost::bind
库。
如何做到这一点...
启动执行线程从未如此简单:
#include <boost/thread.hpp>
// ...
// Somewhere in thread that draws a user interface
if (is_first_run()) {
boost::thread(boost::bind(
&fill_file_with_data,
0,
8 * 1024 * 1024,
"save_file.txt"
)).detach();
}
如何工作...
boost::thread
变量接受一个可以无参数调用的函数对象(我们使用 boost::bind
提供了一个),并创建一个单独的执行线程。该函数对象将被复制到构建的执行线程中并在那里运行。
注意
在所有使用 Boost.Thread
库的配方中,我们将默认使用线程的版本 4(定义 BOOST_THREAD_VERSION
为 4)并指出 Boost.Thread
版本之间的一些重要差异。
之后,我们调用 detach()
函数,它将执行以下操作:
-
执行线程将从
boost::thread
变量中分离,但将继续其执行 -
boost::thread
变量将保持Not-A-Thread
状态
注意,如果没有调用 detach()
,boost::thread
的析构函数会注意到它仍然持有线程,并将调用 std::terminate
,这将终止我们的程序。
默认构造的线程也将具有 Not-A-Thread
状态,并且它们不会创建单独的执行线程。
更多...
如果我们想在执行其他工作之前确保文件已被创建并写入,我们需要使用以下方法连接线程:
// ...
// Somewhere in thread that draws a user interface
if (is_first_run()) {
boost::thread t(boost::bind(
&fill_file_with_data,
0,
8 * 1024 * 1024,
"save_file.txt"
));
// Do some work
// ...
// Waiting for thread to finish
t.join();
}
在线程连接后,boost::thread
变量将保持 Not-A-Thread
状态,其析构函数不会调用 std::terminate
。
注意
记住,在调用析构函数之前,线程必须被连接或分离。否则,你的程序将终止!
注意,当任何非boost::thread_interrupted
类型的异常离开功能对象的边界并传递给boost::thread
构造函数时,会调用std::terminate()
。
boost::thread
类被接受为 C++11 标准的一部分,你可以在std::
命名空间中的<thread>
头文件中找到它。默认情况下,当BOOST_THREAD_VERSION=2
时,boost::thread
的析构函数将调用detach()
,这不会导致std::terminate
。但是这样做会破坏与std::thread
的兼容性,而且有一天,当你的项目转移到 C++标准库线程或者当BOOST_THREAD_VERSION=2
不再被支持时,这会给你带来很多惊喜。Boost.Thread
的版本 4 更加明确和强大,这在 C++语言中通常是首选的。
有一个非常有用的包装器,它作为线程的 RAII 包装器工作,允许你模拟BOOST_THREAD_VERSION=2
的行为;它被称为boost::scoped_thread<T>
,其中T
可以是以下类之一:
-
boost::interrupt_and_join_if_joinable
: 在析构时中断并连接线程 -
boost::join_if_joinable
: 在析构时连接一个线程 -
boost::detach
: 在析构时分离一个线程
这里有一个小例子:
#include <boost/thread/scoped_thread.hpp>
void some_func();
void example_with_raii() {
boost::scoped_thread<boost::join_if_joinable> t(
(boost::thread(&some_func))
);
// 't' will be joined at scope exit
}
注意
我们在(boost::thread(&some_func))
周围添加了额外的括号,这样编译器就不会将其解释为函数声明而不是变量构造。
Boost
和 C++11 STL 版本的thread
类之间没有太大区别;然而,boost::thread
在 C++03 编译器上可用,因此它的使用更加灵活。
参见
-
本章中的所有配方都将使用
Boost.Thread
;你可以继续阅读以获取更多关于它们的信息 -
官方文档列出了
boost::thread
的所有方法和关于它们在 C++11 STL 实现中可用性的说明;它可以在www.boost.org/doc/libs/1_53_0/doc/html/thread.html
找到。 -
“中断线程”的配方将给你一个关于
boost::interrupt_and_join_if_joinable
类所做事情的概念。
同步访问公共资源
现在我们知道了如何启动执行线程,我们希望从不同的线程访问一些公共资源:
#include <cassert>
#include <cstddef>
// In previous recipe we included
// <boost/thread.hpp>, which includes all
// the classes of Boost.Thread
#include <boost/thread/thread.hpp>
int shared_i = 0;
void do_inc() {
for (std::size_t i = 0; i < 30000; ++i) {
// do some work
// ...
const int i_snapshot = ++ shared_i;
// do some work with i_snapshot
// ...
}
}
void do_dec() {
for (std::size_t i = 0; i < 30000; ++i) {
// do some work
// ...
const int i_snapshot = -- shared_i;
// do some work with i_snapshot
// ...
}
}
void run() {
boost::thread t1(&do_inc);
boost::thread t2(&do_dec);
t1.join();
t2.join();
// assert(shared_i == 0); // Oops!
std::cout << "shared_i == " << shared_i;
}
这个'Oops!'
并不是无意中写上去的。对某些人来说,这可能是个惊喜,但有很大可能性shared_i
不会等于 0:
shared_i == 19567
注意
现代编译器和处理器有大量不同且复杂的优化,这些优化可能会破坏前面的代码。我们在这里不会讨论它们,但在“参见”部分有一个有用的链接,指向一个简要描述它们的文档。
而在公共资源包含一些非平凡类的情况下,情况会更糟;可能会(并且将会)发生段错误和内存泄漏。
我们需要修改代码,使得只有一个线程在某一时刻修改 shared_i
变量,并且绕过所有影响多线程代码的处理器和编译器优化。
准备工作
建议对线程有基本了解才能理解这个食谱。
如何做到这一点...
让我们看看如何修复前面的示例,并使 shared_i
在运行结束时相等:
-
首先,我们需要创建一个互斥锁:
#include <boost/thread/mutex.hpp> #include <boost/thread/locks.hpp> int shared_i = 0; boost::mutex i_mutex;
-
将修改或从
shared_i
变量获取数据的所有操作放在以下内容之间:{ // Critical section begin boost::lock_guard<boost::mutex> lock(i_mutex);
以及以下内容:
} // Critical section end
它看起来是这样的:
void do_inc() {
for (std::size_t i = 0; i < 30000; ++i) {
// do some work
// …
int i_snapshot;
{ // Critical section begin
boost::lock_guard<boost::mutex> lock(i_mutex);
i_snapshot = ++ shared_i;
} // Critical section end
// do some work with i_snapshot
// ...
}
}
void do_dec() {
for (std::size_t i = 0; i < 30000; ++i) {
// do some work
// ...
int i_snapshot;
{ // Critical section begin
boost::lock_guard<boost::mutex> lock(i_mutex);
i_snapshot = -- shared_i;
} // Critical section end
// do some work with i_snapshot
// ...
}
}
工作原理...
boost::mutex
类负责处理所有的同步问题。当一个线程尝试通过 boost::lock_guard<boost::mutex>
变量来锁定它,并且没有其他线程持有锁时,它将成功获取对代码段的独占访问权,直到锁被解锁或销毁。如果其他线程已经持有锁,尝试获取锁的线程将等待直到另一个线程解锁。所有的锁定/解锁操作都隐含了特定的指令,以确保在临界区中做出的更改对所有线程都是可见的。此外,你也不再需要确保修改后的资源值对所有核心都是可见的,并且不仅仅是在处理器的寄存器中修改,以及强制处理器和编译器不重新排序指令。
boost::lock_guard
类是一个非常简单的 RAII 类,它存储对互斥锁的引用,并在单参数构造函数中调用 lock()
,在析构函数中调用 unlock()
。注意前面示例中的花括号使用;lock
变量是在其中构造的,这样当达到 critical section
结束括号时,lock
变量的析构函数将被调用,互斥锁将被解锁。即使临界区中发生异常,互斥锁也会被正确解锁。
注意
如果你有一些资源被不同的线程使用,通常所有使用它们的代码都必须被视为临界区,并由互斥锁来保护。
还有更多...
锁定互斥锁可能是一个非常慢的操作,这可能会导致你的代码长时间停止,直到其他线程释放锁。尽量使临界区尽可能小,并尽量减少代码中的临界区数量。
让我们看看一些操作系统(OS)如何在多核 CPU 上处理锁定。当 thread #1
在 CPU1 上运行并尝试锁定另一个线程已锁定的互斥量时,thread #1
会被操作系统停止,直到锁被释放。被停止的线程不会消耗处理器资源,因此操作系统仍然会在 CPU1 上执行其他线程。现在我们在 CPU1 上有一些线程正在运行;其他某个线程释放了锁,现在操作系统必须恢复 thread #1
的执行。所以它将在当前空闲的 CPU 上恢复执行,例如,CPU2。这将导致 CPU 缓存未命中,并且在互斥量释放后代码将运行得略慢。这是减少关键区数量另一个原因。然而,事情并不那么糟糕,因为一个好的操作系统会尝试在之前使用的相同 CPU 上恢复线程。
不要尝试在同一个线程中两次锁定一个 boost::mutex
变量;这将导致死锁。如果需要从单个线程多次锁定互斥量,请使用 boost::recursive_mutex
而不是 <boost/thread/recursive_mutex.hpp>
头文件。多次锁定它不会导致死锁。boost::recursive_mutex
只在每次 lock()
调用后对每个 unlock()
调用释放锁。避免使用 boost::recursive_mutex
;它比 boost::mutex
慢,通常表示代码流程设计不佳。
boost::mutex
、boost::recursive_mutex
和 boost::lock_guard
类被纳入 C++11 标准,你可以在 std::
命名空间中的 <mutex>
头文件中找到它们。Boost 和 STL 版本之间没有太大区别;Boost 版本可能有一些扩展(这些扩展在官方文档中被标记为 EXTENSION),并且提供更好的可移植性,因为它们甚至可以在 C++03 编译器上使用。
另请参阅
-
下一个示例将给你一些想法,如何使这个例子更快(更短)。
-
阅读本章的第一个示例以获取更多关于
boost::thread
类的信息。Boost.Thread
的官方文档也可能有所帮助;它可以在www.boost.org/doc/libs/1_53_0/doc/html/thread.html
找到。 -
更多关于第一个示例为何会失败以及多处理器如何与公共资源协同工作的信息,请参阅 《Memory Barriers: a Hardware View for Software Hackers》,可在
www.rdrop.com/users/paulmck/scalability/paper/whymb.2010.07.23a.pdf
查看。
使用原子操作快速访问公共资源
在前面的示例中,我们看到了如何从不同的线程安全地访问一个公共资源。但在那个示例中,我们只是为了从一个整数中获取值,就做了两次系统调用(在锁定和解锁互斥量时):
{ // Critical section begin
boost::lock_guard<boost::mutex> lock(i_mutex);
i_snapshot = ++ shared_i;
} // Critical section end
这看起来很糟糕!而且很慢!我们能否使前面示例中的代码更好?
准备工作
阅读第一个食谱就是开始这个的起点。或者,只需要一些关于多线程的基本知识。
如何做到这一点...
让我们看看如何改进我们之前的示例:
-
我们将需要不同的头文件:
#include <cassert> #include <cstddef> #include <boost/thread/thread.hpp> #include <boost/atomic.hpp>
-
需要更改
shared_i
的类型(因为它在互斥锁中不再需要):boost::atomic<int> shared_i(0);
-
移除所有的
boost::lock_guard
变量:void do_inc() { for (std::size_t i = 0; i < 30000; ++i) { // do some work // ... const int i_snapshot = ++ shared_i; // do some work with i_snapshot // ... } } void do_dec() { for (std::size_t i = 0; i < 30000; ++i) { // do some work // ... const int i_snapshot = -- shared_i; // do some work with i_snapshot // ... } }
就这样!现在它工作了。
int main() { boost::thread t1(&do_inc); boost::thread t2(&do_dec); t1.join(); t2.join(); assert(shared_i == 0); std::cout << "shared_i == " << shared_i << std::endl; }
如何工作...
处理器提供特定的原子操作,这些操作不会被其他处理器或处理器核心干扰。对于系统来说,这些操作似乎瞬间发生。Boost.Atomic
提供围绕系统特定原子操作的类,并提供一个统一且可移植的接口来与之交互。
换句话说,可以安全地在不同的线程中同时使用boost::atomic<>
变量。对原子变量的每次操作都会被系统视为一个单独的事务。对原子变量的操作序列将被系统视为一系列事务:
-- shared_i; // Transaction #1
// Some other thread may work here with shared_i and change its value
++shared_i; // Transaction #2
还有更多...
Boost.Atomic
库只能与 POD 类型一起工作;否则,其行为是未定义的。一些平台/处理器可能不提供某些类型的原子操作,因此Boost.Atomic
将使用boost::mutex
来模拟原子行为。如果类型特定的宏设置为2
,则原子类型不会使用boost::mutex
:
#include <boost/static_assert.hpp>
BOOST_STATIC_ASSERT(BOOST_ATOMIC_INT_LOCK_FREE == 2);
boost::atomic<T>::is_lock_free
成员函数依赖于运行时,因此它不适合编译时检查,但在运行时检查足够的情况下,它可能提供更易读的语法:
assert(shared_i.is_lock_free());
原子操作比互斥锁快得多。如果我们比较使用互斥锁的食谱的执行时间(0:00.08 秒)和这个食谱中前一个示例的执行时间(0:00.02 秒),我们会看到差异(在 3,00,000 次迭代中进行了测试)。
C++11 编译器应该在std::
命名空间中的<atomic>
头文件中包含所有的原子类、typedefs 和宏。如果编译器正确支持 C++11 内存模型,并且原子操作不再是编译器的障碍,那么特定编译器的std::atomic
实现可能比 Boost 版本运行得更快。
参见
-
官方文档可能会给你提供更多关于这个主题的示例和一些理论信息;它可以在
www.boost.org/doc/libs/1_53_0/doc/html/atomic.html
找到 -
关于原子操作如何工作的更多信息,请参阅
www.rdrop.com/users/paulmck/scalability/paper/whymb.2010.07.23a.pdf
上的Memory Barriers: a Hardware View for Software Hackers
创建一个work_queue
类
让我们称这个不接受任何参数的功能对象(简称为任务)。
typedef boost::function<void()> task_t;
现在,想象一下我们有两种类型的线程:一种是发布任务的线程,另一种是执行已发布任务的线程。我们需要设计一个可以被这两种类型的线程安全使用的类。这个类必须具有获取任务(或阻塞并等待任务,直到另一个线程发布它)的方法,检查和获取任务(如果没有任务剩余,则返回空任务),以及发布任务的方法。
准备工作
确保你对boost::thread
或std::thread
感到舒适,并且了解互斥锁的一些基础知识。
如何做到这一点...
我们将要实现的类在功能上将与std::queue<task_t>
相似,并且也将具有线程同步。让我们开始:
-
我们需要以下头文件和成员:
#include <deque> #include <boost/function.hpp> #include <boost/thread/mutex.hpp> #include <boost/thread/locks.hpp> #include <boost/thread/condition_variable.hpp> class work_queue { public: typedef boost::function<void()> task_type; private: std::deque<task_type> tasks_; boost::mutex tasks_mutex_; boost::condition_variable cond_;
-
将任务放入队列的函数看起来像这样:
public: void push_task(const task_type& task) { boost::unique_lock<boost::mutex> lock(tasks_mutex_); tasks_.push_back(task); lock.unlock(); cond_.notify_one(); }
-
用于获取已推送任务或空任务(如果没有任务剩余)的非阻塞函数:
task_type try_pop_task() { task_type ret; boost::lock_guard<boost::mutex> lock(tasks_mutex_); if (!tasks_.empty()) { ret = tasks_.front(); tasks_.pop_front(); } return ret; }
-
用于获取已推送任务或阻塞直到另一个线程推送任务的阻塞函数:
task_type pop_task() { boost::unique_lock<boost::mutex> lock(tasks_mutex_); while (tasks_.empty()) { cond_.wait(lock); } task_type ret = tasks_.front(); tasks_.pop_front(); return ret; } };
这就是
work_queue
类可能的使用方式:#include <boost/thread/thread.hpp> work_queue g_queue; void do_nothing(){} const std::size_t tests_tasks_count = 3000; void pusher() { for (std::size_t i = 0; i < tests_tasks_count; ++i) { // Adding task to do nothing g_queue.push_task(&do_nothing); } } void popper_sync() { for (std::size_t i = 0; i < tests_tasks_count; ++i) { g_queue.pop_task() // Getting task (); // Executing task } } int main() { boost::thread pop_sync1(&popper_sync); boost::thread pop_sync2(&popper_sync); boost::thread pop_sync3(&popper_sync); boost::thread push1(&pusher); boost::thread push2(&pusher); boost::thread push3(&pusher); // Waiting for all the tasks to pop pop_sync1.join(); pop_sync2.join(); pop_sync3.join(); push1.join(); push2.join(); push3.join(); // Asserting that no tasks remained, // and falling though without blocking assert(!g_queue.try_pop_task()); g_queue.push_task(&do_nothing); // Asserting that there is a task, // and falling though without blocking assert(g_queue.try_pop_task()); }
工作原理...
在这个例子中,我们将看到一个新的 RAII 类boost::unique_lock
。它只是具有附加功能的boost::lock_guard
类;例如,它具有显式解锁和锁定互斥锁的方法。
回到我们的work_queue
类,让我们从pop_task()
函数开始。一开始,我们获取一个锁并检查是否有可用的任务。如果有任务,我们返回它;否则,调用cond_.wait(lock)
。此方法将解锁锁并暂停执行线程,直到其他线程通知当前线程。
现在,让我们看看push_task
方法。在其中,我们也获取了一个锁,将任务推送到tasks_.queue
,解锁锁,并调用cond_notify_one()
,这将唤醒在cond_wait(lock)
中等待的线程(如果有)。所以,在那之后,如果某个线程在pop_task()
方法中等待一个条件变量,该线程将继续执行,在cond_wait(lock)
深处调用lock.lock()
,并在 while 循环中检查tasks_empty()
。因为我们刚刚在tasks_
中添加了一个任务,所以我们将退出while
循环,解锁互斥锁(lock
变量将超出作用域),并返回一个任务。
注意
强烈建议你在循环中检查条件,而不仅仅是if
语句。如果thread #1
在thread #2
推送任务之后弹出任务,但thread #3
在它(thread #3
)开始等待之前被thread #2
通知,那么if
语句将导致错误。
还有更多...
注意,我们在调用notify_one()
之前明确解锁了互斥锁。如果没有解锁,我们的示例仍然可以工作。
但是,在这种情况下,唤醒的线程可能在尝试在cond_wait(lock)
深处调用lock.lock()
时再次被阻塞,这会导致更多的上下文切换和更差的表现。
当将 tests_tasks_count
设置为 3000000
且没有明确解锁时,此示例运行时间为 7 秒:
$time -f E ./work_queue
0:07.38
使用显式解锁,此示例运行时间为 5 秒:
$ time -f E ./work_queue
0:05.39
你也可以使用 cond_notify_all()
通知等待特定条件变量的所有线程。
C++11 标准在 <condition_variable>
头文件中声明了 std::condition_variable
,在 <mutex>
头文件中声明了 std::unique_lock
。如果你需要可移植的行为,使用 Boost 版本,使用 C++03 编译器,或者只是使用一些 Boost 的扩展。
参见
-
本章的前三个食谱提供了关于
Boost.Thread
的许多有用信息 -
官方文档可能会给你提供更多示例以及一些关于该主题的理论信息;它可以在
www.boost.org/doc/libs/1_53_0/doc/html/thread.html
找到
多读单写锁
想象一下我们正在开发一些在线服务。我们有一个注册用户的映射,每个用户有一些属性。这个集合被许多线程访问,但它很少被修改。所有对以下集合的操作都是以线程安全的方式完成的:
#include <map>
#include <boost/thread/mutex.hpp>
#include <boost/thread/locks.hpp>
struct user_info {
std::string address;
unsigned short age;
// Other parameters
// ...
};
class users_online {
typedef boost::mutex mutex_t;
mutable mutex_t users_mutex_;
std::map<std::string, user_info> users_;
public:
bool is_online(const std::string& username) const {
boost::lock_guard<mutex_t> lock(mutex_);
return users_.find(username) != users_.end();
}
unsigned short get_age(const std::string& username) const {
boost::lock_guard<mutex_t> lock(mutex_);
return users_.at(username).age;
}
void set_online(const std::string& username, const user_info& data) {
boost::lock_guard<mutex_t> lock(mutex_);
users_.insert(std::make_pair(username, data));
}
// Other methods
// ...
};
但任何操作都会在 mutex_
变量上获取唯一锁,因此即使获取资源也会导致在锁定互斥锁上等待;因此,这个类很快就会成为瓶颈。
我们能修复它吗?
如何做到这一点...
对于不修改数据的方法,将 boost::unique_locks
替换为 boost::shared_lock
:
#include <boost/thread/shared_mutex.hpp>
class users_online {
typedef boost::shared_mutex mutex_t;
mutable mutex_t users_mutex_;
std::map<std::string, user_info> users_;
public:
bool is_online(const std::string& username) const {
boost::shared_lock<mutex_t> lock(users_mutex_);
return users_.find(username) != users_.end();
}
unsigned short get_age(const std::string& username) const {
boost::shared_lock<mutex_t> lock(users_mutex_);
return users_.at(username).age;
}
void set_online(const std::string& username, const user_info& data) {
boost::lock_guard<mutex_t> lock(users_mutex_);
users_.insert(std::make_pair(username, data));
}
// Other methods
// ...
};
它是如何工作的...
如果那些线程不修改数据,我们可以允许从多个线程同时获取数据。只有当我们打算修改其中的数据时,我们才需要唯一拥有互斥锁;在其他所有情况下,允许同时访问它。这正是 boost::shared_mutex
被设计出来的原因。它允许共享锁定(读取锁定),这允许对资源的多个同时访问。
当我们尝试对共享锁定的资源进行唯一锁定时,操作将被阻塞,直到没有剩余的读取锁,并且只有在那个资源被唯一锁定之后,才允许新的共享锁等待直到唯一锁被释放。
一些读者可能第一次看到可变关键字。此关键字可以应用于非静态和非常量类成员。可变数据成员可以在常量成员函数中修改。
还有更多...
当你只需要唯一锁时,不要使用 boost::shared_mutex
,因为它比普通的 boost::mutex
类稍微慢一些。然而,在其他情况下,它可能会带来很大的性能提升。例如,对于四个读取线程,共享互斥锁将比 boost::mutex
快近四倍。
不幸的是,共享互斥锁不是 C++11 标准的一部分。
参见
-
此外,还有一个
boost::upgrade_mutex
类,在需要将共享锁提升为独占锁的情况下可能很有用。有关更多信息,请参阅Boost.Thread
文档www.boost.org/doc/libs/1_53_0/doc/html/thread.html
。 -
更多关于
mutable
关键字的信息,请参阅herbsutter.com/2013/01/01/video-you-dont-know-const-and-mutable/
。
创建每个线程唯一的变量
让我们快速看一下 创建一个 工作队列类 的配方。那里的每个任务都可以在许多线程中的一个上执行,我们不知道是哪一个。想象一下,我们想要使用某个连接发送已执行任务的成果。
#include <boost/noncopyable.hpp>
class connection: boost::noncopyable {
public:
// Opening a connection is a slow operation
void open();
void send_result(int result);
// Other methods
// ...
};
我们有以下解决方案:
-
当我们需要发送数据时打开一个新的连接(这很慢)
-
为所有线程提供一个单一的连接,并将它们包装在互斥锁中(这也很慢)
-
拥有一个连接池,以线程安全的方式从中获取一个连接并使用它(需要大量的编码,但这个解决方案速度快)
-
每个线程有一个单一的连接(快速且易于实现)
那么,我们如何实现最后的解决方案呢?
准备工作
需要基本了解线程知识。
如何做...
是时候创建一个线程局部变量了:
// In header file
#include <boost/thread/tss.hpp>
connection& get_connection();
// In source file
boost::thread_specific_ptr<connection> connection_ptr;
connection& get_connection() {
connection* p = connection_ptr.get();
if (!p) {
connection_ptr.reset(new connection);
p = connection_ptr.get();
p->open();
}
return *p;
}
使用线程特定的资源从未如此简单:
void task() {
int result;
// Some computations go there
// ...
// Sending result
get_connection().send_result(result);
}
它是如何工作的...
boost::thread_specific_ptr
变量为每个线程持有单独的指针。最初,这个指针等于 NULL
;这就是为什么我们检查 !p
并在它是 NULL
时打开一个连接。
因此,当我们从已经初始化指针的线程进入 get_connection()
时,!p
将返回 false
的值,我们将返回已经打开的连接。当线程退出时,将调用 delete
指针,所以我们不需要担心内存泄漏。
还有更多...
您可以提供一个自己的清理函数,该函数将在线程退出时调用而不是 delete
。清理函数必须具有 void (*cleanup_function)(T*)
签名,并在 boost::thread_specific_ptr
构造期间传递。
C++11 有一个特殊的关键字,thread_local
,用于声明具有线程局部存储持续时间的变量。C++11 没有提供 thread_specific_ptr
类,但您可以使用 thread_local boost::scoped_ptr<T>
或 thread_local std::unique_ptr<T>
在支持 thread_local
的编译器上实现相同的行为。
参见
-
Boost.Thread
文档提供了大量关于不同情况的好例子;它可以在www.boost.org/doc/libs/1_53_0/doc/html/thread.html
找到。 -
阅读这个主题
stackoverflow.com/questions/13106049/c11-gcc-4-8-thread-local-performance-penalty.html
以及关于GCC__thread
关键字的gcc.gnu.org/onlinedocs/gcc-3.3.1/gcc/Thread-Local.html
可能会给你一些关于编译器中thread_local
是如何实现的以及它的速度如何的想法
中断线程
有时候,我们需要终止消耗过多资源或执行时间过长的线程。例如,某些解析器在一个线程中工作(并积极使用Boost.Thread
),但我们已经从它那里获得了所需的数据量,因此解析可以停止。我们只需要:
boost::thread parser_thread(&do_parse);
// Some code goes here
// ...
if (stop_parsing) {
// no more parsing required
// TODO: stop parser
}
我们如何做到这一点?
准备工作
对于这个配方,几乎不需要什么。你只需要至少具备基本线程知识。
如何做到这一点...
我们可以通过中断来停止线程:
if (stop_parsing) {
// no more parsing required
parser_thread.interrupt();
}
它是如何工作的...
Boost.Thread
提供了一些预定义的中断点,在这些中断点中,线程通过interrupt()
调用被检查是否被中断。如果线程被中断,将抛出异常boost::thread_interrupted
。
boost::thread_interrupted
不是从std::exception
派生的!
还有更多...
如我们从第一个配方所知,如果一个传递给线程的函数不会捕获异常,并且异常将离开函数边界,应用程序将终止。boost::thread_interrupted
是这一规则的唯一例外;它可以离开函数边界,并且不会std::terminate()
应用程序;相反,它停止执行线程。
我们也可以在任何地方添加中断点。我们只需要调用boost::this_thread::interruption_point()
:
void do_parse() {
while (not_end_of_parsing) {
boost::this_thread::interruption_point();
// Some parsing goes here
}
}
如果项目不需要中断,定义BOOST_THREAD_DONT_PROVIDE_INTERRUPTIONS
可以提供一些性能提升,并完全禁用线程中断。
C++11 没有线程中断,但你可以使用原子操作部分模拟它们:
-
创建一个原子布尔变量
-
检查线程中的原子变量,如果它已更改则抛出一些异常
-
不要忘记在传递给线程的函数中捕获那个异常(否则你的应用程序将终止)
然而,如果代码在条件变量或睡眠方法中的某个地方等待,这不会对你有所帮助。
参见
-
Boost.Thread
的官方文档提供了预定义的中断点的列表,请参阅www.boost.org/doc/libs/1_53_0/doc/html/thread/thread_management.html#thread.thread_management.tutorial.interruption.html
-
作为练习,查看本章的其他配方,并思考在哪些地方添加额外的中断点可以改进代码
-
阅读其他部分的
Boost.Thread
文档可能很有用;请访问www.boost.org/doc/libs/1_53_0/doc/html/thread.html
操作线程组
那些试图自己重复所有示例的读者,或者那些在实验线程的读者,可能已经对编写以下代码来启动线程感到厌烦了:
boost::thread t1(&some_function);
boost::thread t2(&some_function);
boost::thread t3(&some_function);
// ...
t1.join();
t2.join();
t3.join();
可能还有更好的方法来做这件事?
准备工作
对于这个配方,对线程的基本知识将绰绰有余。
如何操作...
我们可以使用boost::thread_group
类来操作一组线程。
-
构建一个
boost::thread_group
变量:boost::thread_group threads;
-
将线程创建到前面的变量中:
// Launching 10 threads for (unsigned i = 0; i < 10; ++i) { threads.create_thread(&some_function); }
-
现在你可以调用
boost::thread_group
内部的所有线程的函数:// Joining all threads threads.join_all(); // We can also interrupt all of them // by calling threads.interrupt_all();
它是如何工作的...
boost::thread_group
变量仅保存构建或移动到其中的所有线程,并可能向所有线程发送一些调用。
还有更多...
C++11 没有thread_group
类;这是 Boost 特有的。
参见
Boost.Thread
的官方文档可能会让你惊讶于本章未描述的许多其他有用的类;请访问www.boost.org/doc/libs/1_53_0/doc/html/thread.html
第六章 操作任务
在本章中,我们将介绍:
-
注册一个任务以处理任意数据类型
-
创建计时器并将计时器事件作为任务处理
-
网络通信作为任务
-
接受传入的连接
-
并行执行不同的任务
-
传送带任务处理
-
创建一个非阻塞屏障
-
存储异常并从它创建任务
-
将获取和处理系统信号作为任务
简介
本章全部关于任务。我们将调用功能对象为任务(因为它更短,更好地反映了它应该做什么)。本章的主要思想是我们可以将所有处理、计算和交互分解为 functors(任务),并几乎独立地处理每个任务。此外,我们可能不会在某些慢操作(如从套接字接收数据或等待超时)上阻塞,而是提供一个回调任务并继续与其他任务一起工作。一旦操作系统完成慢操作,我们的回调将被执行。
在开始之前
本章至少需要了解第一、第三和第五章。
注册一个任务以处理任意数据类型
首先,让我们关注将持有所有任务并提供执行方法类的结构。我们已经在 创建 work_queue 类 的配方中做了类似的事情,但以下一些问题尚未解决:
-
一个任务可能会抛出一个异常,导致调用
std::terminate
-
一个被中断的线程可能不会注意到中断,但会完成其任务,并在下一个任务期间中断(这不是我们想要的;我们想要中断上一个任务)
-
我们的
work_queue
类只存储和返回任务,但我们需要添加执行现有任务的方法 -
我们需要一种停止处理任务的方法
准备就绪
这个配方需要链接到 libboost_system
库。还需要了解 Boost.Bind
以及对 Boost.Thread
的基本了解。
如何做到...
我们将使用 boost::io_service
而不是上一章中的 work_queue
。这样做有原因,我们将在接下来的配方中看到。
-
让我们从围绕用户任务的包装结构开始:
#include <boost/thread/thread.hpp> namespace detail { template <class T> struct task_wrapped { private: T task_unwrapped_; public: explicit task_wrapped(const T& task_unwrapped) : task_unwrapped_(task_unwrapped) {} void operator()() const { // resetting interruption try { boost::this_thread::interruption_point(); } catch(const boost::thread_interrupted&){} try { // Executing task task_unwrapped_(); } catch (const std::exception& e) { std::cerr<< "Exception: " << e.what() << '\n'; } catch (const boost::thread_interrupted&) { std::cerr<< "Thread interrupted\n"; } catch (...) { std::cerr<< "Unknown exception\n"; } } };
-
为了便于使用,我们将创建一个函数,从用户的函数对象生成
task_wrapped
:template <class T> task_wrapped<T> make_task_wrapped(const T& task_unwrapped) { return task_wrapped<T>(task_unwrapped); } } // namespace detail
-
现在我们已经准备好编写
tasks_processor
类:#include <boost/asio/io_service.hpp> class tasks_processor: private boost::noncopyable { boost::asio::io_service ios_; boost::asio::io_service::work work_; tasks_processor() : ios_() , work_(ios_) {} public: static tasks_processor& get();
-
现在我们将添加
push_task
方法:template <class T> inline void push_task(const T& task_unwrapped) { ios_.post(detail::make_task_wrapped(task_unwrapped)); }
-
让我们通过添加启动和停止任务执行循环的成员函数来完成这个类:
void start() { ios_.run(); } void stop() { ios_.stop(); } }; // tasks_processor
是时候测试我们的类了。为此,我们将创建一个测试函数:
int g_val = 0; void func_test() { ++ g_val; if (g_val == 3) { throw std::logic_error("Just checking"); } boost::this_thread::interruption_point(); if (g_val == 10) { // Emulation of thread interruption. // Will be caught and won't stop execution. throw boost::thread_interrupted(); } if (g_val == 90) { tasks_processor::get().stop(); } }
main
函数可能看起来像这样:int main () { static const std::size_t tasks_count = 100; // stop() is called at 90 BOOST_STATIC_ASSERT(tasks_count > 90); for (std::size_t i =0; i < tasks_count; ++i) { tasks_processor::get().push_task(&func_test); } // We can also use result of boost::bind call // as a task tasks_processor::get().push_task( boost::bind(std::plus<int>(), 2, 2) // counting 2 + 2 ); // Processing was not started. assert(g_val == 0); // Will not throw, but blocks till // one of the tasks it is owning // calls stop(). tasks_processor::get().start(); assert(g_val== 90); }
它是如何工作的...
boost::io_service
变量可以存储和执行发送给它的任务。但我们可能不能直接将用户的任务发送给它,因为它们可能会抛出或接收针对其他任务的干扰。这就是为什么我们用detail::task_wrapped
结构包装用户的任务。它通过调用以下方式重置所有之前的干扰:
try {
boost::this_thread::interruption_point();
} catch(const boost::thread_interrupted&){}
并且在try{}catch()
块中执行任务,确保没有异常会离开operator()
的作用域。
boost::io_service::run()
方法将从队列中获取准备好的任务并逐个执行。这个循环通过调用boost::io_service::stop()
来停止。如果没有更多任务,boost::io_service
类将从run()
函数返回,因此我们使用boost::asio::io_service::work
的一个实例强制它继续执行。
注意
iostream
类以及如std::cerr
和std::cout
这样的变量不是线程安全的。在实际项目中,必须使用额外的同步来获取可读的输出。为了简单起见,我们这里没有这样做。
还有更多...
C++11 STL 库没有io_service
;然而,它(以及Boost.Asio
库的大部分内容)被提议作为技术报告(TR)作为 C++的补充。
参考以下内容
-
以下菜谱将展示我们为什么选择
boost::io_service
而不是我们手写的代码。 -
你可以考虑阅读
Boost.Asio
文档,以获取一些示例、教程和类参考,请访问www.boost.org/doc/libs/1_53_0/doc/html/boost_asio.html
。 -
你也可以阅读《Boost.Asio C++网络编程》这本书,它对
Boost.Asio
提供了一个更平滑的介绍,并涵盖了本书未涉及的一些细节。
将计时器和处理计时器事件作为任务
检查指定间隔内的某些内容是一个常见任务;例如,我们每 5 秒钟需要检查一些会话的活动。对此类问题有两种流行的解决方案:创建一个线程或睡眠 5 秒钟。这是一个非常糟糕的解决方案,它会消耗大量的系统资源,并且扩展性很差。我们可以使用特定于系统的 API 来异步操作计时器,这是一个更好的解决方案,但它需要大量的工作,并且不太便携(直到你为不同的平台编写了许多包装器)。它还让你与那些并不总是很友好的操作系统 API 打交道。
准备工作
你必须知道如何使用Boost.Bind
和Boost.SmartPtr
。参见本章的第一道菜谱,以获取有关boost::asio::io_service
和task_queue
类的信息。将此菜谱与libboost_system
库链接。
这个菜谱有点棘手,所以请做好准备!
如何做到这一点...
此菜谱基于前一道菜谱中的代码。我们只是通过添加新方法来修改tasks_processor
类,以便在指定时间运行任务。
-
让我们在
tasks_processor
类中添加一个方法,以便在某个时间运行一个任务:typedef boost::asio::deadline_timer::time_type time_type; template <class Functor> void run_at(time_type time, const Functor& f) { detail::make_timer_task(ios_, time, f) .push_task(); }
-
我们为
task_queue
类添加了一个方法,用于在所需的时间间隔过后运行一个任务:typedef boost::asio::deadline_timer::duration_type duration_type; template <class Functor> void run_after(duration_type duration, const Functor& f) { detail::make_timer_task(ios_, duration, f) .push_task(); }
-
是时候照顾
detail::make_timer_task
函数了:namespace detail { template <class Time, class Functor> inline timer_task<Functor> make_timer_task( boost::asio::io_service& ios, const Time& duration_or_time, const Functor& task_unwrapped) { return timer_task<Functor>(ios, duration_or_time, task_unwrapped); } }
-
最后一步将是编写一个
timer_task
结构:#include <boost/asio/io_service.hpp> #include <boost/asio/deadline_timer.hpp> #include <boost/system/error_code.hpp> #include <boost/make_shared.hpp> #include <iostream> namespace detail { typedef boost::asio::deadline_timer::duration_type duration_type; template <class Functor> struct timer_task: public task_wrapped<Functor> { private: typedef task_wrapped<Functor> base_t; boost::shared_ptr<boost::asio::deadline_timer> timer_; public: template <class Time> explicit timer_task( boost::asio::io_service& ios, const Time& duration_or_time, const Functor& task_unwrapped) : base_t(task_unwrapped) , timer_(boost::make_shared<boost::asio::deadline_timer>( boost::ref(ios), duration_or_time )) {} void push_task() const { timer_->async_wait(*this); } void operator()(const boost::system::error_code& error) const { if (!error) { base_t::operator()(); } else { std::cerr << error << '\n'; } } }; } // namespace detail
工作原理...
这就是所有工作的原理;用户向 run_after
函数提供超时和一个函数对象。在其中,构建了一个 detail::timer_task
对象,该对象存储了一个用户提供的函数对象并创建了一个指向 boost::asio::deadline_timer
的共享指针。构建的 detail::timer_task
对象被推送到一个函数对象,该函数对象必须在定时器触发时被调用。detail::timer_task::operator()
方法接受 boost::system::error_code
,它将包含等待过程中发生的任何错误的描述。如果没有发生错误,我们调用被包裹的用户函数对象来捕获异常(我们重用第一道菜谱中的 detail::task_wrapped
结构)。以下图表说明了这一点:
注意,我们将 boost::asio::deadline_timer
包裹在 boost::shared_ptr
中,并将整个 timer_task
函数对象(包括 shared_ptr
)传递给 timer_->async_wait(*this)
。这样做是因为 boost::asio::deadline_timer
必须在触发之前不被销毁,将 timer_task
函数对象存储在 io_service
中可以保证这一点。
注意
简而言之,当指定的时间经过后,boost::asio::deadline_timer
将将用户的任务推送到 boost::asio::io_service
队列类以执行。
还有更多...
一些平台没有提供良好的定时器 API,因此 Boost.Asio
库通过为每个 io_service
创建一个额外的执行线程来模拟异步定时器的行为。无论如何,Boost.Asio
是处理定时器的最便携和最有效的库之一。
参见
-
阅读本章的第一道菜谱将教会你
boost::asio::io_service
的基础知识。接下来的菜谱将为你提供更多io_service
用法的示例,并展示如何使用Boost.Asio
处理网络通信、信号和其他功能。 -
你可以考虑查看
Boost.Asio
文档以获取一些示例、教程和类参考,请访问www.boost.org/doc/libs/1_53_0/doc/htm
l/boost_asio.html。
网络通信作为一个任务
通过网络接收或发送数据是一个缓慢的操作。当机器接收数据包,操作系统验证它们并将数据复制到用户指定的缓冲区时,可能需要几秒钟的时间。而我们可能能够做很多工作而不是等待。让我们修改我们的tasks_processor
类,使其能够以异步方式发送和接收数据。用非技术术语来说,我们要求它“从远程主机接收至少 N 个字节,然后完成这个操作后,调用我们的函数。顺便说一下,不要在这个调用上阻塞”。那些了解libev
、libevent
或Node.js
的读者会发现这个配方中有许多熟悉的东西。
准备工作
为了更容易地采用这一材料,需要了解boost::bind
、boost::shared_ptr
和占位符。还需要了解如何将此配方与libboost_system
库链接的信息。
如何操作...
让我们通过添加创建连接的方法来扩展前面配方中的代码。一个连接将由tcp_connection_ptr
类表示,它必须仅使用tasks_processor
来构造(作为一个类比,tasks_processor
是构造此类连接的工厂)。
-
我们需要在
tasks_processor
中添加一个方法来创建端点(我们将称它们为连接)的套接字:tcp_connection_ptr create_connection(const char* addr, unsigned short port_num) { return tcp_connection_ptr( ios_, boost::asio::ip::tcp::endpoint( boost::asio::ip::address_v4::from_string(addr), port_num ) ); }
-
我们需要包含以下许多头文件:
#include <boost/asio/ip/tcp.hpp> #include <boost/asio/placeholders.hpp> #include <boost/asio/write.hpp> #include <boost/asio/read.hpp> #include <boost/shared_ptr.hpp> #include <boost/function.hpp> #include <boost/enable_shared_from_this.hpp>
-
tcp_connection_ptr
类需要管理连接。它拥有套接字并管理其生命周期。它只是boost::shared_ptr<boost::asio::ip::tcp::socket>
的一个薄包装器,它隐藏了Boost.Asio
对用户。class tcp_connection_ptr { boost::shared_ptr<boost::asio::ip::tcp::socket> socket_; public: explicit tcp_connection_ptr( boost::shared_ptr<boost::asio::ip::tcp::socket> socket) : socket_(socket) {} explicit tcp_connection_ptr( boost::asio::io_service& ios, const boost::asio::ip::tcp::endpoint& endpoint) : socket_(boost::make_shared<boost::asio::ip::tcp::socket>( boost::ref(ios) )) { socket_->connect(endpoint); }
-
tcp_connection_ptr
类将需要读取数据的方法:template <class Functor> void async_read( const boost::asio::mutable_buffers_1& buf, const Functor& f, std::size_t at_least_bytes) const { boost::asio::async_read( *socket_, buf, boost::asio::transfer_at_least( at_least_bytes ), f ); }
-
还需要编写数据的方法:
template <class Functor> void async_write( const boost::asio::const_buffers_1& buf, const Functor& f) const { boost::asio::async_write(*socket_, buf, f); } template <class Functor> void async_write( const boost::asio::mutable_buffers_1& buf, const Functor& f) const { boost::asio::async_write(*socket_, buf, f); }
-
我们还将添加一个关闭连接的方法:
void shutdown() const { socket_->shutdown(boost::asio::ip::tcp::socket::shutdown_both); socket_->close(); } };
现在,图书馆用户可以使用前面的类这样发送数据:
const unsigned short g_port_num = 65001; void send_auth_task() { tcp_connection_ptr soc = tasks_processor::get().create_connection("127.0.0.1", g_port_num); boost::shared_ptr<std::string> data = boost::make_shared<std::string>("auth_name"); soc.async_write( boost::asio::buffer(*data), boost::bind( &recieve_auth_task, boost::asio::placeholders::error, soc, data ) ); }
用户也可以这样使用它来接收数据:
void recieve_auth_task( const boost::system::error_code& err, const tcp_connection_ptr& soc, const boost::shared_ptr<std::string>& data) { if (err) { std::cerr << "recieve_auth_task: Client error on recieve: " << err.message() << '\n'; assert(false); } soc.async_read( boost::asio::buffer(&(*data)[0], data->size()), boost::bind( &finsh_socket_auth_task, boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred, soc, data ), 1 ); }
这就是图书馆用户可能处理接收到的数据的方式:
bool g_authed = false; void finsh_socket_auth_task( const boost::system::error_code& err, std::size_t bytes_transfered, const tcp_connection_ptr& soc, const boost::shared_ptr<std::string>& data) { if (err && err != boost::asio::error::eof) { std::cerr << "finsh_socket_auth_task: Client error " << "on recieve: " << err.message() << '\n'; assert(false); } if (bytes_transfered != 2) { std::cerr << "finsh_socket_auth_task: wrong bytes count\n"; assert(false); } data->resize(bytes_transfered); if (*data != "OK") { std::cerr << "finsh_socket_auth_task: wrong response: " << *data << '\n'; assert(false); } g_authed = true; soc.shutdown(); tasks_processor::get().stop(); }
如何工作...
所有有趣的事情都发生在async_*
函数的调用中。就像定时器的情况一样,异步调用会立即返回而不执行函数。它们只告诉boost::asio::io_service
类在某个操作(例如从套接字读取数据)完成后执行回调任务。io_service
将在调用io_service::run()
方法的线程之一中执行我们的函数。
以下图表说明了这一点:
现在,让我们一步一步地检查这个步骤。
tcp_connection_ptr
类持有对boost::asio::ip::tcp::socket
的共享指针,它是围绕本地套接字的Boost.Asio
包装器。我们不希望用户能够直接使用这个包装器,因为它有同步方法,我们正在试图避免使用这些方法。
第一个构造函数接受套接字的指针(并将用于我们的下一个配方)。这个构造函数不会由用户使用,因为 boost::asio::ip::tcp::socket
构造函数需要一个对 boost::asio::io_service
的引用,它隐藏在 tasks_processor
中。
注意
当然,我们库的一些用户可能足够聪明,能够创建一个 boost::asio::io_service
的实例,初始化套接字,并将任务推送到该实例。将 Boost.Asio
库的内容移动到源文件并实现 Pimpl 习语可以帮助你保护用户免受自己伤害,但为了简单起见,我们不会在这里实现它。另一种做事的方式是将 tasks_processor
类声明为 tcp_connection_ptr
的朋友,并使 tcp_connection_ptr
构造函数私有。
第二个构造函数接受一个远程端点和 io_service
的引用。在那里,你可以看到如何使用 socket_->connect(endpoint)
方法将套接字连接到端点。此外,这个构造函数不应该由用户使用;用户应该使用 tasks_processor::create_connection
代替。
在使用 async_write
和 async_read
函数时应该格外小心。套接字和缓冲区必须在异步操作完成之前不能被销毁;这就是为什么在调用 async_*
函数时我们将 shared_ptr
绑定到功能对象:
tcp_connection_ptr soc = tasks_processor::get()
.create_connection("127.0.0.1", g_port_num);
boost::shared_ptr<std::string> data
= boost::make_shared<std::string>("auth_name");
soc.async_write(
boost::asio::buffer(*data),
boost::bind(
&recieve_auth_task,
boost::asio::placeholders::error,
soc,
data
)
);
将共享指针绑定到将在异步操作结束时调用的功能对象,确保至少存在一个 boost::shared_ptr
的连接和数据实例。这意味着连接和数据将不会在功能对象析构函数被调用之前被销毁。
注意
Boost.Asio
可能会复制函数对象,这就是为什么我们使用 boost::shared_ptr<std::string>
类而不是按值传递 std::string
类(这将使 boost::asio::buffer(*data)
无效并导致段错误)。
还有更多...
仔细看看 finsh_socket_auth_task
函数。它检查 err != boost::asio::error::eof
。这样做是因为数据输入的末尾被视为一个错误;然而,这也可能意味着端点主机关闭了套接字,这并不总是坏事(在我们的例子中,我们将其视为非错误行为)。
Boost.Asio
并不是 C++11 的一部分,但它被提议包含在 C++ 中,我们可能会在下一个 TR 中看到它(或者至少它的某些部分)被包含。
参见
-
请参阅
www.boost.org/doc/libs/1_53_0/doc/html/boost_asio.html
的官方文档,了解更多关于Boost.Asio
的示例、教程和完整参考,以及如何使用 UDP 和 ICMP 协议的示例。对于熟悉 BSD 套接字 API 的读者,www.boost.org/doc/libs/1_53_0/doc/html/boost_asio/overview/networking/bsd_sockets.html
页面提供了关于在Boost.Asio
中 BSD 调用看起来像什么的信息。 -
有关
Boost.Bind
的更多信息,请阅读第一章中的记录函数参数和将值绑定为函数参数菜谱,开始编写您的应用程序。第三章管理资源中的跨方法使用类指针的引用计数菜谱将为您提供更多关于boost::shared_ptr
类所做工作的信息。 -
您还可以阅读《Boost.Asio C++网络编程》这本书,Packt Publishing,它更详细地描述了
Boost.Asio
。
接受传入的连接
与网络一起工作的服务器通常看起来像这样一个序列:我们首先获取数据,然后处理它,最后发送结果。想象一下,我们正在创建一种授权服务器,它每秒将处理大量的请求。在这种情况下,我们需要异步接收和发送数据,并在多个线程中处理任务。
在这个菜谱中,我们将看到如何扩展我们的tasks_processor
类以接受和处理传入的连接,在下一个菜谱中,我们将看到如何使其多线程化。
准备工作
这个菜谱需要您对boost::asio::io_service
基础知识有很好的了解,这些知识在本书的第一和第三道菜谱中有描述。一些关于网络通信的知识将有助于您。了解boost::bind, boost::function
, boost::shared_ptr
以及至少前两道菜谱中的信息也是必需的。别忘了将此示例与libboost_system
链接。
如何做到这一点...
就像在前面的菜谱中一样,我们将向我们的tasks_processor
类添加新方法。
-
首先,我们需要添加一个在指定端口上开始监听的函数:
template <class Functor> void add_listener(unsigned short port_num, const Functor& f) { listeners_map_t::const_iterator it = listeners_.find(port_num); if (it != listeners_.end()) { throw std::logic_error( "Such listener for port '" + boost::lexical_cast<std::string>(port_num) + "' already created" ); } listeners_[port_num] = boost::make_shared<detail::tcp_listener>( boost::ref(ios_), port_num, f ); listeners_[port_num]->push_task(); // Start accepting }
-
我们还将添加一个
std::map
变量来保存所有监听器:typedef std::map< unsigned short, boost::shared_ptr<detail::tcp_listener> > listeners_map_t; listeners_map_t listeners_;
-
以及一个用于停止监听器的函数:
void remove_listener(unsigned short port_num) { listeners_map_t::iterator it = listeners_.find(port_num); if (it == listeners_.end()) { throw std::logic_error( "No listener for port '" + boost::lexical_cast<std::string>(port_num) + "' created" ); } (*it).second->stop(); listeners_.erase(it); }
-
现在我们需要关注
detail::tcp_listener
类本身。它必须有一个接受者:namespace detail { class tcp_listener : public boost::enable_shared_from_this<tcp_listener> { typedef boost::asio::ip::tcp::acceptor acceptor_t; acceptor_t acceptor_;
-
以及一个在成功接受时将被调用的函数:
boost::function<void(tcp_connection_ptr)> func_; public: template <class Functor> tcp_listener( boost::asio::io_service& io_service, unsigned short port, const Functor& task_unwrapped) : acceptor_(io_service,boost::asio::ip::tcp::endpoint( boost::asio::ip::tcp::v4(), port )) , func_(task_unwrapped) {}
-
这是一个用于启动接受的函数的样子:
void push_task() { if (!acceptor_.is_open()) { return; } typedef boost::asio::ip::tcp::socket socket_t; boost::shared_ptr<socket_t> socket = boost::make_shared<socket_t>( boost::ref(acceptor_.get_io_service()) ); acceptor_.async_accept(*socket, boost::bind( &tcp_listener::handle_accept, this->shared_from_this(), tcp_connection_ptr(socket), boost::asio::placeholders::error )); }
-
停止接受的函数编写如下:
void stop() { acceptor_.close(); }
-
这是我们将在成功接受时调用的包装函数:
private: void handle_accept( const tcp_connection_ptr& new_connection, const boost::system::error_code& error) { push_task(); if (!error) { make_task_wrapped(boost::bind(func_, new_connection)) (); // Run the task } else { std::cerr << error << '\n'; } } }; // class tcp_listener } // namespace detail
它是如何工作的...
add_listener
函数只是检查我们是否已经在指定的端口上没有监听器,构造一个新的detail::tcp_listener
,并将其添加到listeners_
列表中。
当我们构造boost::asio::ip::tcp::acceptor
并指定端点(见步骤 5)时,它将在指定的地址上打开一个套接字。
对boost::asio::ip::tcp::acceptor
的async_accept(socket, handler)
调用,当接受传入连接时将调用我们的处理程序。当有新的连接进入时,acceptor_
将此连接绑定到一个套接字,并将准备好的任务推送到task_queue
(在boost::asio::io_service
中)以执行处理程序。正如我们从先前的配方中理解的那样,所有的async_*
调用都会立即返回,async_accept
不是一个特殊情况,因此它不会直接调用处理程序。让我们更仔细地看看我们的处理程序:
boost::bind(
&tcp_listener::handle_accept,
this->shared_from_this(),
tcp_connection_ptr(socket),
boost::asio::placeholders::error
)
当发生接受操作时,我们需要当前类的实例保持活跃,因此我们为boost::bind
提供了一个boost::shared_ptr
变量作为第二个参数(我们通过this->shared_from_this()
调用来实现)。我们还需要保持套接字活跃,因此将其作为第三个参数提供。最后一个参数是一个占位符(例如_1
和_2
对于boost::bind
),它说明了async_accept
函数应该将error
变量放在你的方法中的位置。
现在让我们更仔细地看看我们的handle_accept
方法。调用push_task()
方法是必需的,以便重新启动acceptor_
的接受。之后,我们将检查错误,如果没有错误,我们将用户提供的处理程序绑定到tcp_connection_ptr
,从它创建一个task_wrapped
实例(这对于正确处理异常和中断点是有必要的),并执行它。
现在让我们看看remove_listener()
方法。在调用时,它将在列表中找到一个监听器并对其调用stop()
。在stop()
内部,我们将对接受器调用close()
,然后返回到remove_listener
方法,并从监听器映射中删除指向tcp_listener
的共享指针。之后,指向tcp_listener
的共享指针仅剩在一个接受任务中。
当我们为接受器调用stop()
时,它所有的异步操作都将被取消,并且将调用处理程序。如果我们查看上一步中的handle_accept
方法,我们会看到在发生错误(或停止的接受器)的情况下,不会添加更多的接受任务。
在所有处理程序被调用之后,没有接受器的共享指针将保留,并且将调用tcp_connection
的析构函数。
还有更多...
我们没有使用boost::asio::ip::tcp::acceptor
类的所有功能。如果我们提供一个特定的boost::asio::ip::tcp::endpoint
,它可以绑定到特定的 IPv6 或 IPv4 地址。你也可以通过native_handle()
方法获取本地套接字,并使用一些特定于操作系统的调用来调整行为。你可以通过调用set_option
为acceptor_
设置一些选项。例如,这是如何强制接受器重用地址的:
boost::asio::socket_base::reuse_address option(true);
acceptor_.set_option(option);
注意
重新使用地址提供了一种在服务器正确关闭后快速重新启动服务器的功能。服务器终止后,套接字可能打开一段时间,如果没有 reuse_address
选项,你将无法在相同的地址上启动服务器。
参见
-
从本章的开始部分开始,这是一个获取更多关于
Boost.Asio
信息的不错主意。 -
参见
Boost.Asio
的官方文档,了解更多示例、教程和完整的参考信息,请访问www.boost.org/doc/libs/1_53_0/doc/html/boost_asio.html
。 -
读取 第一章 中关于 重新排序函数参数 和 将值绑定为函数参数 的食谱,以获取更多关于
Boost.Bind
的信息。 -
第三章 中关于 跨方法使用类指针的引用计数 的食谱将为你提供更多关于
boost::shared_ptr
做了什么的信息。
并行执行不同的任务
现在是时候让我们的 tasks_queue
在多个线程中处理任务了。这能有多难?
准备工作
你需要阅读本章的第一篇食谱。还需要了解一些多线程知识,特别是阅读 第五章 中关于 操纵一组线程 的食谱。
如何操作...
我们需要做的只是将 start_multiple
方法添加到我们的 tasks_queue
类中:
#include <boost/thread/thread.hpp>
// Default value will attempt to guess optimal count of threads
void start_multiple(std::size_t threads_count = 0) {
if (!threads_count) {
threads_count = (std::max)(static_cast<int>(
boost::thread::hardware_concurrency()), 1
);
}
// one thread is the current thread
-- threads_count;
boost::thread_group tg;
for (std::size_t i = 0; i < threads_count; ++i) {
tg.create_thread(boost::bind(
&boost::asio::io_service::run, boost::ref(ios_)
));
}
ios_.run();
tg.join_all();
}
现在我们可以做更多的工作,如下面的图示所示:
它是如何工作的...
boost::asio::io_service::run
方法是线程安全的。几乎所有的 Boost.Asio
方法都是线程安全的,所以我们只需要从不同的线程中运行 boost::asio::io_service::run
方法。
注意
如果你正在执行修改公共资源的任务,你将需要在那个资源周围添加互斥锁。
看到对 boost::thread::hardware_concurrency()
的调用吗?它返回可以同时运行的线程数。但它只是一个提示,有时可能会返回一个 0
值,这就是为什么我们调用 std::max
函数的原因。这确保了 threads_count
至少存储了 1
的值。
注意
我们将 std::max
放在括号中,因为一些流行的编译器定义了 min()
和 max()
宏,所以我们需要额外的技巧来解决这个问题。
还有更多...
boost::thread::hardware_concurrency()
函数是 C++11 的一部分;你将在 std::
命名空间中的 <thread>
头文件中找到它。然而,并不是所有的 boost::asio
类都是 C++11 的一部分(但它们被提议包含在内,所以我们可能会在下一个技术报告(TR)中看到它们)。
参见
-
请参阅
www.boost.org/doc/libs/1_53_0/doc/html/boost_asio.html
上的Boost.Asio
文档,获取更多示例和有关不同类的信息。 -
请参阅
www.boost.org/doc/libs/1_53_0/doc/html/thread.html
上的Boost.Thread
文档,了解有关boost::thread_group
和boost::threads
的信息。 -
第五章中的食谱(特别是最后一个名为“操作线程组”的食谱)将为您提供有关
Boost.Thread
使用的更多信息。 -
“将值绑定为函数参数”的食谱将帮助您更好地理解
boost::bind
函数。
传送带任务处理
有时需要在一个指定的时间间隔内处理任务。与之前尝试按任务在队列中出现的顺序处理任务的食谱相比,这是一个很大的不同。
考虑一个例子,我们正在编写一个程序,该程序连接两个子系统,其中一个生成数据包,另一个将修改后的数据写入磁盘(这种类型的东西可以在视频摄像头、录音机和其它设备中看到)。我们需要逐个处理数据包,以最小的抖动平滑处理,并在多个线程中处理。
我们之前的tasks_queue
在按指定顺序处理任务方面做得不好:
// global variables
tasks_queue queue;
subsystem1 subs1;
subsystem2 subs2;
tasks_queue& operator<< (tasks_queue&, data_packet& data) {
decoded_data d_decoded = decode_data(data);
compressed_data c_data = compress_data(d_decoded);
subs2.send_data(c_data);
}
void start_data_accepting() {
while (!subs1.is_stopped()) {
queue << subs1.get_data();
}
}
#include <boost/thread/thread.hpp>
int main() {
// Getting data packets from first device
// and putting them to queue
boost::thread t(&start_data_accepting);
// Which data packet will be processed first in
// multi-threaded environment?
// packet #2 may be processed before packet #1,
// no guarantee that packets will be processed in
// order of their appearance
queue.run_multiple();
t.join();
}
那我们该如何解决这个问题呢?
准备工作
对于这个食谱,需要了解boost::asio::io_service
的基本知识;至少阅读本章的第一个食谱。为了理解这个例子,需要了解第五章中“创建一个工作队列类”的食谱。代码必须链接到boost_thread
库。
如何做到这一点...
这个食谱基于第五章中“创建一个工作队列类”食谱的work_queue
类的代码。我们将进行一些修改,并使用该类的一些实例。
-
让我们先为数据解码、数据压缩和数据发送创建单独的队列:
workqueue decoding_queue, compressing_queue, sending_queue;
-
现在是重构操作符
<<
并将其拆分为多个函数的时候了:#include <boost/bind.hpp> void do_decode(const data_packet& packet); void start_data_accepting() { while (!subs1.is_stopped()) { decoding_queue.push_task(boost::bind( &do_decode, subs1.get_data() )); } } void do_compress(const decoded_data& packet); void do_decode(const data_packet& packet) { compressing_queue.push_task(boost::bind( &do_compress, decode_data(packet) )); } void do_compress(const decoded_data& packet) { sending_queue.push_task(boost::bind( &subsystem2::send_data, boost::ref(subs2), compress_data(packet) )); }
-
我们在第五章第五章。多线程中的
work_queue
类没有stop()
函数。让我们添加它:// class work_queue from chapter 5 #include <deque> #include <boost/function.hpp> #include <boost/thread/mutex.hpp> #include <boost/thread/locks.hpp> #include <boost/thread/condition_variable.hpp> class work_queue { public: typedef boost::function<void()> task_type; private: std::deque<task_type> tasks_; boost::mutex mutex_; boost::condition_variable cond_; bool is_stopped_; public: work_queue() : is_stopped_(false) {} void stop() { boost::unique_lock<boost::mutex> lock(mutex_); is_stopped_ = true; lock.unlock(); cond_.notify_all(); } void push_task(const task_type& task) { boost::unique_lock<boost::mutex> lock(mutex_); if (is_stopped_) { return; } tasks_.push_back(task); lock.unlock(); cond_.notify_one(); } task_type pop_task() { boost::unique_lock<boost::mutex> lock(mutex_); while (tasks_.empty()) { if (is_stopped_) { return task_type(); } cond_.wait(lock); } task_type ret = tasks_.front(); tasks_.pop_front(); return ret; } };
现在可以停止
work_queue
类。如果work_queue
被停止且tasks_
变量中没有更多的任务,pop_task()
方法将返回空的任务。 -
在完成第 3 步中显示的所有操作后,我们可以编写如下代码:
void run_while_not_stopped(work_queue& queue) { work_queue::task_type task; while (task = queue.pop_task()) { task(); } }
-
那就是全部了!现在我们只需要启动传送带:
#include <boost/thread/thread.hpp> int main() { // Getting data packets from first device and putting them // to queue boost::thread t_data_accepting(&start_data_accepting); boost::thread t_data_decoding(boost::bind( &run_while_not_stopped, boost::ref(decoding_queue) )); boost::thread t_data_compressing(boost::bind( &run_while_not_stopped, boost::ref(compressing_queue) )); boost::thread t_data_sending(boost::bind( &run_while_not_stopped, boost::ref(sending_queue) ));
-
可以这样停止传送带:
t_data_accepting.join(); decoding_queue.stop(); t_data_decoding.join(); compressing_queue.stop(); t_data_compressing.join(); sending_queue.stop(); t_data_sending.join();
它是如何工作的...
诀窍是将单个数据包的处理分解成一些同样小的子任务,并在不同的work_queues
中逐个处理它们。在本例中,我们可以将数据处理分解为数据解码、数据压缩和数据发送。
理想情况下,处理六个数据包的过程将看起来像这样:
时间 | 接收 | 解码 | 压缩 | 发送 |
---|---|---|---|---|
Tick 1: |
数据包 #1 |
|||
Tick 2: |
数据包 #2 |
数据包 #1 |
||
Tick 3: |
数据包 #3 |
数据包 #2 |
数据包 #1 |
|
Tick 4: |
数据包 #4 |
数据包 #3 |
数据包 #2 |
数据包 #1 |
Tick 5: |
数据包 #5 |
数据包 #4 |
数据包 #3 |
数据包 #2 |
Tick 6: |
数据包 #6 |
数据包 #5 |
数据包 #4 |
数据包 #3 |
Tick 7: |
数据包 #6 |
数据包 #5 |
数据包 #4 |
|
Tick 8: |
数据包 #6 |
数据包 #5 |
||
Tick 9: |
数据包 #6 |
然而,我们的世界并不完美,所以一些任务可能比其他任务完成得更快。例如,接收可能比解码快,在这种情况下,解码队列将保留一组待完成的任务。我们没有在我们的示例中使用io_service
,因为它不能保证按任务提交的顺序执行已提交的任务。
还有更多...
在本例中创建传送带所使用的所有工具都可在 C++11 中找到,因此没有任何东西会阻止你在 C++11 兼容的编译器上创建相同的东西而不使用 Boost。然而,Boost 会使你的代码更易于移植,并且可以在 C++03 编译器上使用。
参见
-
这种技术广为人知,并被处理器开发者所使用。参见指令流水线。在这里,你可以找到关于传送带所有特性的简要描述。
-
从第五章的创建工作队列 类配方和第一章的将值绑定为函数参数配方中,开始编写您的应用程序,将为您提供有关在此配方中使用的方法的更多信息。
制作非阻塞屏障
在多线程编程中,有一个称为屏障的抽象。它阻止到达它的执行线程,直到请求的线程数不是阻塞在它上面。之后,所有线程都会被释放,并继续执行。考虑以下示例,看看它可以用在哪里。
我们希望在不同线程中处理数据的不同部分,然后发送数据:
#include <cstddef>
static const std::size_t data_length = 10000;
#include <boost/array.hpp>
struct vector_type : public boost::array<std::size_t, data_length> {
void* alignment;
};
typedef boost::array<vector_type, 4> data_t;
void fill_data(vector_type& data);
void compute_send_data(data_t& data);
#include <boost/thread/barrier.hpp>
void runner(std::size_t thread_index, boost::barrier& data_barrier, data_t& data) {
for (std::size_t i = 0; i < 1000; ++ i) {
fill_data(data.at(thread_index));
data_barrier.wait();
if (!thread_index) {
compute_send_data(data);
}
data_barrier.wait();
}
}
#include <boost/thread/thread.hpp>
int main() {
// Initing barriers
boost::barrier data_barrier(data_t::static_size);
// Initing data
data_t data;
// Run on 4 threads
boost::thread_group tg;
for (std::size_t i = 0; i < data_t::static_size; ++i) {
tg.create_thread(boost::bind(
&runner,
i,
boost::ref(data_barrier),
boost::ref(data)
));
}
tg.join_all();
}
data_barrier.wait()
方法会阻塞,直到所有线程填充数据。之后,所有线程都会被释放;索引为0
的线程将使用compute_send_data(data)
计算要发送的数据,而其他线程则再次在屏障处等待,如下面的图所示:
看起来很笨拙,不是吗?
准备中
这个配方需要了解本章的第一个配方。还需要了解Boost.Bind
和Boost.Thread
。本配方的代码需要链接到boost_thread
和boost_system
库。
如何做...
我们根本不需要阻塞!让我们更仔细地看看这个例子。我们只需要发布四个fill_data
任务,并让最后一个完成的任务调用compute_send_data(data)
。
-
我们需要从第一个配方中获取
tasks_processor
类;不需要对其进行任何更改。 -
而不是使用屏障,我们将使用原子变量:
#include <boost/atomic.hpp> typedef boost::atomic<unsigned int> atomic_count_t;
-
我们的新运行函数将看起来像这样:
void clever_runner( std::size_t thread_index, std::size_t iteration, atomic_count_t& counter, data_t& data) { fill_data(data.at(thread_index)); if (++counter == data_t::static_size) { compute_send_data(data); ++ iteration; if (iteration == 1000) { // exiting, because 1000 iterations are done tasks_processor::get().stop(); return; } counter = 0; for (std::size_t i = 0; i < data_t::static_size; ++ i) { tasks_processor::get().push_task(boost::bind( clever_runner, i, iteration, boost::ref(counter), boost::ref(data) )); } } }
-
只有主函数会略有变化,如下所示:
// Initing counter atomic_count_t counter(0); // Initing data data_t data; // Run on 4 threads tasks_processor& tp = tasks_processor::get(); for (std::size_t i = 0; i < data_t::static_size; ++i) { tp.push_task(boost::bind( &clever_runner, i, 0, // first run boost::ref(counter), boost::ref(data) )); } tp.start();
它是如何工作的...
我们不会阻塞,因为没有线程会等待资源。而不是阻塞,我们通过counter atomic
变量来计算完成填充数据的任务数。最后一个剩余的任务将有一个counter
变量等于data_t::static_size
。它只需要计算并发送数据。
之后,我们检查退出条件(已完成 1000 次迭代),并通过向队列中填充任务来发布新数据。
还有更多...
这个解决方案更好吗?首先,它的可扩展性更好:
这种方法对于程序执行大量不同工作的情况也可能更有效。因为没有线程在屏障中等待,空闲线程可以在其中一个线程计算和发送数据的同时执行其他工作。
用于此示例的所有工具都可在 C++11 中找到(您只需将tasks_processor
中的io_service
替换为第五章中的work_queue
即可)。
参见
-
Boost.Asio
的官方文档可能为您提供有关io_service
使用的更多信息,请参阅www.boost.org/doc/libs/1_53_0/doc/html/boost_asio.html
-
参见第三章中所有与
Boost.Function
相关的配方,管理资源,以及官方文档www.boost.org/doc/libs/1_53_0/doc/html/function.html
,以了解任务是如何工作的 -
参见第一章中与
Boost.Bind
相关的配方,开始编写您的应用程序,以获取有关boost::bind
函数更多信息,或者查看官方文档www.boost.org/doc/libs/1_53_0/libs/bind/bind.html
存储异常并从它创建任务
处理异常并不总是简单的,可能需要花费很多时间。考虑这种情况,异常必须被序列化并通过网络发送。这可能需要毫秒级和几千行代码。在捕获异常后并不总是处理它的最佳时间和地点。
那么,我们能否存储异常并延迟它们的处理?
准备工作
这个配方需要了解 boost::asio::io_service
,这在本章的第一个配方中已经描述过。还需要了解 Boost.Bind
。
如何实现...
我们所需要的只是能够像普通变量一样存储异常并在线程之间传递它们。
-
让我们从处理异常的函数开始。在我们的例子中,它只会将异常信息输出到控制台:
#include <boost/exception_ptr.hpp> #include <boost/lexical_cast.hpp> void func_test2(); // Forward declaration void process_exception(const boost::exception_ptr& exc) { try { boost::rethrow_exception(exc); } catch (const boost::bad_lexical_cast& /*e*/) { std::cout << "Lexical cast exception detected\n" << std::endl; // Pushing another task to execute tasks_processor::get().push_task(&func_test2); } catch (...) { std::cout << "Can not handle such exceptions:\n" << boost::current_exception_diagnostic_information() << std::endl; // Stopping tasks_processor::get().stop(); } }
-
现在,我们将编写一些函数来演示异常是如何工作的:
void func_test1() { try { boost::lexical_cast<int>("oops!"); } catch (...) { tasks_processor::get().push_task(boost::bind( &process_exception, boost::current_exception() )); } } #include <stdexcept> void func_test2() { try { // Some code goes here BOOST_THROW_EXCEPTION(std::logic_error( "Some fatal logic error" )); // Some code goes here } catch (...) { tasks_processor::get().push_task(boost::bind( &process_exception, boost::current_exception() )); } }
-
现在,如果我们像这样运行示例:
tasks_processor::get().push_task(&func_test1); tasks_processor::get().start();
我们将得到以下输出:
Lexical cast exception detected Can not handle such exceptions: ../../../BoostBook/Chapter6/exception_ptr/main.cpp(109): Throw in function void func_test2() Dynamic exception type: boost::exception_detail::clone_impl<boost::exception_detail::error_info_injector<std::logic_error> > std::exception::what: Some fatal logic error
它是如何工作的...
Boost.Exception
库提供了一种存储和重新抛出异常的能力。必须从 catch()
块内部调用 boost::current_exception()
方法,它返回一个 boost::exception_ptr
类型的对象。因此,在 func_test1()
中,将抛出 boost::bad_lexical_cast
异常,它将由 boost::current_exception()
返回,并从该异常和 process_exception
函数的指针创建一个任务(一个函数对象)。
process_exception
函数将重新抛出异常(从 boost::exception_ptr
恢复异常类型的方法是通过使用 boost::rethrow_exception(exc)
重新抛出它,然后通过指定异常类型来捕获它)。
在 func_test2
中,我们使用 BOOST_THROW_EXCEPTION
宏抛出一个 std::logic_error
异常。这个宏做了很多有用的工作:它检查我们的异常是否从 std::exception
派生,并添加有关异常来源的文件名、函数名和抛出异常的代码行号的信息。因此,当异常被重新抛出并由 catch(...)
捕获时,通过 boost::current_exception_diagnostic_information()
,我们将能够输出更多关于它的信息。
还有更多...
通常,exception_ptr
用于在线程之间传递异常。例如:
void run_throw(boost::exception_ptr& ptr) {
try {
// A lot of code goes here
} catch (...) {
ptr = boost::current_exception();
}
}
int main () {
boost::exception_ptr ptr;
// Do some work in parallel
boost::thread t(boost::bind(
&run_throw,
boost::ref(ptr)
));
// Some code goes here
// …
t.join();
// Checking for exception
if (ptr) {
// Exception occured in thread
boost::rethrow_exception(ptr);
}
}
boost::exception_ptr
类可能会通过堆多次分配内存,使用原子操作,并通过重新抛出和捕获异常来实现一些操作。尽量在没有实际需要的情况下不要使用它。
C++11 已经采用了 boost::current_exception
、boost::rethrow_exception
和 boost::exception_ptr
。你将在 std::
命名空间中的 <exception>
头文件中找到它们。然而,BOOST_THROW_EXCEPTION
和 boost::current_exception_diagnostic_information()
方法不在 C++11 中,所以你需要自己实现它们(或者只需使用 Boost 版本)。
参见
-
Boost.Exception
的官方文档包含了关于实现和限制的许多有用信息,请参阅www.boost.org/doc/libs/1_53_0/libs/exception/doc/boost-exception.html
。您也可能找到一些本食谱中没有涵盖的信息(例如,如何向已抛出的异常添加附加信息)。 -
本章的第一个食谱将为您提供有关
tasks_processor
类的信息。来自第一章的将值绑定为函数参数食谱,来自第二章的转换字符串为数字食谱将帮助您使用Boost.Bind
和Boost.LexicalCast
。
将获取和处理系统信号作为任务
在编写某些服务器应用程序(特别是针对 Linux 操作系统)时,需要捕获和处理信号。通常,所有信号处理程序都在服务器启动时设置,并且在应用程序执行期间不会改变。
本食谱的目标是使我们的tasks_processor
类能够处理信号。
准备工作
我们将需要本章第一个食谱中的代码。对Boost.Bind
和Boost.Function
有良好的了解也是必需的。
如何操作...
本食谱与之前的食谱类似;我们有一些信号处理程序、注册它们的函数和一些支持代码。
-
让我们从包含以下头文件开始:
#include <boost/asio/signal_set.hpp> #include <boost/function.hpp>
-
现在我们向
tasks_processor
类添加一个用于信号处理的成员:private: boost::asio::signal_set signals_; boost::function<void(int)> users_signal_handler_;
-
在信号捕获时将被调用的函数如下:
// private void handle_signals( const boost::system::error_code& error, int signal_number) { if (error) { std::cerr << "Error in signal handling: " << error << '\n'; } else { // If signals occurs while there is no // waiting handlers, signal notification // is queued, so it won't be missed // while we are running // the users_signal_handler_ detail::make_task_wrapped(boost::bind( boost::ref(users_signal_handler_), signal_number ))(); // make and run task_wrapped } signals_.async_wait(boost::bind( &tasks_processor::handle_signals, this, _1, _2 )); }
-
不要忘记在
tasks_processor
构造函数中初始化signals_
成员:tasks_processor() : ios_() , work_(ios_) , signals_(ios_) {}
-
现在我们需要一个用于注册信号处理程序的函数:
// This function is not threads safe! // Must be called before all the 'start()' calls // Function can be called only once template <class Func> void register_signals_handler( const Func& f, const std::vector<int>& signals_to_wait) { // Making sure that this is the first call assert(!users_signal_handler_); users_signal_handler_ = f; std::for_each( signals_to_wait.begin(), signals_to_wait.end(), boost::bind( &boost::asio::signal_set::add, &signals_, _1 ) ); signals_.async_wait(boost::bind( &tasks_processor::handle_signals, this, _1, _2 )); }
就这些。现在我们已准备好处理信号。以下是一个测试程序:
void accept_3_signals_and_stop(int signal) { static int signals_count = 0; assert(signal == SIGINT); ++ signals_count; std::cout << "Captured " << signals_count << " SIGINT\n"; if (signals_count == 3) { tasks_processor::get().stop(); } } int main () { tasks_processor::get().register_signals_handler( &accept_3_signals_and_stop, std::vector<int>(1, SIGINT) // vector containing 1 element ); tasks_processor::get().start(); }
这将产生以下输出:
Captured 1 SIGINT Captured 2 SIGINT Captured 3 SIGINT Press any key to continue . . .
它是如何工作的...
这里没有什么是困难的(与本章的一些先前食谱相比)。register_signals_handler
函数添加将被处理的信号编号。这是通过调用boost::asio::signal_set::add
函数对signals_to_wait
向量的每个元素进行操作来完成的(我们使用std::for_each
和boost::bind
的一些魔法来完成)。
接下来,指令使signals_ 成员
等待信号,并在信号捕获时调用tasks_processor::handle_signals
成员函数。tasks_processor::handle_signals
函数检查错误,如果没有错误,它通过引用users_signal_handler_
和信号编号创建一个功能对象。这个功能对象将被包裹在task_wrapped
结构中(该结构处理所有异常)并执行。
之后,我们再次使signals_ 成员
等待信号。
还有更多...
当需要线程安全的动态添加和删除信号时,我们可以修改这个示例,使其看起来像本章的制作定时器和处理定时事件作为任务配方中的detail::timer_task
。当多个boost::asio::signal_set
对象注册为等待同一信号时,每个signal_set
的处理程序将在单个信号上被调用。
C++长期以来一直能够使用<csignal>
头文件中的signal
函数处理信号。然而,它无法使用功能性对象(这是一个巨大的缺点)。
参见
-
来自第一章的将值绑定为函数参数和重新排序函数参数配方,提供了关于
boost::bind
的大量信息。官方文档也可能有所帮助:www.boost.org/doc/libs/1_53_0/libs/bind/bind.html
-
来自第三章的将任何功能性对象存储在变量中配方(关于
Boost.Function
),提供了关于boost::function
的信息。 -
查看官方
Boost.Asio
文档以获取更多关于boost::asio::signal_set
和其他该伟大库特性的信息和示例。
第七章. 字符串操作
在本章中,我们将涵盖:
-
改变大小写和不区分大小写的比较
-
使用正则表达式匹配字符串
-
使用正则表达式搜索和替换字符串
-
使用安全的 printf-like 函数格式化字符串
-
替换和删除字符串
-
使用两个迭代器表示字符串
-
使用字符串类型的引用
简介
整章都致力于字符串更改、搜索和表示的不同方面。我们将看到如何使用 Boost 库轻松完成一些常见的字符串相关任务。本章内容足够简单;它解决了非常常见的字符串操作任务。那么,让我们开始吧!
改变大小写和不区分大小写的比较
这是一个相当常见的任务。我们有两个非 Unicode 或 ANSI 字符字符串:
#include <string>
std::string str1 = "Thanks for reading me!";
std::string str2 = "Thanks for reading ME!";
我们需要以不区分大小写的方式比较它们。有很多方法可以做到这一点;让我们看看 Boost 的方法。
准备工作
在这里我们只需要std::string
的基本知识。
如何做到这一点...
这里有一些不同的方法来进行不区分大小写的比较:
-
最简单的一个是:
#include <boost/algorithm/string/predicate.hpp> boost::iequals(str1, str2)
-
使用 Boost 谓词和 STL 方法:
#include <boost/algorithm/string/compare.hpp> #include <algorithm> str1.size() == str2.size() && std::equal( str1.begin(), str1.end(), str2.begin(), boost::is_iequal() )
-
创建两个字符串的小写副本:
#include <boost/algorithm/string/case_conv.hpp> std::string str1_low = boost::to_lower_copy(str1); std::string str2_low = boost::to_lower_copy(str2); assert(str1_low == str2_low);
-
创建原始字符串的大写副本:
#include <boost/algorithm/string/case_conv.hpp> std::string str1_up = boost::to_upper_copy(str1); std::string str2_up = boost::to_upper_copy(str2); assert(str1_up == str2_up);
-
将原始字符串转换为小写:
#include <boost/algorithm/string/case_conv.hpp> boost::to_lower(str1); boost::to_lower(str2); assert(str1 == str2);
它是如何工作的...
第二种方法并不明显。在第二种方法中,我们比较字符串的长度;如果它们的长度相同,我们使用boost::is_iequal
谓词逐字符比较字符串。boost::is_iequal
谓词以不区分大小写的方式比较两个字符。
注意
Boost.StringAlgorithm
库在方法或类的名称中使用i
,如果这个方法是不区分大小写的。例如,boost::is_iequal
、boost::iequals
、boost::is_iless
以及其他。
还有更多...
Boost.StringAlgorithm
库中所有与大小写相关的函数和功能对象都接受std::locale
。默认情况下(以及在我们的示例中),方法和类使用默认构造的std::locale
。如果我们大量处理字符串,那么构造一个std::locale
变量一次并传递给所有方法可能是一个很好的优化。另一个好的优化是使用'C'区域设置(如果您的应用程序逻辑允许的话)通过std::locale::classic()
:
// On some platforms std::locale::classic() works
// faster than std::locale()
boost::iequals(str1, str2, std::locale::classic());
注意
没有什么禁止你使用这两种优化。
很不幸,C++11 没有Boost.StringAlgorithm
的字符串函数。所有算法都是快速且可靠的,所以不要害怕在你的代码中使用它们。
参见
-
关于 Boost 字符串算法库的官方文档可以在
www.boost.org/doc/libs/1_53_0/doc/html/string_algo.html
找到 -
请参阅 Andrei Alexandrescu 和 Herb Sutter 所著的《C++编码标准》一书,了解如何用几行代码创建一个不区分大小写的字符串的示例。
使用正则表达式匹配字符串
让我们做一些有用的事情!通常,用户的输入必须使用某些正则表达式特定的模式进行检查,这提供了一种灵活的匹配方式。问题是正则表达式语法有很多;使用一种语法编写的表达式不能很好地由另一种语法处理。另一个问题是长正则表达式不容易编写。
因此,在这个菜谱中,我们将编写一个程序,该程序可能使用不同类型的正则表达式语法,并检查输入字符串是否与指定的正则表达式匹配。
准备中
这个菜谱需要基本的 STL 知识。了解正则表达式语法可能会有帮助,但并非必需。
需要将示例链接到 libboost_regex
库。
如何做到这一点...
这个正则表达式匹配器由 main()
函数中的几行代码组成;然而,我经常使用它。它总有一天会帮到你的。
-
为了实现它,我们需要以下头文件:
#include <boost/regex.hpp> #include <iostream>
-
在程序开始时,我们需要输出可用的正则表达式语法:
int main() { std::cout << "Available regex syntaxes:\n" << "\t[0] Perl\n" << "\t[1] Perl case insensitive\n" << "\t[2] POSIX extended\n" << "\t[3] POSIX extended case insensitive\n" << "\t[4] POSIX basic\n" << "\t[5] POSIX basic case insensitive\n" << "Choose regex syntax: ";
-
现在根据选择的语法正确设置标志:
boost::regex::flag_type flag; switch (std::cin.get()) { case '0': flag = boost::regex::perl; break; case '1': flag = boost::regex::perl|boost::regex::icase; break; case '2': flag = boost::regex::extended; break; case '3': flag = boost::regex::extended|boost::regex::icase; break; case '4': flag = boost::regex::basic; break; case '5': flag = boost::regex::basic|boost::regex::icase; break; default: std::cout << "Inccorect number of regex syntax." <<"Exiting... \n"; return -1; } // Disabling exceptions flag |= boost::regex::no_except;
-
现在,我们将循环请求正则表达式模式:
// Restoring std::cin std::cin.ignore(); std::cin.clear(); std::string regex, str; do { std::cout << "Input regex: "; if (!std::getline(std::cin, regex) || regex.empty()) { return 0; } // Without `boost::regex::no_except`flag this // constructor may throw const boost::regex e(regex, flag); if (e.status()) { std::cout << "Incorrect regex pattern!\n"; continue; }
-
在循环中获取一个字符串进行匹配:
std::cout << "String to match: "; while (std::getline(std::cin, str) && !str.empty()) {
-
将正则表达式应用于它并输出结果:
bool matched = boost::regex_match(str, e); std::cout << (matched ? "MATCH\n" : "DOES NOT MATCH\n"); std::cout << "String to match: "; } // end of `while (std::getline(std::cin, str))`
-
通过恢复
std::cin
并请求新的正则表达式模式来完成我们的示例:// Restoring std::cin std::cin.ignore(); std::cin.clear(); } while (1); } // int main()
现在如果我们运行前面的示例,我们会得到以下输出:
Available regex syntaxes: [0] Perl [1] Perl case insensitive [2] POSIX extended [3] POSIX extended case insensitive [4] POSIX basic [5] POSIX basic case insensitive Choose regex syntax: 0 Input regex: (\d{3}[#-]){2} String to match: 123-123# MATCH String to match: 312-321- MATCH String to match: 21-123- DOES NOT MATCH String to match: ^Z Input regex: \l{3,5} String to match: qwe MATCH String to match: qwert MATCH String to match: qwerty DOES NOT MATCH String to match: QWE DOES NOT MATCH String to match: ^Z Input regex: ^Z Press any key to continue . . .
它是如何工作的...
所有这些都是由 boost::regex
类完成的。它构建了一个能够进行正则表达式解析和编译的对象。flags
变量添加了额外的配置选项。
如果正则表达式不正确,它会抛出异常;如果传递了 boost::regex::no_except
标志,它会在 status()
调用中返回非零值以报告错误(就像在我们的示例中一样):
if (e.status()) {
std::cout << "Incorrect regex pattern!\n";
continue;
}
这将导致:
Input regex: (incorrect regex(
Incorrect regex pattern!
Input regex:
正则表达式匹配是通过调用 boost::regex_match
函数来完成的。如果匹配成功,则返回 true
。可以传递额外的标志给 regex_match
,但我们为了避免示例的简洁性而避免了它们的用法。
还有更多...
C++11 几乎包含了所有的 Boost.Regex
类和标志。它们可以在 std::
命名空间中的 <regex>
头文件中找到(而不是 boost::
)。官方文档提供了有关 C++11 和 Boost.Regex
之间差异的信息。它还包含了一些性能指标,表明 Boost.Regex
很快。
参见
-
“使用正则表达式搜索和替换字符串”菜谱将为你提供更多有关
Boost.Regex
使用的详细信息 -
你也可以考虑官方文档来获取有关标志、性能指标、正则表达式语法和 C++11 兼容性的更多信息,请参阅
www.boost.org/doc/libs/1_53_0/libs/regex/doc/html/index.html
使用正则表达式搜索和替换字符串
我的妻子非常喜欢 使用正则表达式匹配字符串 配方,并告诉我,除非我将它改进到能够根据正则表达式匹配替换输入字符串的部分,否则我不会得到食物。每个匹配的子表达式(正则表达式中的括号部分)必须从 1 开始有一个唯一的数字;这个数字将用于创建一个新的字符串。
这就是更新后的程序将如何工作的样子:
Available regex syntaxes:
[0] Perl
[1] Perl case insensitive
[2] POSIX extended
[3] POSIX extended case insensitive
[4] POSIX basic
[5] POSIX basic case insensitive
Choose regex syntax: 0
Input regex: (\d)(\d)
String to match: 00
MATCH: 0, 0,
Replace pattern: \1#\2
RESULT: 0#0
String to match: 42
MATCH: 4, 2,
Replace pattern: ###\1-\1-\2-\1-\1###
RESULT: ###4-4-2-4-4###
…
准备工作
我们将使用来自 使用正则表达式匹配字符串 配方的代码。在使用此配方之前,您应该阅读它。
需要将示例链接到 libboost_regex
库。
如何做到这一点...
此配方基于上一个配方中的代码。让我们看看需要更改什么。
-
不需要包含额外的头文件;然而,我们需要一个额外的字符串来存储替换模式:
std::string regex, str, replace_string;
-
我们将用
boost::regex_find
替换boost::regex_match
并输出匹配的结果:std::cout << "String to match: "; while (std::getline(std::cin, str) && !str.empty()) { boost::smatch results; bool matched = regex_search(str, results, e); if (matched) { std::cout << "MATCH: "; std::copy( results.begin() + 1, results.end(), std::ostream_iterator<std::string>( std::cout, ", ") );
-
之后,我们需要获取替换模式并应用它:
std::cout << "\nReplace pattern: "; if (std::getline(std::cin, replace_string) && !replace_string.empty()) { std::cout << "RESULT: " << boost::regex_replace(str, e, replace_string); } else { // Restoring std::cin std::cin.ignore(); std::cin.clear(); } } else { // `if (matched) ` std::cout << "DOES NOT MATCH"; }
就这样!大家都满意,我也吃饱了。
它是如何工作的...
boost::regex_search
函数不仅返回一个真或假(如 boost::regex_match
函数所做的那样)的值,而且还存储匹配的部分。我们使用以下构造来输出匹配的部分:
std::copy(
results.begin() + 1,
results.end(),
std::ostream_iterator<std::string>( std::cout, ", ")
);
注意,我们通过跳过第一个结果(results.begin() + 1
)来输出结果;这是因为 results.begin()
包含整个正则表达式匹配。
boost::regex_replace
函数执行所有替换并返回修改后的字符串。
还有更多...
regex_*
函数有不同的变体;其中一些接收双向迭代器而不是字符串,而另一些则向迭代器提供输出。
boost::smatch
是 boost::match_results<std::string::const_iterator>
的 typedef
;因此,如果您使用的是 std::string::const_iterator
以外的其他双向迭代器,您需要将您的双向迭代器类型用作 match_results
的模板参数。
match_results
具有格式化功能,因此我们可以用它来调整示例。而不是:
std::cout << "RESULT: " << boost::regex_replace(str, e, replace_string);
我们可以使用以下内容:
std::cout << "RESULT: " << results.format(replace_string);
顺便说一句,replace_string
可能具有不同的格式:
Input regex: (\d)(\d)
String to match: 12
MATCH: 1, 2,
Replace pattern: $1-$2---$&---$$
RESULT: 1-2---12---$
此配方中的所有类和函数都存在于 C++11 中,位于 <regex>
头文件的 std::
命名空间中。
参考以下内容
- 关于
Boost.Regex
的官方文档将为您提供更多示例以及有关性能、C++11 标准兼容性和正则表达式语法的更多信息,请参阅www.boost.org/doc/libs/1_53_0/libs/regex/doc/html/index.html
。使用正则表达式匹配字符串 配方将向您介绍Boost.Regex
的基础知识。
使用安全的 printf-like 函数格式化字符串
printf
函数族对安全性构成了威胁。允许用户将他们自己的字符串作为类型并格式化说明符是非常糟糕的设计。那么当需要用户定义的格式时我们该怎么办?我们该如何实现以下类的std::string to_string(const std::string& format_specifier) const;
成员函数?
class i_hold_some_internals {
int i;
std::string s;
char c;
// ...
};
准备工作
对于这个示例,只需要基本的 STL 知识就足够了。
如何做到这一点...
我们希望允许用户为字符串指定自己的输出格式。
-
为了安全地做到这一点,我们需要以下头文件:
#include <boost/format.hpp>
-
现在我们将为用户添加一些注释:
// fmt parameter must contain the following: // $1$ for outputting integer 'i' // $2$ for outputting string 's' // $3$ for outputting character 'c' std::string to_string(const std::string& format_specifier) const {
-
现在是时候让它们全部工作了:
boost::format f(format_specifier); unsigned char flags = boost::io::all_error_bits; flags ^= boost::io::too_many_args_bit; f.exceptions(flags); return (f % i % s % c).str(); }
就这些了。看看这段代码:
i_hold_some_internals class_instance; std::cout << class_instance.to_string( "Hello, dear %2%! " "Did you read the book for %1% %% %3%\n" ); std::cout << class_instance.to_string( "%1% == %1% && %1%%% != %1%\n\n" );
假设
class_instance
有一个成员i
等于100
,一个成员s
等于"Reader"
,一个成员c
等于'!'
。然后,程序将输出以下内容:Hello, dear Reader! Did you read the book for 100 % ! 100 == 100 && 100% != 100
它是如何工作的...
boost::format
类接受指定结果的字符串。参数通过operator%
传递给boost::format
。在格式指定字符串中指定的%1%
、%2%
、%3%
、%4%
等值将被传递给boost::format
的参数替换。
当格式字符串包含的参数少于传递给boost::format
的参数时,我们禁用异常:
boost::format f(format_specifier);
unsigned char flags = boost::io::all_error_bits;
flags ^= boost::io::too_many_args_bit;
这样做是为了允许一些格式,例如:
// Outputs 'Reader'
std::cout << class_instance.to_string("%2%\n\n");
更多...
如果格式不正确会发生什么?
try {
class_instance.to_string("%1% %2% %3% %4% %5%\n");
assert(false);
} catch (const std::exception& e) {
// boost::io::too_few_args exception must be caught
std::cout << e.what() << '\n';
}
好吧,在这种情况下,不会触发断言,以下行将被输出到控制台:
boost::too_few_args: format-string referred to more arguments than were passed
C++11 没有std::format
。Boost.Format
库不是一个非常快的库;尽量不在性能关键部分使用它。
参见
- 官方文档包含了关于
Boost.Format
库性能的更多信息。有关扩展 printf-like 格式的更多示例和文档,请访问www.boost.org/doc/libs/1_53_0/libs/format/
字符串的替换和删除
需要在字符串中删除某些内容、替换字符串的一部分或删除子字符串的第一个或最后一个出现的情况非常常见。STL 允许我们完成大部分这些操作,但通常需要编写过多的代码。
我们在改变大小写和大小写不敏感比较的示例中看到了Boost.StringAlgorithm
库的应用。让我们看看它如何简化我们在需要修改字符串时的生活:
#include <string>
const std::string str = "Hello, hello, dear Reader.";
准备工作
对于这个示例,需要基本的 C++知识。
如何做到这一点...
这个示例展示了Boost.StringAlgorithm
库中不同的字符串删除和替换方法是如何工作的。
删除操作需要包含#include <boost/algorithm/string/erase.hpp>
头文件:
namespace ba = boost::algorithm;
std::cout << "\n erase_all_copy :" << ba::erase_all_copy(str, ",");
std::cout << "\n erase_first_copy :" << ba::erase_first_copy(str, ",");
std::cout << "\n erase_last_copy :" << ba::erase_last_copy(str, ",");
std::cout << "\n ierase_all_copy :" << ba::ierase_all_copy(str, "hello");
std::cout << "\n ierase_nth_copy :" << ba::ierase_nth_copy(str, ",", 1);
这段代码将输出以下内容:
erase_all_copy :Hello hello dear Reader.
erase_first_copy :Hello hello, dear Reader.
erase_last_copy :Hello, hello dear Reader.
ierase_all_copy :, , dear Reader.
ierase_nth_copy :Hello, hello dear Reader.
替换操作需要包含<boost/algorithm/string/replace.hpp>
头文件:
namespace ba = boost::algorithm;
std::cout << "\n replace_all_copy :" << ba::replace_all_copy(str, ",", "!");
std::cout << "\n replace_first_copy :" << ba::replace_first_copy(str, ",", "!");
std::cout << "\n replace_head_copy :" << ba::replace_head_copy(str, 6, "Whaaaaaaa!");
这段代码将输出以下内容:
replace_all_copy :Hello! hello! dear Reader.
replace_first_copy :Hello! hello, dear Reader.
replace_head_copy :Whaaaaaaa! hello, dear Reader.
它是如何工作的...
所有示例都是自文档化的。唯一不明显的是replace_head_copy
函数。它接受要替换的字节数作为第二个参数,以及替换字符串作为第三个参数。所以,在上面的例子中,Hello
被替换为Whaaaaaaa!
。
还有更多...
还有修改字符串的内置方法。它们不仅以_copy
结尾并返回void
。所有不区分大小写的方法(以i
开头的方法)接受std::locale
作为最后一个参数,并使用默认构造的locale
作为默认参数。
C++11 没有Boost.StringAlgorithm
方法和类。
参见
-
官方文档包含大量示例和所有方法的完整参考,请访问
www.boost.org/doc/libs/1_53_0/doc/html/string_algo.html
-
有关
Boost.StringAlgorithm
库的更多信息,请参阅本章的改变大小写和大小写不敏感比较配方。
用两个迭代器表示字符串
有时候我们需要将一些字符串分割成子字符串并对这些子字符串进行操作。例如,计算字符串中的空格数,当然,我们希望使用 Boost 并尽可能高效。
准备工作
您需要了解一些基本的 STL 算法知识才能使用此配方。
如何做...
我们不会计算空格数;相反,我们将字符串分割成句子。您将看到使用 Boost 做这件事非常简单。
-
首先,包含正确的头文件:
#include <boost/algorithm/string/split.hpp> #include <boost/algorithm/string/classification.hpp> #include <algorithm>
-
现在,让我们定义我们的测试字符串:
int main() { const char str[] = "This is a long long character array." "Please split this character array to sentences!" "Do you know, that sentences are separated using period, " "exclamation mark and question mark? :-)" ;
-
现在我们为我们的分割迭代器创建一个
typedef
:typedef boost::split_iterator<const char*> split_iter_t;
-
构建那个迭代器:
split_iter_t sentences = boost::make_split_iterator(str, boost::algorithm::token_finder(boost::is_any_of("?!.")) );
-
现在我们可以遍历匹配项之间:
for (unsigned int i = 1; !sentences.eof(); ++sentences, ++i) { boost::iterator_range<const char*> range = *sentences; std::cout << "Sentence #" << i << " : \t" << range << '\n';
-
计算字符数:
std::cout << "Sentence has " << range.size() << " characters.\n";
-
然后计算空格数:
std::cout << "Sentence has " << std::count(range.begin(), range.end(), ' ') << " whitespaces.\n\n"; } // end of for(...) loop } // end of main()
就这样。现在如果我们运行这个示例,它将输出:
Sentence #1 : This is a long long character array Sentence has 35 characters. Sentence has 6 whitespaces. Sentence #2 : Please split this character array to sentences Sentence has 46 characters. Sentence has 6 whitespaces. Sentence #3 : Do you know, that sentences are separated using dot, exclamation mark and question mark Sentence has 87 characters. Sentence has 13 whitespaces. Sentence #4 : :-) Sentence has 4 characters. Sentence has 1 whitespaces.
如何工作...
此配方的核心思想是我们不需要从子字符串中构造std::string
。我们甚至不需要一次性对整个字符串进行分词。我们只需要找到第一个子字符串,并将其作为一对迭代器返回,一对迭代器分别指向子字符串的开始和结束。如果我们需要更多子字符串,找到下一个子字符串,并返回该子字符串的迭代器对。
现在,让我们更仔细地看看boost::split_iterator
。我们使用boost::make_split_iterator
函数构建了一个迭代器,该函数以range
作为第一个参数,以二进制查找谓词(或二进制谓词)作为第二个参数。当split_iterator
被解引用时,它返回第一个子字符串作为boost::iterator_range<const char*>
,它只包含一对迭代器,并有一些方法可以与之交互。当我们增加split_iterator
时,它将尝试找到下一个子字符串,如果没有找到子字符串,split_iterator::eof()
将返回true
。
还有更多...
boost::iterator_range
类在所有 Boost 库中都有广泛的应用。你可能会发现在需要返回一对迭代器或函数应该接受/使用一对迭代器的情况下,它对你的代码和库很有用。
boost::split_iterator<>
和 boost::iterator_range<>
类接受一个前向迭代器类型作为模板参数。因为我们之前的工作是在字符数组上,所以我们提供了 const char*
作为迭代器。如果我们使用 std::wstring
,我们需要使用 boost::split_iterator<std::wstring::const_iterator>
和 boost::iterator_range<std::wstring::const_iterator>
类型。
C++11 既没有 iterator_range
也没有 split_iterator
。
由于 boost::iterator_range
类没有虚拟函数和动态内存分配,它既快又高效。然而,它的输出流操作符 <<
对字符数组没有特定的优化,因此流操作较慢。
boost::split_iterator
类中有一个 boost::function
类,因此构造它可能较慢;然而,迭代只会增加微小的开销,你甚至可能在性能关键部分都感觉不到。
参见
-
下一个配方将告诉你关于
boost::iterator_range<const char*>
的一个很好的替代方案。 -
Boost.StringAlgorithm
的官方文档将为你提供关于类和大量示例的更详细信息,请参阅www.boost.org/doc/libs/1_53_0/doc/html/string_algo.html
。 -
更多关于
boost::iterator_range
的信息可以在以下链接找到:www.boost.org/doc/libs/1_53_0/libs/range/doc/html/range/reference/utilities.html
。它是Boost.Range
库的一部分,本书没有描述,但你可能希望自学。
使用字符串类型的引用
这个配方是本章最重要的配方!让我们看看一个非常常见的案例,其中我们编写一个函数,该函数接受一个字符串,并返回 starts
和 ends
参数传递的字符值之间的字符串部分:
#include <string>
#include <algorithm>
std::string between_str(const std::string& input, char starts,
char ends)
{
std::string::const_iterator pos_beg
= std::find(input.begin(), input.end(), starts);
if (pos_beg == input.end()) {
return std::string(); // Empty
}
++ pos_beg;
std::string::const_iterator pos_end
= std::find(input.begin(), input.end(), ends);
return std::string(pos_beg, pos_end);
}
你喜欢这个实现吗?在我看来,它看起来很糟糕;考虑以下对其的调用:
between_str("Getting expression (between brackets)", '(', ')');
在这个例子中,一个临时的 std::string
变量将从 "Getting expression (between brackets)"
构造出来。字符数组足够长,所以有很大可能在 std::string
构造函数内部调用动态内存分配,并将字符数组复制到其中。然后,在 between_str
函数的某个地方,将构造一个新的 std::string
,这也可能导致另一个动态内存分配,并导致复制。
因此,这个简单的函数可能,并且在大多数情况下会:
-
调用动态内存分配(两次)
-
复制字符串(两次)
-
释放内存(两次)
我们能做得更好吗?
准备工作
此配方需要基本的 STL 和 C++ 知识。
如何实现...
在这里我们实际上并不需要一个 std::string
类,我们只需要指向字符数组的指针以及数组的大小。Boost 提供了 std::string_ref
类。
-
要使用
boost::string_ref
类,需要包含以下头文件:#include <boost/utility/string_ref.hpp>
-
修改方法的签名:
boost::string_ref between( const boost::string_ref& input, char starts, char ends)
-
在函数体内将
std::string
改为boost::string_ref
:{ boost::string_ref::const_iterator pos_beg = std::find(input.cbegin(), input.cend(), starts); if (pos_beg == input.cend()) { return boost::string_ref(); // Empty } ++ pos_beg; boost::string_ref::const_iterator pos_end = std::find(input.cbegin(), input.cend(), ends); // ...
-
boost::string_ref
构造函数接受大小作为第二个参数,因此我们需要稍微修改代码:if (pos_end == input.cend()) { return boost::string_ref(pos_beg, input.end() - pos_beg); } return boost::string_ref(pos_beg, pos_end - pos_beg); }
就这样!现在我们可以调用
between("Getting expression (between brackets)", '(', ')')
,它将无需任何动态内存分配和字符复制即可工作。我们仍然可以使用它来处理std::string
:between(std::string("(expression)"), '(', ')')
它是如何工作的...
如前所述,boost::string_ref
只包含指向字符数组的指针和数据的大小。它有很多构造函数,并且可以以不同的方式初始化:
boost::string_ref r0("^_^");
std::string O_O("O__O");
boost::string_ref r1 = O_O;
std::vector<char> chars_vec(10, '#');
boost::string_ref r2(&chars_vec.front(), chars_vec.size());
boost::string_ref
类拥有容器类所需的所有方法,因此它可以与 STL 算法和 Boost 算法一起使用:
#include <boost/algorithm/string/case_conv.hpp>
#include <boost/algorithm/string/replace.hpp>
#include <boost/lexical_cast.hpp>
#include <iterator>
void string_ref_algorithms_examples() {
boost::string_ref r("O_O");
// Finding symbol
std::find(r.cbegin(), r.cend(), '_');
// Will print 'o_o'
boost::to_lower_copy(std::ostream_iterator<char>(std::cout), r);
std::cout << '\n';
// Will print 'O_O'
std::cout << r << '\n';
// Will print '^_^'
boost::replace_all_copy(
std::ostream_iterator<char>(std::cout), r, "O", "^"
);
}
注意
boost::string_ref
类实际上并不拥有字符串,因此它所有的方法都返回常量迭代器。正因为如此,我们不能在修改数据的函数中使用它,例如 boost::to_lower(r)
。
当使用 boost::string_ref
时,我们应该特别注意它所引用的数据;它必须在整个 boost::string_ref
的生命周期内存在且有效。
还有更多...
boost::string_ref
类不是 C++11 的组成部分,但它被提议包含在下一个标准中。
string_ref
类快速且高效;在可能的情况下使用它们。
boost::string_ref
类实际上是 boost::
命名空间中的一个 typedef:
typedef basic_string_ref<char, std::char_traits<char> >
string_ref;
你可能还会发现 boost::
命名空间中宽字符的以下 typedefs 有用:
typedef basic_string_ref<wchar_t, std::char_traits<wchar_t> >
wstring_ref;
typedef basic_string_ref<char16_t, std::char_traits<char16_t> >
u16string_ref;
typedef basic_string_ref<char32_t, std::char_traits<char32_t> >
u32string_ref;
参见
-
将
string_ref
包含到 C++ 标准中的官方提案可以在www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3442.html
找到 -
string_ref
的 Boost 文档可以在www.boost.org/doc/libs/1_53_0/libs/utility/doc/html/string_ref.html
找到
第八章。元编程
在本章中,我们将介绍:
-
使用类型“类型向量”
-
操作类型向量
-
在编译时获取函数的结果类型
-
创建高阶元函数
-
惰性评估元函数
-
将所有元组元素转换为字符串
-
分解元组
简介
本章致力于一些酷且难以理解的元编程方法。这些方法不是日常使用,但它们将在通用库的开发中提供实际帮助。
第四章,编译时技巧已经涵盖了元编程的基础。阅读它以更好地理解。在本章中,我们将更深入地探讨如何将多个类型打包到一个单一的元组类型中。我们将创建用于操作类型集合的函数,我们将看到编译时集合的类型如何改变,以及编译时技巧如何与运行时混合。所有这些都是元编程。
系好安全带,准备就绪,我们出发了!
使用类型“类型向量”
有时候,如果我们能像在容器中一样处理所有模板参数,那将非常棒。想象一下,我们正在编写像Boost.Variant
这样的东西:
#include <boost/mpl/aux_/na.hpp>
// boost::mpl::na == n.a. == not available
template <
class T0 = boost::mpl::na,
class T1 = boost::mpl::na,
class T2 = boost::mpl::na,
class T3 = boost::mpl::na,
class T4 = boost::mpl::na,
class T5 = boost::mpl::na,
class T6 = boost::mpl::na,
class T7 = boost::mpl::na,
class T8 = boost::mpl::na,
class T9 = boost::mpl::na
>
struct variant;
而前面的代码是所有以下有趣任务开始发生的地方:
-
我们如何从所有类型中移除常量和易失性限定符?
-
我们如何移除重复的类型?
-
我们如何获取所有类型的大小?
-
我们如何获取输入参数的最大大小?
所有这些任务都可以轻松地使用Boost.MPL
解决。
准备工作
为了使用这个配方,需要具备第四章编译时技巧的基本知识。在阅读之前鼓起勇气——这个配方中会有很多元编程。
如何做到这一点…
我们已经看到如何在编译时操作类型。为什么我们不能更进一步,将多个类型组合到一个数组中,并对该数组的每个元素执行操作?
-
首先,让我们将所有类型打包到
Boost.MPL
类型容器中的一个:#include <boost/mpl/vector.hpp> template < class T0, class T1, class T2, class T3, class T4, class T5, class T6, class T7, class T8, class T9 > struct variant { typedef boost::mpl::vector<T0, T1, T2, T3, T4, T5, T6, T7,T8, T9> types; };
-
让我们使我们的例子不那么抽象,看看如果我们指定类型,它会如何工作:
#include <string> struct declared{ unsigned char data[4096]; }; struct non_defined; typedef variant< volatile int, const int, const long, declared, non_defined, std::string >::types types;
-
我们可以在编译时检查一切。让我们断言类型不为空:
#include <boost/static_assert.hpp> #include <boost/mpl/empty.hpp> BOOST_STATIC_ASSERT((!boost::mpl::empty<types>::value));
-
我们还可以检查,例如,
non_defined
类型仍然位于索引4
的位置:#include <boost/mpl/at.hpp> #include <boost/type_traits/is_same.hpp> BOOST_STATIC_ASSERT((boost::is_same< non_defined, boost::mpl::at_c<types, 4>::type >::value));
-
以及最后一个类型仍然是
std::string
:#include <boost/mpl/back.hpp> BOOST_STATIC_ASSERT((boost::is_same< boost::mpl::back<types>::type, std::string >::value));
-
现在,当我们确信类型确实包含传递给我们的变体结构的所有类型时,我们可以进行一些转换。我们将从移除常量和易失性限定符开始:
#include <boost/mpl/transform.hpp> #include <boost/type_traits/remove_cv.hpp> typedef boost::mpl::transform< types, boost::remove_cv<boost::mpl::_1> >::type noncv_types;
-
现在我们移除重复的类型:
#include <boost/mpl/unique.hpp> typedef boost::mpl::unique< noncv_types, boost::is_same<boost::mpl::_1, boost::mpl::_2> >::type unique_types;
-
现在我们可以检查这个向量只包含
5
种类型:#include <boost/mpl/size.hpp> BOOST_STATIC_ASSERT((boost::mpl::size<unique_types>::value == 5));
-
下一步是计算大小:
// Without this we'll get an error: // use of undefined type 'non_defined' struct non_defined{}; #include <boost/mpl/sizeof.hpp> typedef boost::mpl::transform< unique_types, boost::mpl::sizeof_<boost::mpl::_1> >::type sizes_types;
-
最后一步是获取最大大小:
#include <boost/mpl/max_element.hpp> typedef boost::mpl::max_element<sizes_types>::type max_size_type;
我们可以断言类型的最大大小等于结构的声明大小,这必须是我们例子中的最大值:
BOOST_STATIC_ASSERT(max_size_type::type::value == sizeof(declared));
它是如何工作的…
boost::mpl::vector
类是一个编译时容器,用于存储类型。更准确地说,它是一个存储类型的类型。我们不会为其创建实例;相反,我们只是在 typedef 中使用它。
与 STL 容器不同,Boost.MPL
容器没有成员方法。相反,方法是在单独的头文件中声明的。因此,为了使用某些方法,我们需要:
-
包含正确的头文件
-
调用该方法,通常通过指定容器作为第一个参数
这里是另一个例子:
#include <boost/mpl/size.hpp>
#include <cassert>
template <class Vector>
int foo_size() {
return boost::mpl::size<Vector>::value;
}
int main() {
typedef boost::mpl::vector<int,int,int> vector1_type;
assert(foo_size<vector1_type>() == 3);
}
这些方法你应该很熟悉。我们已经在第四章,编译时技巧中看到了元函数。顺便说一下,我们还在使用一些来自熟悉的 Boost.TypeTraits
库的元函数(例如 boost::is_same
)。
因此,在第 3 步、第 4 步和第 5 步中,我们只是调用我们容器类型的元函数。
最难的部分即将到来!
记住,占位符在 boost::bind
和 Boost.Asio
库中被广泛使用。Boost.MPL
也有它们,并且它们是组合元函数所必需的:
typedef boost::mpl::transform<
types,
boost::remove_cv<boost::mpl::_1>
>::type noncv_types;
在这里,boost::mpl::_1
是一个占位符,整个表达式意味着“对于类型中的每个类型,执行 boost::remove_cv<>::type
并将那个类型推回到结果向量中。通过 ::type
返回结果向量”。
让我们转到第 7 步。在这里,我们使用 boost::is_same<boost::mpl::_1, boost::mpl::_2>
模板参数指定 boost::mpl::unique
的比较元函数,其中 boost::mpl::_1
和 boost::mpl::_2
是占位符。你可能觉得它与 boost::bind(std::equal_to(), _1, _2)
类似,第 7 步中的整个表达式类似于以下伪代码:
std::vector<type> types;
// ...
std::unique(types.begin(), types.end(),
boost::bind(std::equal_to<type>(), _1, _2));
在第 9 步中,有一些有趣的内容,这对于更好地理解是必需的。在先前的代码中,sizes_types
不是一个值的向量,而是一个整型常量的向量——代表数字的类型。sizes_types
typedef 实际上是以下类型:
struct boost::mpl::vector<
struct boost::mpl::size_t<4>,
struct boost::mpl::size_t<4>,
struct boost::mpl::size_t<4096>,
struct boost::mpl::size_t<1>,
struct boost::mpl::size_t<32>
>
最后一步现在应该很清晰了。它只是从 sizes_types
typedef 中获取最大元素。
注意
我们可以在任何允许使用 typedef 的地方使用 Boost.MPL
元函数。
还有更多...
使用 Boost.MPL
库会导致编译时间更长,但它让你能够使用类型做任何你想做的事情。它不会增加运行时开销,甚至不会在二进制文件中增加一条指令。C++11 没有 Boost.MPL
类,Boost.MPL
也不使用 C++11 的特性,如变长模板。这使得在 C++11 编译器上 Boost.MPL
的编译时间更长,但使其在 C++03 编译器上可用。
参见
-
参见第四章,编译时技巧,了解元编程的基础
-
操作类型向量 的配方将为您提供更多关于元编程和
Boost.MPL
库的信息 -
请参阅官方
Boost.MPL
文档以获取更多示例和完整参考,请访问www.boost.org/doc/libs/1_53_0/libs/mpl/doc/in
dex.html
操作类型向量
本配方的任务将是根据第二个 boost::mpl::vector
函数的内容修改一个 boost::mpl::vector
函数的内容。我们将调用第二个向量作为修饰符向量,并且每个修饰符都可以有以下的类型:
// Make unsigned
struct unsigne; // No typo: 'unsigned' is a keyword, we cannot use it.
// Make constant
struct constant;
// Otherwise we do not change type
struct no_change;
那么我们应该从哪里开始呢?
准备工作
需要基本了解 Boost.MPL
。阅读 使用类型 "类型向量" 配方和 第四章,编译时技巧 可能有所帮助。
如何做到这一点...
本配方与上一个配方类似,但它还使用了条件编译时语句。准备好,这不会容易!
-
我们应该从头文件开始:
// we'll need this at step 3 #include <boost/mpl/size.hpp> #include <boost/type_traits/is_same.hpp> #include <boost/static_assert.hpp> // we'll need this at step 4 #include <boost/mpl/if.hpp> #include <boost/type_traits/make_unsigned.hpp> #include <boost/type_traits/add_const.hpp> // we'll need this at step 5 #include <boost/mpl/transform.hpp>
-
现在,让我们将所有元编程魔法放入结构中,以便于更简单的重用:
template <class Types, class Modifiers> struct do_modifications {
-
检查传入的向量是否具有相同的大小是一个好主意:
BOOST_STATIC_ASSERT((boost::is_same< typename boost::mpl::size<Types>::type, typename boost::mpl::size<Modifiers>::type >::value));
-
现在让我们处理修改元函数:
typedef boost::mpl::if_< boost::is_same<boost::mpl::_2, unsigne>, boost::make_unsigned<boost::mpl::_1>, boost::mpl::if_< boost::is_same<boost::mpl::_2, constant>, boost::add_const<boost::mpl::_1>, boost::mpl::_1 > > binary_operator_t;
-
最后一步:
typedef typename boost::mpl::transform< Types, Modifiers, binary_operator_t >::type type; };
我们现在可以运行一些测试,确保我们的元函数工作正确:
#include <boost/mpl/vector.hpp> typedef boost::mpl::vector<unsigne, no_change, constant, unsigne> modifiers; typedef boost::mpl::vector<int, char, short, long> types; typedef do_modifications<types, modifiers>::type result_type; #include <boost/mpl/at.hpp> BOOST_STATIC_ASSERT((boost::is_same< boost::mpl::at_c<result_type, 0>::type, unsigned int >::value)); BOOST_STATIC_ASSERT((boost::is_same< boost::mpl::at_c<result_type, 1>::type, char >::value)); BOOST_STATIC_ASSERT((boost::is_same< boost::mpl::at_c<result_type, 2>::type, const short >::value)); BOOST_STATIC_ASSERT((boost::is_same< boost::mpl::at_c<result_type, 3>::type, unsigned long >::value));
它是如何工作的...
在第 3 步中,我们断言大小相等,但我们以一种不寻常的方式进行。boost::mpl::size<Types>::type
元函数实际上返回整数常量 struct boost::mpl::long_<4>
,因此在静态断言中,我们实际上比较的是两种类型,而不是两个数字。这可以以更熟悉的方式重写:
BOOST_STATIC_ASSERT((
boost::mpl::size<Types>::type::value
==
boost::mpl::size<Modifiers>::type::value
));
注意
注意我们使用的 typename
关键字。没有它,编译器将无法决定 ::type
实际上是一个类型还是某个变量。之前的配方不需要它,因为我们在使用元函数参数时,参数是完全已知的。但在这个配方中,元函数的参数是一个模板。
在处理第 4 步之前,我们先看看第 5 步。在第 5 步中,我们将第 4 步中的 Types
、Modifiers
和 binary_operator_t
参数传递给 boost::mpl::transform
元函数。这个元函数相当简单——对于每个传入的向量,它取一个元素并将其传递给第三个参数——一个二元元函数。如果我们用伪代码重写它,它将看起来像以下这样:
vector result;
for (std::size_t i = 0; i < Types.size(); ++i) {
result.push_back(
binary_operator_t(Types[i], Modifiers[i])
);
}
return result;
第 4 步可能会让人头疼。在这一步,我们正在编写一个元函数,它将为 Types
和 Modifiers
向量中的每一对类型调用(参见前面的伪代码)。正如我们已经知道的,boost::mpl::_2
和 boost::mpl::_1
是占位符。在这个配方中,_1
是 Types
向量中类型的占位符,而 _2
是 Modifiers
向量中类型的占位符。
所以整个元函数就是这样工作的:
-
将传递给它的第二个参数(通过
_2
)与unsigned
类型进行比较 -
如果类型相等,则将传递给它的第一个参数(通过
_1
)转换为unsigned
并返回该类型 -
否则,将传递给它的第二个参数(通过
_2
)与一个常量类型进行比较 -
如果类型相等,则将其传递给它的第一个参数(通过
_1
)设为常量并返回该类型 -
否则,返回传递给它的第一个参数(通过
_1
)
在构建这个元函数时,我们需要非常小心。还应该特别注意,不要在它的末尾调用::type
:
>::type binary_operator_t; // INCORRECT!
如果我们调用::type
,编译器将尝试在此处评估二元运算符,这将导致编译错误。在伪代码中,这样的尝试将看起来像这样:
binary_operator_t foo;
// Attempt to call binary_operator_t::operator() without parameters,
// when it has version only with two parameters
foo();
还有更多...
使用元函数需要一些练习。即使是你的谦卑仆人也无法在第一次尝试就正确编写一些函数(第二次和第三次尝试也不是很好)。不要害怕实验!
Boost.MPL
库不是 C++11 的一部分,也不使用 C++11 特性,但它可以与 C++11 变长模板一起使用:
template <class... T>
struct vt_example {
typedef typename boost::mpl::vector<T...> type;
};
BOOST_STATIC_ASSERT((boost::is_same<
boost::mpl::at_c<vt_example<int, char, short>::type, 0>::type,
int
>::value));
与往常一样,元函数不会向生成的二进制文件添加任何指令,并且不会降低性能。然而,通过使用它们,你可以使你的代码更适应特定的情况。
参见
-
从本章的开始阅读,以获取更多
Boost.MPL
使用的简单示例 -
请参阅第四章,编译时技巧,特别是为模板参数选择最佳运算符配方,其中包含类似于
binary_operator_t
元函数的代码 -
Boost.MPL
的官方文档有更多示例和完整的目录表,请参阅www.boost.org/doc/libs/1_53_0/libs/mpl/doc/index.html
在编译时获取函数的结果类型
C++11 添加了许多功能以简化元编程。为了接近替代函数语法,必须用 C++03 编写大量代码。
template <class T1, class T2>
auto my_function_cpp11(const T1& v1, const T2& v2)
-> decltype(v1 + v2)
{
return v1 + v2;
}
它允许我们更容易地编写泛型函数并在困难的情况下工作:
#include <cassert>
struct s1 {};
struct s2 {};
struct s3 {};
inline s3 operator + (const s1& /*v1*/, const s2& /*v2*/) {
return s3();
}
inline s3 operator + (const s2& /*v1*/, const s1& /*v2*/) {
return s3();
}
int main() {
s1 v1;
s2 v2;
my_function_cpp11(v1, v2);
my_function_cpp11(v1, v2);
assert(my_function_cpp11('\0', 1) == 1);
}
但 Boost 有很多这样的函数,并且不需要 C++11 即可工作。
这怎么可能,以及我们如何创建my_function_cpp11
函数的 C++03 版本?
准备工作
对于这个配方,需要具备基本的 C++和模板知识。
如何做到这一点...
C++11 极大地简化了元编程。为了接近替代函数语法,必须用 C++03 编写大量代码。
-
我们需要包含以下头文件:
#include <boost/type_traits/common_type.hpp>
-
现在,我们需要为任何类型在
result_of
命名空间中创建一个元函数:namespace result_of { template <class T1, class T2> struct my_function_cpp03 { typedef typename boost::common_type<T1, T2>::type type; };
-
并针对类型
s1
和s2
进行专门化:template <> struct my_function_cpp03<s1, s2> { typedef s3 type; }; template <> struct my_function_cpp03<s2, s1> { typedef s3 type; }; } // namespace result_of
-
现在我们已经准备好编写
my_function_cpp03
函数:template <class T1, class T2> inline typename result_of::my_function_cpp03<T1, T2>::type my_function_cpp03(const T1& v1, const T2& v2) { return v1 + v2; }
现在我们可以几乎像使用 C++11 函数一样使用这个函数:
s1 v1; s2 v2; my_function_cpp03(v1, v2); my_function_cpp03(v2, v1); assert(my_function_cpp03('\0', 1) == 1);
它是如何工作的...
这个食谱的主要思想是我们可以制作一个特殊的元函数,该元函数将推导出结果类型。这种技术可以在整个 Boost 库中看到,例如在 Boost.Variants
的 boost::get<>
实现中,或者在 Boost.Fusion
的几乎所有函数中。
现在,让我们一步一步地来。result_of
命名空间只是一种传统,但你可以使用自己的,这不会有什么影响。boost::common_type<>
元函数推导出几个类型共有的类型,所以我们将其用作一般情况。我们还为 s1
和 s2
类型添加了 my_function_cpp03
结构的两个模板特化。
注意
在 C++03 中编写元函数的缺点是,有时我们可能需要编写大量的代码。比较 my_function_cpp11
和 my_function_cpp03
包括 result_of
命名空间的代码量,以查看差异。
当元函数准备好后,我们可以在不使用 C++11 的情况下推导出结果类型,因此编写 my_function_cpp03
将会像做饼一样简单:
template <class T1, class T2>
inline typename result_of::my_function_cpp03<T1, T2>::type
my_function_cpp03(const T1& v1, const T2& v2)
{
return v1 + v2;
}
更多...
这种技术不会增加运行时开销,但它可能会稍微减慢编译速度。你也可以使用 C++11 编译器。
参见
- 从第四章(Chapter 4
制作一个高阶元函数
接受其他函数作为输入参数或返回其他函数的函数称为高阶函数。例如,以下函数是高阶的:
function_t higher_order_function1();
void higher_order_function2(function_t f);
function_t higher_order_function3(function_t f);
我们已经在本章的“使用类型 '类型向量'”和“操作类型向量”的食谱中看到了高阶元函数,其中我们使用了 boost::transform
。
在这个食谱中,我们将尝试制作一个名为 coalesce
的高阶元函数,它接受两种类型和两个元函数。coalesce
元函数将第一个类型参数应用于第一个元函数,并将结果类型与 boost::mpl::false_ type
元函数进行比较。如果结果类型是 boost::mpl::false_ type
元函数,它将返回将第二个类型参数应用于第二个元函数的结果,否则,它将返回第一个结果类型:
template <class Param1, class Param2, class Func1, class Func2>
struct coalesce;
准备工作
这个食谱(和章节)有点棘手。强烈建议从本章开始阅读。
如何做到这一点…
Boost.MPL
元函数实际上是结构,可以很容易地作为模板参数传递。困难的部分是正确地做到这一点。
-
我们需要以下头文件来编写高阶元函数:
#include <boost/mpl/apply.hpp> #include <boost/mpl/if.hpp> #include <boost/type_traits/is_same.hpp>
-
下一步是评估我们的函数:
template <class Param1, class Param2, class Func1, class Func2> struct coalesce { typedef typename boost::mpl::apply<Func1, Param1>::type type1; typedef typename boost::mpl::apply<Func2, Param2>::type type2;
-
现在我们需要选择正确的结果类型:
typedef typename boost::mpl::if_< boost::is_same< boost::mpl::false_, type1>, type2, type1 >::type type; };
就这样!我们已经完成了一个高阶元函数!现在我们可以像这样使用它:
#include <boost/static_assert.hpp> #include <boost/mpl/not.hpp> using boost::mpl::_1; using boost::mpl::_2; typedef coalesce< boost::mpl::true_, boost::mpl::true_, boost::mpl::not_<_1>, boost::mpl::not_<_1> >::type res1_t; BOOST_STATIC_ASSERT((!res1_t::value)); typedef coalesce< boost::mpl::true_, boost::mpl::false_, boost::mpl::not_<_1>, boost::mpl::not_<_1> >::type res2_t; BOOST_STATIC_ASSERT((res2_t::value));
它是如何工作的...
编写高阶元函数的主要问题是处理占位符。这就是为什么我们不应该直接调用Func1<Param1>::type
。相反,我们应该使用boost::apply
元函数,它接受一个函数和最多五个参数,这些参数将被传递给该函数。
注意
您可以配置boost::mpl::apply
以接受更多的参数,将BOOST_MPL_LIMIT_METAFUNCTION_ARITY
宏定义为所需的参数数量,例如,为 6。
还有更多...
C++11 没有与Boost.MPL
库类似的库来应用元函数。
参见
- 请参阅官方文档,特别是教程部分,以获取有关
Boost.MPL
的更多信息,请访问www.boost.org/doc/libs/1_53_0/libs/mpl/doc/index.html
惰性评估元函数
惰性求值意味着函数只有在真正需要其结果时才会被调用。了解这个配方对于编写良好的元函数非常重要。惰性求值的重要性将在以下示例中展示。
想象我们正在编写一个接受函数、参数和条件的元函数。如果条件为false
,该函数的结果类型必须是fallback
类型,否则结果如下:
struct fallback;
template <
class Func,
class Param,
class Cond,
class Fallback = fallback>
struct apply_if;
而前面的代码是我们不能没有惰性求值的地方。
准备工作
阅读第四章 Chapter 4,编译时技巧,强烈推荐。然而,对元编程的良好了解应该足够了。
如何做到这一点...
我们将看到这个配方对于编写良好的元函数是至关重要的:
-
我们需要以下头文件:
#include <boost/mpl/apply.hpp> #include <boost/mpl/eval_if.hpp> #include <boost/mpl/identity.hpp>
-
函数的开始很简单:
template <class Func, class Param, class Cond, class Fallback> struct apply_if { typedef typename boost::mpl::apply< Cond, Param >::type condition_t;
-
但我们在这里要小心:
typedef boost::mpl::apply<Func, Param> applied_type;
-
在评估表达式时必须格外小心:
typedef typename boost::mpl::eval_if_c< condition_t::value, applied_type, boost::mpl::identity<Fallback> >::type type; };
就这样!现在我们可以像这样自由地使用它:
#include <boost/static_assert.hpp> #include <boost/type_traits/is_integral.hpp> #include <boost/type_traits/make_unsigned.hpp> #include <boost/type_traits/is_same.hpp> using boost::mpl::_1; using boost::mpl::_2; typedef apply_if< boost::make_unsigned<_1>, int, boost::is_integral<_1> >::type res1_t; BOOST_STATIC_ASSERT(( boost::is_same<res1_t, unsigned int>::value )); typedef apply_if< boost::make_unsigned<_1>, float, boost::is_integral<_1> >::type res2_t; BOOST_STATIC_ASSERT(( boost::is_same<res2_t, fallback>::value ));
它是如何工作的...
这个配方的核心思想是,如果条件为false
,我们不应该执行元函数。因为当条件为false
时,该类型的元函数可能不起作用:
// will fail with static assert somewhere deep in implementation
// of boost::make_unsigned<_1> if we won't be evaluating function // lazy.
typedef apply_if<
boost::make_unsigned<_1>,
float,
boost::is_integral<_1>
>::type res2_t;
BOOST_STATIC_ASSERT((
boost::is_same<res2_t, fallback>::value
));
那么,我们如何惰性评估一个元函数呢?
如果没有访问元函数的内部类型或值,编译器不会查看元函数。换句话说,当我们尝试通过::
获取其成员时,编译器将尝试编译元函数。这就是apply_if
的错误版本的样子:
template <class Func, class Param, class Cond, class Fallback>
struct apply_if {
typedef boost::mpl::apply<Cond, Param> condition_t;
// Incorrect, metafunction is evaluated when `::type` called
typedef typename boost::mpl::apply<Func, Param>::type applied_type;
typedef typename boost::mpl::if_c<
condition_t::value,
applied_type,
boost::mpl::identity<Fallback>
>::type type;
};
这与我们的示例不同,在步骤 3 我们没有调用::type
,而是使用eval_if_c
实现了步骤 4,它只为其中一个参数调用::type
。boost::mpl::eval_if_c
元函数是这样实现的:
template<bool C, typename F1, typename F2>
struct eval_if_c {
typedef typename if_c<C,F1,F2>::type f_;
typedef typename f_::type type;
};
因为boost::mpl::eval_if_c
在成功条件下调用::type
,而fallback
可能没有::type
,所以我们被要求将fallback
包装进boost::mpl::identity
类。boost::mpl::identity
类是一个非常简单但有用的结构,它通过::type
调用返回其模板参数:
template <class T>
struct identity {
typedef T type;
};
还有更多...
正如我们之前提到的,C++11 没有Boost.MPL
的类,但我们可以像boost::mpl::identity<T>
一样使用std::common_type<T>
的单个参数。
就像往常一样,元函数不会向输出二进制文件中添加任何行。因此,你可以多次使用元函数。你编译时做得越多,运行时剩下的就越少。
参见
-
boost::mpl::identity
类型可以用来禁用模板函数的参数依赖查找(ADL)。参见<boost/implicit_cast.hpp>
头文件中boost::implicit_cast
的源代码。 -
从头开始阅读这一章和
Boost.MPL
的官方文档可能会有所帮助:www.boost.org/doc/libs/1_53_0/libs/mpl/doc/index.html
将所有元组元素转换为字符串
这个菜谱和下一个菜谱都是关于编译时间和运行时特性的混合。我们将使用Boost.Fusion
库来看看它能做什么。
记住,我们在第一章中讨论了元组和数组。现在我们想要编写一个单一函数,该函数可以将元组和数组的元素流式传输到字符串中。
准备工作
你应该了解boost::tuple
和boost::array
类以及boost::lexical_cast
函数。
如何做到这一点...
我们已经几乎知道了在这个菜谱中将要使用的所有函数和类。我们只需要将它们全部聚集在一起。
-
我们需要编写一个将任何类型转换为字符串的函数:
#include <boost/lexical_cast.hpp> #include <boost/noncopyable.hpp> struct stringize_functor: boost::noncopyable { private: std::string& result; public: explicit stringize_functor(std::string& res) : result(res) {} template <class T> void operator()(const T& v) const { result += boost::lexical_cast<std::string>(v); } };
-
这段代码的棘手之处在于:
#include <boost/fusion/include/for_each.hpp> template <class Sequence> std::string stringize(const Sequence& seq) { std::string result; boost::fusion::for_each(seq, stringize_functor(result)); return result; }
-
就这些!现在我们可以将任何东西转换成字符串:
struct cat{}; std::ostream& operator << (std::ostream& os, const cat& ) { return os << "Meow! "; } #include <iostream> #include <boost/fusion/adapted/boost_tuple.hpp> #include <boost/fusion/adapted/std_pair.hpp> #include <boost/fusion/adapted/boost_array.hpp> int main() { boost::fusion::vector<cat, int, std::string> tup1(cat(), 0, "_0"); boost::tuple<cat, int, std::string> tup2(cat(), 0, "_0"); std::pair<cat, cat> cats; boost::array<cat, 10> many_cats; std::cout << stringize(tup1) << '\n' << stringize(tup2) << '\n' << stringize(cats) << '\n' << stringize(many_cats) << '\n'; }
之前的示例将输出以下内容:
Meow! 0_0 Meow! 0_0 Meow! Meow! Meow! Meow! Meow! Meow! Meow! Meow! Meow! Meow! Meow! Meow!
它是如何工作的...
stringize
函数的主要问题在于boost::tuple
和std::pair
都没有begin()
或end()
方法,因此我们无法调用std::for_each
。这正是Boost.Fusion
介入的地方。
Boost.Fusion
库包含许多出色的算法,可以在编译时操作结构。
boost::fusion::for_each
函数按顺序遍历元素,并将一个函数应用于每个元素。
注意,我们已经包含了:
#include <boost/fusion/adapted/boost_tuple.hpp>
#include <boost/fusion/adapted/std_pair.hpp>
#include <boost/fusion/adapted/boost_array.hpp>
这是因为默认情况下,Boost.Fusion
只与其自己的类一起工作。Boost.Fusion
有自己的元组类,boost::fusion::vector
,它与boost::tuple
非常相似:
#include <boost/tuple/tuple.hpp>
#include <string>
#include <cassert>
void tuple_example() {
boost::tuple<int, int, std::string> tup(1, 2, "Meow");
assert(boost::get<0>(tup) == 1);
assert(boost::get<2>(tup) == "Meow");
}
#include <boost/fusion/include/vector.hpp>
#include <boost/fusion/include/at_c.hpp>
void fusion_tuple_example() {
boost::fusion::vector<int, int, std::string> tup(1, 2, "Meow");
assert(boost::fusion::at_c<0>(tup) == 1);
assert(boost::fusion::at_c<2>(tup) == "Meow");
}
但 boost::fusion::vector
并不像 boost::tuple
那样简单。我们将在 拆分元组 配方中看到区别。
还有更多...
boost::fusion::for_each
和 std::for_each
之间存在一个基本区别。std::for_each
函数内部包含一个循环,并在运行时确定将执行多少次迭代。然而,boost::fusion::for_each
在编译时就知道迭代次数,并完全展开循环,为 stringize(tup2)
生成以下代码:
std::string result;
// Instead of
// boost::fusion::for_each(seq, stringize_functor(result));
// there'll be the following:
{
stringize_functor functor(result);
functor(boost::fusion::at_c<0>(tup2));
functor(boost::fusion::at_c<1>(tup2));
functor(boost::fusion::at_c<2>(tup2));
}
return result;
C++11 不包含 Boost.Fusion
类。Boost.Fusion
的所有方法都非常有效。它们尽可能在编译时完成工作,并有一些非常先进的优化。
参见
-
拆分元组 配方将提供更多关于
Boost.Fusion
真正强大功能的信息。 -
Boost.Fusion
的官方文档包含一些有趣的示例和完整的参考,可以在www.boost.org/doc/libs/1_53_0/libs/fusion/doc/html/index.html
找到。
拆分元组
这个配方将展示 Boost.Fusion
库能力的一小部分。我们将把一个元组拆分成两个元组,一个包含算术类型,另一个包含所有其他类型。
准备工作
这个配方需要了解 Boost.MPL
、占位符和 Boost.Tuple
。阅读以下配方,从 第一章,开始编写您的应用程序,将多个值组合成一个 获取有关元组的信息,以及 重新排列函数的参数 获取有关占位符的信息。建议从本章开始阅读。
如何实现...
这可能是本章中最难的配方之一。结果类型将在编译时确定,而那些类型的值将在运行时填充。
-
为了实现这种混合,我们需要以下头文件:
#include <boost/fusion/include/remove_if.hpp> #include <boost/type_traits/is_arithmetic.hpp>
-
现在我们已经准备好创建一个返回非算术类型的函数:
template <class Sequence> typename boost::fusion::result_of::remove_if< const Sequence, boost::is_arithmetic<boost::mpl::_1> >::type get_nonarithmetics(const Sequence& seq) { return boost::fusion::remove_if< boost::is_arithmetic<boost::mpl::_1> >(seq); }
-
以及一个返回算术类型的函数:
template <class Sequence> typename boost::fusion::result_of::remove_if< const Sequence, boost::mpl::not_< boost::is_arithmetic<boost::mpl::_1> > >::type get_arithmetics(const Sequence& seq) { return boost::fusion::remove_if< boost::mpl::not_< boost::is_arithmetic<boost::mpl::_1> > >(seq); }
就这样!现在我们能够执行以下任务:
#include <boost/fusion/include/vector.hpp>
#include <cassert>
#include <boost/fusion/include/at_c.hpp>
int main() {
typedef boost::fusion::vector<
int, boost::blank, boost::blank, float
> tup1_t;
tup1_t tup1(8, boost::blank(), boost::blank(), 0.0);
boost::fusion::vector<boost::blank, boost::blank> res_na
= get_nonarithmetics(tup1);
boost::fusion::vector<int, float> res_a = get_arithmetics(tup1);
assert(boost::fusion::at_c<0>(res_a) == 8);
}
它是如何工作的...
Boost.Fusion
的理念是编译器在编译时知道结构布局,并且编译时编译器知道的所有内容,我们都可以同时更改。Boost.Fusion
允许我们修改不同的序列,添加和删除字段,以及更改字段类型。这就是我们在第 2 步和第 3 步所做的事情;我们从元组中移除了非必需的字段。
现在,让我们非常仔细地看看 get_arithmetics
。首先,它的结果类型是通过以下构造推导出来的:
typename boost::fusion::result_of::remove_if<
const Sequence,
boost::is_arithmetic<boost::mpl::_1>
>::type
这对我们来说应该是熟悉的。我们在本章的 在编译时获取函数的结果类型 配方中看到了类似的东西。Boost.MPL
占位符 boost::mpl::_1
也应该是熟悉的。
现在让我们进入函数内部,我们将看到以下代码:
return boost::fusion::remove_if<
boost::is_arithmetic<boost::mpl::_1>
>(seq);
请记住,编译器在编译时知道 seq
的所有类型。这意味着 Boost.Fusion
可以将元函数应用于 seq
的不同元素,并为它们获取元函数的结果。这也意味着 Boost.Fusion
将能够从旧结构复制所需的字段到新结构中。
注意
然而,Boost.Fusion
尽可能避免复制字段。
第 3 步中的代码与第 2 步中的代码非常相似,但它有一个取反的谓词来移除非必需的类型。
我们的功能可以与 Boost.Fusion
支持的任何类型一起使用,而不仅仅是 boost::fusion::vector
。
还有更多...
您可以使用 Boost.MPL
函数处理 Boost.Fusion
容器。您只需包含 #include <boost/fusion/include/mpl.hpp>
即可:
#include <boost/fusion/include/mpl.hpp>
#include <boost/mpl/transform.hpp>
#include <boost/type_traits/remove_const.hpp>
template <class Sequence>
struct make_nonconst: boost::mpl::transform<
Sequence,
boost::remove_const<boost::mpl::_1>
> {};
typedef boost::fusion::vector<
const int, const boost::blank, boost::blank
> type1;
typedef make_nonconst<type1>::type nc_type;
BOOST_STATIC_ASSERT((boost::is_same<
boost::fusion::result_of::value_at_c<nc_type, 0>::type,
int
>::value));
注意
我们使用了 boost::fusion::result_of::value_at_c
而不是 boost::fusion::result_of::at_c
,因为 boost::fusion::result_of::at_c
返回在 boost::fusion::at_c
调用中将用作返回类型的精确类型,这是一个引用。boost::fusion::result_of::value_at_c
返回不带引用的类型。
Boost.Fusion
和 Boost.MPL
库不是 C++11 的一部分。Boost.Fusion
非常快。它有许多优化。您与之一起使用的所有元函数都将被编译时评估。
值得注意的是,我们只看到了 Boost.Fusion
能力的一小部分。关于它可能可以写一本书。
参见
-
关于
Boost.Fusion
的良好教程和完整文档可在 Boost 网站上找到www.boost.org/doc/libs/1_53_0/libs/fusion/doc/html/index.html
-
您还可以查看
Boost.MPL
的官方文档www.boost.org/doc/libs/1_53_0/libs/mpl/doc/index.html
第九章. 容器
在本章中,我们将介绍:
-
以超快的方式比较字符串
-
使用无序集合和映射
-
创建一个映射,其中值也是键
-
使用多索引容器
-
获得单链表和内存池的好处
-
使用扁平关联容器
简介
本章专门介绍 Boost 容器及其直接相关的内容。本章提供了有关可以在日常编程中使用、可以使代码运行得更快、使新应用程序的开发更简单的 Boost 类的信息。
容器不仅通过功能不同,而且通过其成员的一些效率(复杂度)也不同。了解复杂度对于编写快速应用程序至关重要。本章不仅向您介绍了一些新的容器,还提供了何时以及何时不要使用特定类型的容器或其方法的建议。
那么,让我们开始吧!
以超快的方式比较字符串
操作字符串是一个常见任务。在这里,我们将看到如何使用一些简单的技巧快速执行字符串比较操作。这个配方是下一个配方的跳板,其中这里描述的技术将被用来实现常数时间复杂度的搜索。
所以,我们需要创建一个能够快速比较字符串相等的类。我们将创建一个模板函数来测量比较的速度:
#include <string>
template <class T>
std::size_t test_default() {
// Constants
const std::size_t ii_max = 20000000;
const std::string s(
"Long long long string that "
"will be used in tests to compare "
"speed of equality comparisons."
);
// Making some data, that will be
// used in comparisons
const T data[] = {
T(s),
T(s + s),
T(s + ". Whooohooo"),
T(std::string(""))
};
const std::size_t data_dimensions = sizeof(data) / sizeof(data[0]);
std::size_t matches = 0u;
for (std::size_t ii = 0; ii < ii_max; ++ii) {
for (std::size_t i = 0; i < data_dimensions; ++i) {
for (std::size_t j = 0; j < data_dimensions; ++j) {
if (data[i] == data[j]) {
++ matches;
}
}
}
}
return matches;
}
准备就绪
这个配方只需要基本的 STL 和 C++知识。
如何做到这一点...
我们将使std::string
成为我们自己的类的公共字段,并将所有比较代码添加到我们的类中,而不需要编写与存储的std::string
一起工作的辅助方法,如下面的步骤所示:
-
为了做到这一点,我们需要以下头文件:
#include <boost/functional/hash.hpp>
-
现在,我们可以创建我们的快速比较类:
struct string_hash_fast { typedef std::size_t comp_type; const comp_type comparison_; const std::string str_; explicit string_hash_fast(const std::string& s) : comparison_( boost::hash<std::string>()(s) ) , str_(s) {} };
-
不要忘记定义相等比较运算符:
inline bool operator == (const string_hash_fast& s1, const string_hash_fast& s2) { return s1.comparison_ == s2.comparison_ && s1.str_ == s2.str_; } inline bool operator != (const string_hash_fast& s1, const string_hash_fast& s2) { return !(s1 == s2); }
-
然后,这就完成了!现在我们可以运行我们的测试,并使用以下代码查看结果:
#include <iostream> int main(int argc, char* argv[]) { if (argc < 2) { assert( test_default<string_hash_fast>() == test_default<std::string>() ); return 0; } switch (argv[1][0]) { case 'h': std::cout << "HASH matched: " << test_default<string_hash_fast>(); break; case 's': std::cout << "STD matched: " << test_default<std::string>(); break; default: assert(false); return -2; } }
它是如何工作的...
字符串比较之所以慢,是因为如果我们要求比较字符串的所有字符,如果字符串长度相等,我们就必须逐个比较这些字符。而不是这样做,我们用整数比较来替换字符串比较。这是通过哈希函数完成的——该函数使字符串具有某种短固定长度的表示。让我们谈谈苹果上的哈希值。想象一下,你有两个带有标签的苹果,如图所示,你希望检查这些苹果是否属于同一品种。比较这些苹果的最简单方法是通过标签来比较它们。否则,你将花费大量时间根据颜色、大小、形状和其他参数来比较苹果。哈希就像一个标签,反映了对象的价值。
那么,让我们一步一步来。
在步骤 1 中,我们包含了包含哈希函数定义的头文件。在步骤 2 中,我们声明了我们的新字符串类,它包含 str_
,这是字符串的原始值,以及 comparison_
,这是计算出的哈希值。注意构造:
boost::hash<std::string>()(s)
在这里,boost::hash<std::string>
是一个结构,一个功能对象,就像 std::negate<>
。这就是为什么我们需要第一个括号——我们构建这个功能对象。第二个括号内包含 s
的括号是对 std::size_t operator()(const std::string& s)
的调用,它将计算哈希值。
现在看看步骤 3,我们定义了 operator==
。看看以下代码:
return s1.comparison_ == s2.comparison_ && s1.str_ == s2.str_;
此外,还要注意表达式的第二部分。哈希操作会丢失信息,这意味着可能存在多个字符串产生完全相同的哈希值。这意味着如果哈希值不匹配,则可以保证字符串不会匹配,否则我们要求使用传统方法比较字符串。
好吧,现在是时候比较数字了。如果我们使用默认的比较方法来测量执行时间,它将给出 819 毫秒;然而,我们的哈希比较几乎快两倍,只需 475 毫秒就能完成。
还有更多...
C++11 提供了哈希功能对象,你可以在 std::
命名空间中的 <functional>
头文件中找到它。你会知道默认的 Boost 哈希实现不会分配额外的内存,也没有虚拟函数。Boost 和 STL 中的哈希既快又可靠。
你还可以为你的自定义类型特化哈希。在 Boost 中,这是通过在自定义类型的命名空间中特化 hash_value
函数来完成的:
// Must be in namespace of string_hash_fast class
inline std::size_t hash_value(const string_hash_fast& v) {
return v.comparison_;
}
这与 STL 的 std::hash
特化不同,在 std::
命名空间中,你需要对 hash<>
结构进行模板特化。
在 Boost 中,哈希被定义为所有基本类型数组(例如 int
、float
、double
和 char
),以及所有 STL 容器,包括 std::array
、std::tuple
和 std::type_index
。一些库也提供了哈希特化,例如,Boost.Variant
可以对任何 boost::variant
类进行哈希。
参见
-
阅读有关使用无序集和映射的菜谱,以了解更多关于哈希函数使用的信息。
-
Boost.Functional/Hash
的官方文档会告诉你如何组合多个哈希并提供更多示例。请阅读www.boost.org/doc/libs/1_53_0/doc/html/hash.html
。
使用无序集和映射
在之前的菜谱中,我们看到了如何通过哈希来优化字符串比较。阅读之后,可能会产生以下疑问:“我们能否创建一个容器来缓存哈希值,以便更快地进行比较?”
答案是肯定的,我们可以做更多。我们可以实现几乎恒定的时间复杂度,用于搜索、插入和删除元素。
准备工作
需要具备 C++ 和 STL 容器的基本知识。阅读之前的食谱也会有所帮助。
如何做到这一点...
这将是所有食谱中最简单的一个:
-
您只需要包含
<boost/unordered_map.hpp>
头文件,如果我们想使用映射,或者包含<boost/unordered_set.hpp>
头文件,如果我们想使用集合。 -
现在,您可以自由地使用
boost::unordered_map
而不是std::map
,以及使用boost::unordered_set
而不是std::set
:#include <boost/unordered_set.hpp> void example() { boost::unordered_set<std::string> strings; strings.insert("This"); strings.insert("is"); strings.insert("an"); strings.insert("example"); assert(strings.find("is") != strings.cend()); }
它是如何工作的...
无序容器存储值并记住每个值的哈希。现在,如果您想在这些容器中查找一个值,它们将计算该值的哈希并搜索容器中的该哈希。找到哈希后,容器将检查找到的值与搜索值之间的相等性。然后,返回值的迭代器或容器的末尾迭代器。
因为容器可以搜索一个常宽整数的哈希值,它可能使用一些仅适用于整数的优化和算法。这些算法保证了常数搜索复杂度 O(1),而传统的 std::set
和 std::map
提供的复杂度更差,为 O(log(N)),其中 N 是容器中元素的数量。这导致了一个情况,即传统 std::set
或 std::map
中的元素越多,其工作速度越慢。然而,无序容器的性能并不依赖于元素数量。
这样的高性能并不是免费的。在无序容器中,值是无序的(您不会感到惊讶,对吧?)。这意味着如果我们将从 begin()
到 end()
输出容器的元素,如下所示:
template <class T>
void output_example() {
T strings;
strings.insert("CZ"); strings.insert("CD");
strings.insert("A"); strings.insert("B");
std::copy(
strings.begin(),
strings.end(),
std::ostream_iterator<std::string>(std::cout, " ")
);
}
对于 std::set
和 boost::unordered_set
,我们将得到以下输出:
boost::unordered_set<std::string> : B A CD CZ
std::set<std::string> : A B CD CZ
那么,性能差异有多大?看看以下输出:
$ TIME="%E" time ./unordered s
STD matched: 20000000
0:31.39
$ TIME="%E" time ./unordered h
HASH matched: 20000000
0:26.93
性能是通过以下代码测量的:
template <class T>
std::size_t test_default() {
// Constants
const std::size_t ii_max = 20000000;
const std::string s("Test string");
T map;
for (std::size_t ii = 0; ii < ii_max; ++ii) {
map[s + boost::lexical_cast<std::string>(ii)] = ii;
}
// Inserting once more
for (std::size_t ii = 0; ii < ii_max; ++ii) {
map[s + boost::lexical_cast<std::string>(ii)] = ii;
}
return map.size();
}
注意,代码中包含大量的字符串构造,因此使用此测试来衡量加速并不完全正确。它在这里是为了表明无序容器通常比有序容器更快。
有时可能会出现需要在使用无序容器中定义用户自定义类型的情况:
struct my_type {
int val1_;
std::string val2_;
};
为了做到这一点,我们需要为该类型编写一个比较运算符:
inline bool operator == (const my_type& v1, const my_type& v2) {
return v1.val1_ == v2.val1_ && v1.val2_ == v2.val2_;}
现在,为该类型特别指定哈希函数。如果类型由多个字段组成,我们通常只需要组合所有参与相等比较的字段的哈希值:
std::size_t hash_value(const my_type& v) {
std::size_t ret = 0u;
boost::hash_combine(ret, v.val1_);
boost::hash_combine(ret, v.val2_);
return ret;
}
注意
强烈推荐使用 boost::hash_combine
函数组合哈希值。
还有更多...
容器的多版本也是可用的:boost::unordered_multiset
在 <boost/unordered_set.hpp>
头文件中定义,而 boost::unordered_multimap
在 <boost/unordered_map.hpp>
头文件中定义。就像在 STL 的情况下,容器多版本能够存储多个相等的键值。
所有的无序容器都允许你指定自己的哈希函数,而不是默认的 boost::hash
。它们还允许你特化自己的相等比较函数,而不是默认的 std::equal_to
。
C++11 包含了所有来自 Boost 的无序容器。你可以在头文件中找到它们:<unordered_set>
和 <unordered_map>
,在 std::
命名空间中,而不是 boost::
。Boost 和 STL 版本具有相同的性能,并且必须以相同的方式工作。然而,Boost 的无序容器甚至在 C++03 编译器上也是可用的,并利用了 Boost.Move
的右值引用仿真,因此你可以使用这些容器来处理 C++03 中的移动只类。
C++11 没有提供 hash_combine
函数,因此你需要自己编写:
template <class T>
inline void hash_combine(std::size_t& seed, const T& v)
{
std::hash<T> hasher;
seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2);
}
或者直接使用 boost::hash_combine
。
参考信息
-
关于
Boost.Move
的右值引用仿真的详细信息,请参考第一章中的使用 C++11 移动仿真配方。 -
关于无序容器的更多信息可以在官方网站上找到
www.boost.org/doc/libs/1_53_0/doc/html/unordered.html
-
关于组合哈希和计算范围哈希的更多信息,请访问
www.boost.org/doc/libs/1_53_0/do
c/html/hash.html
创建一个值也是键的映射
每年有几次,我们需要一种可以存储和索引一对值的东西。此外,我们需要使用第二个值来获取对的第一个部分,使用第一个值来获取第二个部分。困惑吗?让我给你举一个例子。我们正在创建一个词汇类,当用户将其值放入其中时,该类必须返回标识符;当用户将其标识符放入其中时,该类必须返回值。
为了更实用,用户将输入登录名到我们的词汇中,并希望获取一个人的唯一标识符。他们还希望使用标识符获取所有人员的姓名。
让我们看看如何使用 Boost 来实现它。
准备工作
为了完成这个配方,需要基本的 STL 和模板知识。
如何做到这一点...
这个配方是关于 Boost.Bimap
库的能力。让我们看看如何使用它来实现这个任务:
-
我们需要以下包含:
#include <boost/bimap.hpp> #include <boost/bimap/multiset_of.hpp>
-
现在我们已经准备好创建我们的词汇结构:
typedef boost::bimap< std::string, boost::bimaps::multiset_of<std::size_t> > name_id_type; name_id_type name_id;
-
可以使用以下语法来填充:
// Inserting keys <-> values name_id.insert(name_id_type::value_type( "John Snow", 1 )); name_id.insert(name_id_type::value_type( "Vasya Pupkin", 2 )); name_id.insert(name_id_type::value_type( "Antony Polukhin", 3 )); // Same person as "Antony Polukhin" name_id.insert(name_id_type::value_type( "Anton Polukhin", 3 ));
-
我们可以像处理映射的左侧一样处理双向映射的左侧:
std::cout << "Left:\n"; typedef name_id_type::left_const_iterator left_const_iterator; for (left_const_iterator it = name_id.left.begin(), iend = name_id.left.end(); it!= iend; ++it) { std::cout << it->first << " <=> " << it->second << '\n'; }
-
双向映射的右侧几乎与左侧相同:
std::cout << "\nRight:\n"; typedef name_id_type::right_const_iterator right_const_iterator; for (right_const_iterator it = name_id.right.begin(), iend = name_id.right.end(); it!= iend; ++it) { std::cout << it->first << " <=> " << it->second << '\n'; }
-
我们还需要确保这个人在词汇中存在:
assert( name_id.find(name_id_type::value_type( "Anton Polukhin", 3 )) != name_id.end() );
-
那就是全部了。现在,如果我们把所有的代码(除了包含)放在
int main()
中,我们会得到以下输出:Left: Anton Polukhin <=> 3 Antony Polukhin <=> 3 John Snow <=> 1 Vasya Pupkin <=> 2 Right: 1 <=> John Snow 2 <=> Vasya Pupkin 3 <=> Antony Polukhin 3 <=> Anton Polukhin
它是如何工作的...
在步骤 2 中,我们定义了 bimap
类型:
typedef boost::bimap<
std::string,
boost::bimaps::multiset_of<std::size_t>
> name_id_type;
第一个模板参数表示第一个键必须是 std::string
类型,并且应该像 std::set
一样工作。第二个模板参数表示第二个键必须是 std::size_t
类型。多个第一个键可以有一个单一的第二个键值,就像在 std::multimap
中一样。
我们可以使用 boost::bimaps::
命名空间中的类来指定 bimap
的底层行为。我们可以将哈希映射作为第一个键的底层类型:
#include <boost/bimap/unordered_set_of.hpp>
#include <boost/bimap/unordered_multiset_of.hpp>
typedef boost::bimap<
boost::bimaps::unordered_set_of<std::string>,
boost::bimaps::unordered_multiset_of<std::size_t>
> hash_name_id_type;
当我们没有指定键的行为,只是指定其类型时,Boost.Bimap
使用 boost::bimaps::set_of
作为默认行为。就像在我们的例子中,我们可以尝试使用 STL 表达以下代码:
#include <boost/bimap/set_of.hpp>
typedef boost::bimap<
boost::bimaps::set_of<std::string>,
boost::bimaps::multiset_of<std::size_t>
> name_id_type;
使用 STL,它看起来像以下两个变量的组合:
// name_id.left
std::map<std::string, std::size_t> key1;
// name_id.right
std::multimap<std::size_t, std::string> key2;
如前述注释所示,在步骤 4 中调用 name_id.left
将返回一个类似于 std::map<std::string, std::size_t>
接口的引用。在步骤 5 中从 name_id.right
调用将返回一个类似于 std::multimap<std::size_t, std::string>
接口的对象。
在步骤 6 中,我们处理整个 bimap
,搜索键对,并确保它们在容器中。
还有更多...
不幸的是,C++11 没有与 Boost.Bimap
类似的东西。这里还有一些坏消息:Boost.Bimap
不支持右值引用,并且在某些编译器上,将显示大量警告。请参考您的编译器文档以获取有关抑制特定警告的信息。
好消息是,Boost.Bimap
通常比两个 STL 容器使用更少的内存,并且搜索速度与 STL 容器一样快。它内部没有虚函数调用,但确实使用了动态分配。
参见
-
下一个菜谱,使用多索引容器,将为您提供更多关于多索引以及可以替代
Boost.Bimap
的 Boost 库的信息。 -
有关
bimap
的更多示例和信息,请阅读官方文档,www.boost.org/doc/libs/1_53_0/libs/bimap/doc/html/index.html
使用多索引容器
在之前的菜谱中,我们创建了一些词汇,当我们需要处理成对的内容时很有用。但是,如果我们需要更高级的索引呢?让我们编写一个索引人员的程序:
struct person {
std::size_t id_;
std::string name_;
unsigned int height_;
unsigned int weight_;
person(std::size_t id, const std::string& name, unsigned int height, unsigned int weight)
: id_(id)
, name_(name)
, height_(height)
, weight_(weight)
{}
};
inline bool operator < (const person& p1, const person& p2) {
return p1.name_ < p2.name_;
}
我们将需要很多索引;例如,按名称、ID、身高和体重。
准备工作
需要基本了解 STL 容器和无序映射。
如何做到这一点...
所有索引都可以由单个 Boost.Multiindex
容器构建和管理。
-
为了做到这一点,我们需要很多包含:
#include <boost/multi_index_container.hpp> #include <boost/multi_index/ordered_index.hpp> #include <boost/multi_index/hashed_index.hpp> #include <boost/multi_index/identity.hpp> #include <boost/multi_index/member.hpp>
-
最困难的部分是构建多索引类型:
typedef boost::multi_index::multi_index_container< person, boost::multi_index::indexed_by< // names are unique boost::multi_index::ordered_unique< boost::multi_index::identity<person> >, // IDs are not unique, but we do not need then //ordered boost::multi_index::hashed_non_unique< boost::multi_index::member< person, std::size_t, &person::id_ > >, // Height may not be unique, but must be sorted boost::multi_index::ordered_non_unique< boost::multi_index::member< person, unsigned int, &person::height_ > >, // Weight may not be unique, but must be sorted boost::multi_index::ordered_non_unique< boost::multi_index::member< person, unsigned int, &person::weight_ > > > // closing for `boost::multi_index::indexed_by< > indexes_t;
-
现在我们可以将值插入到我们的多索引中:
indexes_t persons; // Inserting values persons.insert(person(1, "John Snow", 185, 80)); persons.insert(person(2, "Vasya Pupkin", 165, 60)); persons.insert(person(3, "Antony Polukhin", 183, 70)); // Same person as "Antony Polukhin" persons.insert(person(3, "Anton Polukhin", 182, 70));
-
让我们构建一个用于打印索引内容的函数:
template <std::size_t IndexNo, class Indexes> void print(const Indexes& persons) { std::cout << IndexNo << ":\n"; typedef typename Indexes::template nth_index< IndexNo >::type::const_iterator const_iterator_t; for (const_iterator_t it = persons.template get<IndexNo>().begin(), iend = persons.template get<IndexNo>().end(); it != iend; ++it) { const person& v = *it; std::cout << v.name_ << ", " << v.id_ << ", " << v.height_ << ", " << v.weight_ << '\n' ; } std::cout << '\n'; }
-
按如下方式打印所有索引:
print<0>(persons); print<1>(persons); print<2>(persons); print<3>(persons);
-
之前菜谱中的某些代码也可以使用:
assert(persons.get<1>().find(2)->name_ == "Vasya Pupkin"); assert( persons.find(person( 77, "Anton Polukhin", 0, 0 )) != persons.end() ); // Won' compile //assert(persons.get<0>().find("John Snow")->id_ == 1);
-
现在如果我们运行我们的示例,它将输出索引的内容:
0: Anton Polukhin, 3, 182, 70 Antony Polukhin, 3, 183, 70 John Snow, 1, 185, 80 Vasya Pupkin, 2, 165, 60 1: John Snow, 1, 185, 80 Vasya Pupkin, 2, 165, 60 Anton Polukhin, 3, 182, 70 Antony Polukhin, 3, 183, 70 2: Vasya Pupkin, 2, 165, 60 Anton Polukhin, 3, 182, 70 Antony Polukhin, 3, 183, 70 John Snow, 1, 185, 80 3: Vasya Pupkin, 2, 165, 60 Antony Polukhin, 3, 183, 70 Anton Polukhin, 3, 182, 70 John Snow, 1, 185, 80
它是如何工作的...
这里最困难的部分是使用 boost::multi_index::multi_index_container
构造一个多索引类型。第一个模板参数是我们将要索引的类。在我们的例子中,它是 person
。第二个参数是类型 boost::multi_index::indexed_by
,所有索引都必须描述为该类的模板参数。
现在,让我们看看第一个索引描述:
boost::multi_index::ordered_unique<
boost::multi_index::identity<person>
>
boost::multi_index::ordered_unique
类的使用意味着索引必须像 std::set
一样工作,并且具有所有其成员。boost::multi_index::identity<person>
类意味着索引将使用 person
类的 operator <
进行排序。
下一个表格显示了 Boost.MultiIndex
类型与 STL 容器之间的关系:
The Boost.MultiIndex types | STL containers |
---|---|
boost::multi_index::ordered_unique |
std::set |
boost::multi_index::ordered_non_unique |
std::multiset |
boost::multi_index::hashed_unique |
std::unordered_set |
boost::multi_index::hashed_non_unique |
std::unordered_multiset |
boost::multi_index::sequenced |
std::list |
让我们看看第二个索引:
boost::multi_index::hashed_non_unique<
boost::multi_index::member<
person, std::size_t, &person::id_
>
>
boost::multi_index::hashed_non_unique
类型意味着索引将像 std::set
一样工作,而 boost::multi_index::member<person, std::size_t, &person::id_>
意味着索引将仅对人的结构体中的单个成员字段应用哈希函数,即 person::id_
。
现在剩余的索引不会造成麻烦,因此让我们看看在打印函数中使用索引的方式。获取特定索引的迭代器类型是通过以下代码完成的:
typedef typename Indexes::template nth_index<
IndexNo
>::type::const_iterator const_iterator_t;
这看起来稍微有些复杂,因为 Indexes
是一个模板参数。如果我们可以在这个 indexes_t
的作用域中编写此代码,示例将更简单:
typedef indexes_t::nth_index<0>::type::const_iterator const_iterator_t;
nth_index
成员元函数接受一个基于零的索引号来使用。在我们的例子中,索引 1 是 ID 的索引,索引 2 是高度的索引,以此类推。
现在,让我们看看如何使用 const_iterator_t
:
for (const_iterator_t it = persons.template get<IndexNo>().begin(),
iend = persons.template get<IndexNo>().end();
it != iend;
++it)
{
const person& v = *it;
// ...
这也可以通过在作用域中简化 indexes_t
:
for (const_iterator_t it = persons.get<0>().begin(),
iend = persons.get<0>().end();
it != iend;
++it)
{
const person& v = *it;
// ...
函数 get<indexNo>()
返回索引。我们可以几乎像使用 STL 容器一样使用那个索引。
还有更多...
C++11 没有多个索引库。Boost.MultiIndex
库是一个快速库,不使用虚拟函数。Boost.MultiIndex
的官方文档包含了性能和内存使用度量,显示在大多数情况下,这个库使用的内存比基于 STL 的手写代码少。不幸的是,boost::multi_index::multi_index_container
不支持 C++11 特性,也没有使用 Boost.Move
的右值引用模拟。
参考信息
Boost.MultiIndex
的官方文档包含教程、性能度量、示例以及其他Boost.Multiindex
库的有用功能描述。请参阅www.boost.org/doc/libs/1_53_0/libs/multi_index/doc/index.html
。
获取单链表和内存池的好处
现在,当我们需要非关联和非有序容器时,我们通常使用 std::vector
。这在 C++ Coding Standards 一书中由 Andrei Alexandrescu 和 Herb Sutter 推荐,甚至那些没有读过这本书的用户通常也会使用 std::vector
。为什么?因为 std::list
比较慢,并且使用的资源比 std::vector
多得多。std::deque
容器非常接近 std::vector
,但它存储的值不是连续的。
一切都很好,直到我们不需要容器;然而,如果我们需要容器,删除和插入元素不会使迭代器失效。然后我们被迫选择较慢的 std::list
。
但是等等,Boost 对于这种情况有一个很好的解决方案!
准备工作
为了理解介绍部分,需要具备对 STL 容器的良好了解。之后,只需要对 C++ 和 STL 容器有基本了解即可。
如何做到这一点...
在这个菜谱中,我们将同时使用两个 Boost 库:Boost.Pool
和来自 Boost.Container
的单链表。
-
我们需要以下头文件:
#include <boost/pool/pool_alloc.hpp> #include <boost/container/slist.hpp>
-
现在我们需要描述我们的列表类型。这可以通过以下代码实现:
typedef boost::fast_pool_allocator<int> allocator_t; typedef boost::container::slist<int, allocator_t> slist_t;
-
我们可以像使用
std::list
一样使用我们的单链表。看看用于测量两种列表类型速度的函数:template <class ListT> void test_lists() { typedef ListT list_t; // Inserting 1000000 zeros list_t list(1000000, 0); for (int i = 0; i < 1000; ++i) { list.insert(list.begin(), i); } // Searching for some value typedef typename list_t::iterator iterator; iterator it = std::find(list.begin(), list.end(), 777); assert(it != list.end()); // Erasing some values for (int i = 0; i < 100; ++i) { list.pop_front(); } // Iterator still valid and points to same value assert(it != list.end()); assert(*it == 777); // Inserting more values for (int i = -100; i < 10; ++i) { list.insert(list.begin(), i); } // Iterator still valid and points to same value assert(it != list.end()); assert(*it == 777); list_specific(list, it); }
-
每种列表类型特有的功能被移动到
list_specific
函数中:void list_specific(slist_t& list, slist_t::iterator it) { typedef slist_t::iterator iterator; // Erasing element 776 assert( *(++iterator(it)) == 776); assert(*it == 777); list.erase_after(it); assert(*it == 777); assert( *(++iterator(it)) == 775); // Freeing memory boost::singleton_pool< boost::pool_allocator_tag, sizeof(int) >::release_memory(); } #include <list> typedef std::list<int> stdlist_t; void list_specific(stdlist_t& list, stdlist_t::iterator it) { typedef stdlist_t::iterator iterator; // Erasing element 776 ++it; assert( *it == 776); it = list.erase(it); assert(*it == 775); }
它是如何工作的...
当我们使用 std::list
时,我们可能会注意到速度变慢,因为列表的每个节点都需要单独的分配。这意味着通常当我们向 std::list
插入 10 个元素时,容器会调用 new 10 次。
正是因为这个原因,我们使用了来自 Boost.Pool
的 boost::fast_pool_allocator<int>
。这个分配器试图分配更大的内存块,这样在稍后的阶段,多个节点可以构建而无需调用分配新的内存。
Boost.Pool
库有一个缺点——它使用内存来满足内部需求。通常,每个元素都会使用额外的 sizeof
指针。为了解决这个问题,我们使用了来自 Boost.Containers
的单链表。
boost::container::slist
类更紧凑,但它的迭代器只能向前迭代。对于熟悉 STL 容器的读者来说,步骤 3 将是微不足道的,所以我们转到步骤 4 来查看一些 boost::container::slist
特定的功能。由于单链表迭代器只能向前迭代,传统的插入和删除算法将需要线性时间 O(N)。那是因为当我们删除或插入时,前一个元素必须被修改以指向列表的新元素。为了解决这个问题,单链表有 erase_after
和 insert_after
方法,它们可以在常数时间 O(1) 内工作。这些方法在迭代器的当前位置之后插入或删除元素。
注意
然而,在单链表的开始处删除和插入值并没有太大的区别。
仔细看看以下代码:
boost::singleton_pool<
boost::pool_allocator_tag,
sizeof(int)
>::release_memory();
这是因为 boost::fast_pool_allocator
不会释放内存,所以我们必须手动完成。来自 第三章 的 在作用域退出时做某事 配方将有助于释放 Boost.Pool
。
让我们看看执行结果以查看差异:
$TIME="Runtime=%E RAM=%MKB" time ./slist_and_pool l
std::list: Runtime=0:00.05 RAM=32440KB
$ TIME="Runtime=%E RAM=%MKB" time ./slist_and_pool s
slist_t: Runtime=0:00.02 RAM=17564KB
如我们所见,slist_t
使用了内存的一半,并且比 std::list
类快两倍。
还有更多...
C++11 有 std::forward_list
,它与 boost::containers::slist
非常相似。它也有 *_after
方法,但没有 size()
方法。它们的性能相同,并且它们都没有虚拟函数,所以这些容器既快又可靠。然而,Boost 版本也可以在 C++03 编译器上使用,并且甚至通过 Boost.Move
提供对右值引用仿真的支持。
池不是 C++11 的一部分。请使用 Boost 的版本;它既快又不使用虚拟函数。
注意
为什么 boost::fast_pool_allocator
不能自己释放内存?那是因为 C++03 没有状态分配器,所以容器不会复制和存储分配器。这使得无法实现一个可以自己释放内存的 boost::fast_pool_allocator
函数。
参见
-
Boost.Pool
的官方文档包含了更多关于内存池的示例和类。请在此处阅读:www.boost.org/doc/libs/1_53_0/libs/pool/doc/html/index.html
。 -
使用平面关联容器 的配方将向您介绍
Boost.Container
中的一些更多类。您也可以阅读Boost.Container
的官方文档,自己学习该库,或在其类中获取完整的参考文档:www.boost.org/doc/libs/1_53_0/doc/html/container.html
。 -
有关为什么可能需要状态分配器的信息,请参阅
www.boost.org/doc/libs/1_53_0/doc/html/interprocess/allocators_containers.html#interprocess.allocators_containers.allocator_introduction
。 -
向量与列表,以及其他来自 C++ 编程语言发明者 Bjarne Stroustrup 的有趣话题,可以在
channel9.msdn.com/Events/GoingNative/GoingNative-2012/Keynote-Bjarne-Stroustrup
-Cpp11-Style 找到。
使用平面关联容器
在阅读了之前的菜谱后,一些读者可能会开始到处使用快速池分配器;特别是,对于 std::set
和 std::map
。好吧,我不会阻止你这样做,但至少让我们看看一个替代方案:平面关联容器。这些容器是在传统的向量容器之上实现的,并按顺序存储值。
准备就绪
需要掌握 STL 关联容器的基本知识。
如何做到这一点...
平面容器是 Boost.Container
库的一部分。我们已经在之前的菜谱中看到了如何使用其中的一些容器。在这个菜谱中,我们将使用一个 flat_set
关联容器:
-
我们只需要包含一个头文件:
#include <boost/container/flat_set.hpp>
-
之后,我们可以自由地构建平面容器:
boost::container::flat_set<int> set;
-
为元素预留空间:
set.reserve(4096);
-
填充容器:
for (int i = 0; i < 4000; ++i) { set.insert(i); }
-
现在,我们可以像使用
std::set
一样使用它:// 5.1 assert(set.lower_bound(500) - set.lower_bound(100) == 400); // 5.2 set.erase(0); // 5.3 set.erase(5000); // 5.4 assert(std::lower_bound(set.cbegin(), set.cend(), 900000) == set.cend()); // 5.5 assert( set.lower_bound(100) + 400 == set.find(500) );
它是如何工作的...
步骤 1 和 2 很简单,但步骤 3 需要特别注意。这是在使用平面关联容器和 std::vector
时最重要的步骤之一。
boost::container::flat_set
类将它的值有序地存储在向量中,这意味着任何元素的插入或删除都花费线性时间 O(N),就像在 std::vector
的情况下。这是一个必要的恶。但为此,我们几乎每个元素节省了三倍多的内存使用,更友好的处理器缓存存储,以及随机访问迭代器。看看第 5 步,5.1
,在那里我们获取由 lower_bound
成员函数调用返回的两个迭代器之间的距离。使用平面集合获取距离需要常数时间 O(1),而同样操作 std::set
的迭代器需要线性时间 O(N)。在 5.1
的情况下,使用 std::set
获取距离会比获取平面集合容器的距离慢 400 倍。
回到第 3 步。如果不预留内存,插入元素有时会变慢且内存效率较低。std::vector
类分配所需的内存块,并在该块上就地构造元素。当我们不预留内存而插入一些元素时,可能会出现预分配的内存块上没有剩余空间的情况,因此std::vector
将分配两倍于之前分配的内存块。之后,std::vector
将复制或移动第一个块中的元素到第二个块,删除第一个块中的元素,并释放第一个块的内存。只有在那时,插入才会发生。这种复制和释放内存可能会在插入过程中多次发生,从而大大降低速度。
备注
如果你知道std::vector
或任何扁平容器必须存储的元素数量,在插入之前为这些元素预留空间。这个规则没有例外!
第 4 步很简单,我们在这里插入元素。请注意,我们正在插入有序元素。这不是必需的,但推荐这样做以加快插入速度。在std::vector
的末尾插入元素比在中间或开头插入要便宜得多。
在第 5 步中,5.2
和5.3
没有太大区别,除了它们的执行速度。删除元素的规定与插入元素的规定几乎相同,所以请参阅前面的段落以获取解释。
备注
也许我在告诉你关于容器的一些简单事情,但我看到一些非常流行的产品使用了 C++11 的特性,有大量的优化,以及 STL 容器的糟糕使用,特别是std::vector
。
在第 5 步中,5.4
展示了std::lower_bound
函数使用boost::container::flat_set
比使用std::set
要快,因为具有随机访问迭代器。
在第 5 步中,5.5
也展示了随机访问迭代器的优势。请注意,我们在这里没有使用std::find
函数。这是因为该函数需要线性时间 O(N),而成员find
函数需要对数时间 O(log(N))。
还有更多...
我们应该在什么时候使用扁平容器,什么时候使用常规容器?嗯,这取决于你,但这里有一个从Boost.Container
官方文档中摘录的差异列表,这将帮助你做出决定:
-
比标准关联容器查找更快
-
比标准关联容器迭代得更快
-
对于小对象(如果使用
shrink_to_fit
则对于大对象)内存消耗更少 -
改善缓存性能(数据存储在连续内存中)
-
非稳定迭代器(在插入和删除元素时迭代器会被无效化)
-
无法存储不可复制和不可移动的值类型
-
比标准关联容器具有较弱的异常安全性(复制/移动构造函数在删除和插入时移动值可能会抛出异常)
-
比标准关联容器插入和删除更慢(特别是对于不可移动的类型)
不幸的是,C++11 没有扁平容器。Boost 中的扁平容器速度快,有很多优化,并且不使用虚函数。Boost.Containers
中的类通过Boost.Move
支持 rvalue 引用的模拟,因此您可以在 C++03 编译器上自由使用它们。
参见
-
有关
Boost.Container
的更多信息,请参考获取单链表和内存池的好处配方。 -
在第一章中,使用 C++11 移动模拟的配方,将向您介绍如何在 C++03 兼容的编译器上实现仿值引用的基础知识。
-
Boost.Container
的官方文档包含了关于Boost.Container
的大量有用信息以及每个类的完整参考。请访问www.boost.org/doc/libs/1_53_0/doc/html/container.html
了解更多信息。
第十章. 收集平台和编译器信息
在本章中,我们将涵盖:
-
检测 int128 支持
-
检测 RTTI 支持
-
使用 C++11 extern 模板加速编译
-
使用更简单的方法编写元函数
-
在 C++11 中减少用户定义类型(UDT)的代码大小并提高性能
-
导出和导入函数和类的可移植方式
-
检测 Boost 版本和获取最新功能
简介
不同的项目和公司有不同的编码要求。其中一些禁止异常或 RTTI,而另一些禁止 C++11。如果您愿意编写可移植的代码,这些代码可以用于广泛的工程,那么这一章就是为您准备的。
想要使您的代码尽可能快并使用最新的 C++功能?您肯定会需要一个用于检测编译器功能的工具。
一些编译器具有独特的功能,这些功能可能会极大地简化您的生活。如果您针对单个编译器,您可以节省许多小时并使用这些功能。无需从头开始实现它们的类似功能!
本章致力于不同类型的辅助宏,用于检测编译器、平台和 Boost 功能。这些宏在 Boost 库中广泛使用,对于编写能够与任何编译器标志一起工作的可移植代码至关重要。
检测 int128 支持
一些编译器支持扩展算术类型,例如 128 位浮点数或整数。让我们快速了解一下如何使用 Boost 来使用它们。我们将创建一个接受三个参数并返回这些方法乘积的方法。
准备工作
只需要具备基本的 C++知识。
如何做到这一点...
我们需要什么来处理 128 位整数?显示它们可用的宏以及一些跨平台的 typedef 来具有可移植的类型名称。
-
我们只需要一个头文件:
#include <boost/config.hpp>
-
现在我们需要检测 int128 支持:
#ifdef BOOST_HAS_INT128
-
添加一些 typedef 并按以下方式实现方法:
typedef boost::int128_type int_t; typedef boost::uint128_type uint_t; inline int_t mul(int_t v1, int_t v2, int_t v3) { return v1 * v2 * v3; }
-
对于不支持 int128 类型的编译器,我们可能需要支持 int64 类型:
#else // BOOST_NO_LONG_LONG #ifdef BOOST_NO_LONG_LONG #error "This code requires at least int64_t support" #endif
-
现在我们需要为不支持 int128 的编译器提供使用 int64 的实现:
struct int_t { boost::long_long_type hi, lo; }; struct uint_t { boost::ulong_long_type hi, lo; }; inline int_t mul(int_t v1, int_t v2, int_t v3) { // Some hand written math // ... } #endif // BOOST_NO_LONG_LONG
它是如何工作的...
头文件 <boost/config.hpp>
包含了许多宏来描述编译器和平台功能。在这个例子中,我们使用了 BOOST_HAS_INT128
来检测 128 位整数的支持,以及 BOOST_NO_LONG_LONG
来检测 64 位整数的支持。
如我们从示例中看到的那样,Boost 为 64 位有符号和无符号整数提供了 typedef:
boost::long_long_type
boost::ulong_long_type
它也提供了 128 位有符号和无符号整数的 typedef:
boost::int128_type
boost::uint128_type
更多...
C++11 通过 long long int
和 unsigned long long int
内置类型支持 64 位类型。不幸的是,并非所有编译器都支持 C++11,所以 BOOST_NO_LONG_LONG
对您将很有用。128 位整数不是 C++11 的一部分,因此 Boost 的 typedef 和宏是编写可移植代码的唯一方法。
参见
-
有关
Boost.Config
的更多信息,请参阅食谱 检测 RTTI 支持。 -
有关其能力的更多信息,请阅读
Boost.Config
的官方文档,链接为www.boost.org/doc/libs/1_53_0/libs/config/doc/html/index.html
。 -
Boost 中有一个库允许构建无限精度的类型。请查看
Boost.Multiprecision
库,链接为www.boost.org/doc/libs/1_53_0/libs/multiprecision/doc/html/index.html
。
检测 RTTI 支持
一些公司和库对他们的 C++代码有特定的要求,例如在运行时类型信息(RTTI)禁用的情况下成功编译。在这个小食谱中,我们将看看我们如何检测禁用的 RTTI,如何存储类型信息,以及如何在运行时比较类型,即使没有typeid
。
准备工作
需要基本了解 C++ RTTI 的使用才能完成这个食谱。
如何做...
检测禁用的 RTTI、存储类型信息以及在运行时比较类型是 Boost 库中广泛使用的技巧。例如,Boost.Exception
和Boost.Function
。
-
要做到这一点,我们首先需要包含以下头文件:
#include <boost/config.hpp>
-
让我们首先看看 RTTI 已启用且 C++11
std::type_index
类可用的情况:#if !defined(BOOST_NO_RTTI) \ && !defined(BOOST_NO_CXX11_HDR_TYPEINDEX) #include <typeindex> using std::type_index; template <class T> type_index type_id() { return typeid(T); }
-
否则,我们需要自己构建自己的
type_index
类:#else #include <cstring> struct type_index { const char* name_; explicit type_index(const char* name) : name_(name) {} }; inline bool operator == (const type_index& v1, const type_index& v2) { return !std::strcmp(v1.name_, v2.name_); } inline bool operator != (const type_index& v1, const type_index& v2) { // '!!' to supress warnings return !!std::strcmp(v1.name_, v2.name_); }
-
最后一步是定义
type_id
函数:#include <boost/current_function.hpp> template <class T> inline type_index type_id() { return type_index(BOOST_CURRENT_FUNCTION); } #endif
-
现在我们可以比较类型了:
assert(type_id<unsigned int>() == type_id<unsigned>()); assert(type_id<double>() != type_id<long double>());
它是如何工作的...
如果 RTTI 被禁用,则将定义BOOST_NO_RTTI
宏,如果编译器没有<typeindex>
头文件和std::type_index
类,则将定义BOOST_NO_CXX11_HDR_TYPEINDEX
宏。
上一个部分步骤 3 中手写的type_index
结构只包含指向某些字符串的指针;这里没有什么真正有趣的内容。
看一下BOOST_CURRENT_FUNCTION
宏。它返回当前函数的完整名称,包括模板参数、参数和返回类型。例如,type_id<double>()
将表示如下:
type_index type_id() [with T = double]
因此,对于任何其他类型,BOOST_CURRENT_FUNCTION
将返回不同的字符串,这就是为什么示例中的type_index
变量不会与它相等。
更多...
不同的编译器有不同的宏来获取完整的函数名和 RTTI。使用 Boost 的宏是最便携的解决方案。BOOST_CURRENT_FUNCTION
宏在编译时返回名称,因此它意味着最小的运行时惩罚。
参见
-
阅读即将到来的食谱,了解更多关于
Boost.Config
的信息 -
浏览到
github.com/apolukhin/type_index
并参考那里的库,该库使用本食谱中的所有技巧来实现type_index
-
在
www.boost.org/doc/libs/1_53_0/libs/config/doc/html/index.html
阅读Boost.Config
的官方文档
使用 C++11 外部模板加速编译
记得你曾经使用过的一些在头文件中声明的复杂模板类的情况吗?这样的类的例子包括boost::variant
、来自Boost.Container
的容器或Boost.Spirit
解析器。当我们使用这样的类或方法时,它们通常在每个使用它们的源文件中单独编译(实例化),并且在链接过程中会丢弃重复项。在某些编译器上,这可能会导致编译速度变慢。
如果有一种方法可以告诉编译器在哪个源文件中实例化它就好了!
准备工作
需要具备模板的基本知识才能完成此食谱。
如何做...
这种方法在现代 C++标准库中广泛用于支持它的编译器。例如,与 GCC 一起提供的 STL 库使用这种技术实例化std::basic_string<char>
和std::basic_fstream<char>
。
-
要自行完成,我们需要包含以下头文件:
#include <boost/config.hpp>
-
我们还需要包含一个包含我们希望减少实例化计数的模板类的头文件:
#include <boost/variant.hpp> #include <boost/blank.hpp> #include <string>
-
以下是为支持 C++11 外部模板的编译器提供的代码:
#ifndef BOOST_NO_CXX11_EXTERN_TEMPLATE extern template class boost::variant< boost::blank, int, std::string, double >; #endif
-
现在,我们需要将以下代码添加到我们希望模板实例化的源文件中:
// Header with 'extern template' #include "header.hpp" #ifndef BOOST_NO_CXX11_EXTERN_TEMPLATE template class boost::variant< boost::blank, int, std::string, double >; #endif
它是如何工作的...
C++11 关键字extern template
只是告诉编译器不要在没有显式请求的情况下实例化模板。
第 4 步中的代码是显式请求在此源文件中实例化模板。
当编译器支持 C++11 外部模板时,定义了BOOST_NO_CXX11_EXTERN_TEMPLATE
宏。
还有更多...
外部模板不会影响你程序的运行时性能,但可以显著减少某些模板类的编译时间。不要过度使用它们;对于小型模板类来说,它们几乎毫无用处。
参见
-
阅读本章的其他食谱,以获取有关
Boost.Config
的更多信息。 -
有关本章未涵盖的宏的官方文档,请参阅
Boost.Config
的文档,网址为www.boost.org/doc/libs/1_53_0/libs/config/doc/html/index.html
使用更简单的方法编写元函数
第四章,“编译时技巧”,和第八章,“元编程”,都是关于元编程的。如果你试图使用那些章节中的技术,你可能已经注意到编写元函数可能需要花费很多时间。因此,在编写可移植实现之前,使用更用户友好的方法,如 C++11 constexpr
进行元函数实验可能是一个好主意。
在这个食谱中,我们将探讨如何检测constexpr
支持。
准备工作
constexpr
函数是可以编译时评估的函数。这就是我们为此食谱需要了解的所有内容。
如何做...
目前,很少有编译器支持 constexpr
功能,因此可能需要一个良好的新编译器来进行实验。让我们看看如何检测编译器对 constexpr
功能的支持:
-
就像本章其他食谱一样,我们从一个以下头文件开始:
#include <boost/config.hpp>
-
现在我们将使用
constexpr
:#if !defined(BOOST_NO_CXX11_CONSTEXPR) \ && !defined(BOOST_NO_CXX11_HDR_ARRAY) template <class T> constexpr int get_size(const T& val) { return val.size() * sizeof(typename T::value_type); }
-
如果缺少 C++11 功能,让我们打印一个错误:
#else #error "This code requires C++11 constexpr and std::array" #endif
-
就这样;现在我们可以自由地编写如下代码:
std::array<short, 5> arr; assert(get_size(arr) == 5 * sizeof(short)); unsigned char data[get_size(arr)];
它是如何工作的...
当 C++11 的 constexpr
可用时,定义了 BOOST_NO_CXX11_CONSTEXPR
宏。
constexpr
关键字告诉编译器,如果该函数的所有输入都是编译时常量,则可以在编译时评估该函数。C++11 对 constexpr
函数能做什么有很多限制。C++14 将移除一些限制。
当 C++11 的 std::array
类和 <array>
头文件可用时,定义了 BOOST_NO_CXX11_HDR_ARRAY
宏。
还有更多...
然而,还有其他可用的和有趣的宏用于 constexpr
,如下所示:
-
BOOST_CONSTEXPR
宏展开为constexpr
或不展开 -
BOOST_CONSTEXPR_OR_CONST
宏展开为constexpr
或const
-
BOOST_STATIC_CONSTEXPR
宏与static BOOST_CONSTEXPR_OR_CONST
相同
使用这些宏,如果可用,可以编写利用 C++11 常量表达式特性的代码:
template <class T, T Value>
struct integral_constant {
BOOST_STATIC_CONSTEXPR T value = Value;
BOOST_CONSTEXPR operator T() const {
return this->value;
}
};
现在,我们可以像以下代码所示使用 integral_constant
:
char array[integral_constant<int, 10>()];
在示例中,BOOST_CONSTEXPR operator T()
将被调用以获取数组大小。
C++11 的常量表达式可以在出错情况下提高编译速度和诊断信息。这是一个很好的特性来使用。
参见
-
有关
constexpr
用法的更多信息,请参阅en.cppreference.com/w/cpp/language/constexpr
-
有关宏的更多信息,请阅读
Boost.Config
的官方文档:www.boost.org/doc/libs/1_53_0/libs/config/doc/html/index.html
在 C++11 中减少用户定义类型(UDTs)的代码大小并提高性能
当在 STL 容器中使用用户定义类型(UDTs)时,C++11 有非常具体的逻辑。如果移动构造函数不抛出异常或者没有复制构造函数,容器将仅使用移动赋值和移动构造。
让我们看看如何确保我们的类型的 move_nothrow
赋值运算符和 move_nothrow
构造函数不会抛出异常。
准备工作
需要具备 C++11 rvalue references 的基本知识才能完成此食谱。了解 STL 容器也将对你大有裨益。
如何做到...
让我们看看如何使用 Boost 来改进我们的 C++ 类。
-
我们需要做的只是用
BOOST_NOEXCEPT
宏标记move_nothrow
赋值运算符和move_nothrow
构造函数:#include <boost/config.hpp> class move_nothrow { // Some class class members go here // ... public: move_nothrow() BOOST_NOEXCEPT {} move_nothrow(move_nothrow&&) BOOST_NOEXCEPT // : members initialization // ... {} move_nothrow& operator=(move_nothrow&&) BOOST_NOEXCEPT { // Implementation // ... return *this; } move_nothrow(const move_nothrow&); move_nothrow& operator=(const move_nothrow&); };
-
现在我们可以直接在 C++11 中使用
std::vector
类,无需任何修改:std::vector<move_nothrow> v(10); v.push_back(move_nothrow());
-
如果我们从移动构造函数中移除
BOOST_NOEXCEPT
,对于 GCC-4.7 及以后的编译器,我们将得到以下错误:/usr/include/c++/4.7/bits/stl_construct.h:77: undefined reference to `move_nothrow::move_nothrow(move_nothrow const&)'
它是如何工作的...
在支持它的编译器上,BOOST_NOEXCEPT
宏展开为 noexcept
。STL 容器使用类型特性来检测构造函数是否抛出异常。类型特性主要基于 noexcept
说明符做出决定。
为什么没有 BOOST_NOEXCEPT
会出错?GCC 的类型特性返回 move_nothrow
抛出的移动构造函数,因此 std::vector
将尝试使用 move_nothrow
的复制构造函数,而这个复制构造函数并未定义。
还有更多...
BOOST_NOEXCEPT
宏无论 noexcept
函数或方法的定义是在单独的源文件中还是不在,都会减少二进制文件的大小。
// In header file
int foo() BOOST_NOEXCEPT;
// In source file
int foo() BOOST_NOEXCEPT {
return 0;
}
这是因为在后一种情况下,编译器知道该函数不会抛出异常,因此不需要生成处理它们的代码。
注意
如果标记为 noexcept
的函数抛出异常,则程序将在不调用构造对象的析构函数的情况下终止。
参考资料还有
-
一份描述为什么移动构造函数允许抛出异常以及容器如何移动对象的文档可在
www.open-std.org/jtc1/sc22/wg21/docs/papers/2010/n3050.html
找到。 -
有关
Boost.Config
的官方文档中提供了更多noexcept
宏在 Boost 中的示例,请参阅www.boost.org/doc/libs/1_53_0/libs/conf
ig/doc/html/index.html。
可移植地导出和导入函数和类的方法
几乎所有现代语言都有创建库的能力,库是一组具有良好定义接口的类和方法。C++ 也不例外。我们有两种类型的库:运行时库(也称为共享或动态加载)和静态库。但在 C++ 中编写库并不是一个简单任务。不同的平台有不同的方法来描述必须从共享库中导出的符号。
让我们看看如何使用 Boost 以可移植的方式管理符号可见性。
准备工作
在此配方中,创建动态和静态库的经验将很有用。
如何操作...
此配方的代码由两部分组成。第一部分是库本身。第二部分是使用该库的代码。这两部分都使用相同的头文件,在该头文件中声明了库方法。使用 Boost 以可移植的方式管理符号可见性简单且可以通过以下步骤完成:
-
在头文件中,我们需要从以下
include
头文件中获取定义:#include <boost/config.hpp>
-
以下代码也必须添加到头文件中:
#if defined(MY_LIBRARY_LINK_DYNAMIC) # if defined(MY_LIBRARY_COMPILATION) # define MY_LIBRARY_API BOOST_SYMBOL_EXPORT # else # define MY_LIBRARY_API BOOST_SYMBOL_IMPORT # endif #else # define MY_LIBRARY_API #endif
-
现在所有声明都必须使用
MY_LIBRARY_API
宏:int MY_LIBRARY_API foo(); class MY_LIBRARY_API bar { public: /* ... */ int meow() const; };
-
异常必须使用
BOOST_SYMBOL_VISIBLE
声明,否则只能在将使用库的代码中使用catch(...)
来捕获:#include <stdexcept> struct BOOST_SYMBOL_VISIBLE bar_exception : public std::exception {};
-
库源文件必须包含头文件:
#define MY_LIBRARY_COMPILATION #include "my_library.hpp"
-
方法的定义也必须在库的源文件中:
int MY_LIBRARY_API foo() { // Implementation // ... return 0; } int bar::meow() const { throw bar_exception(); }
-
现在,我们可以像以下代码所示使用库:
#include "../my_library/my_library.hpp" #include <cassert> int main() { assert(foo() == 0); bar b; try { b.meow(); assert(false); } catch (const bar_exception&) {} }
它是如何工作的...
所有工作都在第 2 步完成。在那里我们定义了宏MY_LIBRARY_API
,它将被应用于我们希望从库中导出的类和方法。在第 2 步中,我们检查MY_LIBRARY_LINK_DYNAMIC
是否已定义;如果没有定义,我们正在构建一个静态库,因此不需要定义MY_LIBRARY_API
。
注意
开发者必须注意MY_LIBRARY_LINK_DYNAMIC
!它不会自动定义。因此,如果我们正在构建动态库,我们需要让我们的构建系统来定义它。
如果定义了MY_LIBRARY_LINK_DYNAMIC
,我们正在构建一个运行时库,这就是解决方案开始的地方。作为开发者,你必须告诉编译器我们现在正在将这些方法导出给用户。用户必须告诉编译器他/她正在从库中导入方法。为了有一个用于库导入和导出的单个头文件,我们使用以下代码:
# if defined(MY_LIBRARY_COMPILATION)
# define MY_LIBRARY_API BOOST_SYMBOL_EXPORT
# else
# define MY_LIBRARY_API BOOST_SYMBOL_IMPORT
# endif
当导出库(或者说,换句话说,编译它)时,我们必须定义MY_LIBRARY_COMPILATION
。这导致MY_LIBRARY_API
被定义为BOOST_SYMBOL_EXPORT
。例如,参见第 5 步,我们在包含my_library.hpp
之前定义了MY_LIBRARY_COMPILATION
。如果未定义MY_LIBRARY_COMPILATION
,则由用户包含头文件,而用户对此宏一无所知。而且,如果头文件由用户包含,则必须从库中导入符号。
必须仅使用BOOST_SYMBOL_VISIBLE
宏来处理那些未导出且用于 RTTI 的类。此类类的例子包括异常和被dynamic_cast
转换的类。
还有更多...
一些编译器默认导出所有符号,但提供标志来禁用此行为。例如,GCC 提供-fvisibility=hidden
标志。强烈建议使用这些标志,因为它会导致二进制文件大小减小,动态库加载更快,以及二进制输入的逻辑结构更好。当导出的符号较少时,一些过程间优化可以表现得更好。
C++11 已经推广了属性,将来可能会被用来提供一种可移植的方式来处理可见性,但在此之前,我们必须使用 Boost 的宏。
参见
-
从头开始阅读本章,以获取更多
Boost.Config
使用的示例 -
考虑阅读
Boost.Config
的官方文档,以获取Boost.Config
宏及其描述的完整列表,请参阅www.boost.org/doc/libs/1_53_0/libs/config/doc/html/index.html
检测 Boost 版本和获取最新功能
Boost 正在积极开发中,因此每个版本都包含新的特性和库。有些人希望有针对不同 Boost 版本的库,并且还想使用新版本的一些特性。
让我们来看看boost::lexical_cast
的变更日志。根据它,Boost 1.53 有一个lexical_cast(const CharType* chars, std::size_t count)
函数重载。我们这个菜谱的任务将是使用该函数重载来处理 Boost 的新版本,并为旧版本处理缺失的函数重载。
准备工作
只需要具备基本的 C++知识和Boost.Lexical
库知识。
如何做...
好吧,我们所需做的只是获取一个 Boost 版本并使用它来编写最优代码。这可以通过以下步骤完成:
-
我们需要包含包含 Boost 版本和
boost::lexical_cast
的头文件:#include <boost/version.hpp> #include <boost/lexical_cast.hpp>
-
如果可用,我们将使用
Boost.LexicalCast
的新特性:#if (BOOST_VERSION >= 105200) int to_int(const char* str, std::size_t length) { return boost::lexical_cast<int>(str, length); }
-
否则,我们首先需要将数据复制到
std::string
中:#else int to_int(const char* str, std::size_t length) { return boost::lexical_cast<int>( std::string(str, length) ); } #endif
-
现在,我们可以使用以下代码:
assert(to_int("10000000", 3) == 100);
它是如何工作的...
BOOST_VERSION
宏包含以以下格式编写的 Boost 版本:一个用于主版本的数字,后面跟着三个用于次版本的数字,然后是两个用于修补级别的数字。例如,Boost 1.46.1 将在BOOST_VERSION
宏中包含104601
这个数字。
因此,在第二步中,我们将检查 Boost 版本,并根据Boost.LexicalCast
的能力选择正确的to_int
函数实现。
更多...
对于大型库来说,拥有一个版本宏是一种常见做法。一些 Boost 库允许你指定要使用的库版本;例如,参见Boost.Thread
及其BOOST_THREAD_VERSION
宏。
参见
-
有关
BOOST_THREAD_VERSION
及其如何影响Boost.Thread
库的更多信息,请参阅第五章中的菜谱创建执行线程,或者阅读www.boost.org/doc/libs/1_53_0/doc/html/thread/changes.html
上的文档。 -
从本章开始阅读,或者考虑阅读
www.boost.org/doc/libs/1_53_0/libs/config/doc/html/index.html
上的官方Boost.Config
文档。
第十一章。与系统一起工作
在本章中,我们将介绍以下内容:
-
列出目录中的文件
-
删除和创建文件和目录
-
快速从一个进程传递到另一个进程的数据
-
同步进程间通信
-
在共享内存中使用指针
-
读取文件的最快方式
-
协程 - 保存状态和推迟执行
简介
每个操作系统都有许多系统调用,以略微不同的方式执行几乎相同的事情。这些调用在性能上有所不同,并且在不同操作系统之间有所不同。Boost 为这些调用提供了可移植和安全的包装器。了解这些包装器对于编写良好的程序至关重要。
本章致力于操作系统的工作。我们在第六章“操作任务”中看到了如何处理网络通信和信号。在本章中,我们将更深入地研究文件系统以及创建和删除文件。我们将了解数据如何在不同的系统进程之间传递,如何以最大速度读取文件,以及如何执行其他技巧。
列出目录中的文件
有 STL 函数和类可以读取和写入文件中的数据。但没有函数可以列出目录中的文件,获取文件类型,或获取文件访问权限。
让我们看看如何使用 Boost 来修复这样的不公正。我们将创建一个程序,列出当前目录中的文件名、写访问权限和文件类型。
准备工作
一些基本的 C++知识就足够使用此配方了。
此配方需要链接到boost_system
和boost_filesystem
库。
如何操作...
此配方和下一个配方是关于用于处理文件系统的可移植包装器:
-
我们需要包含以下两个头文件:
#include <boost/filesystem/operations.hpp> #include <iostream>
-
现在需要指定一个目录:
int main() { boost::filesystem::directory_iterator begin("./");
-
指定目录后,遍历其内容:
boost::filesystem::directory_iterator end; for (; begin != end; ++ begin) {
-
下一步是获取文件信息:
boost::filesystem::file_status fs = boost::filesystem::status(*begin);
-
现在输出文件信息:
switch (fs.type()) { case boost::filesystem::regular_file: std::cout << "FILE "; break; case boost::filesystem::symlink_file: std::cout << "SYMLINK "; break; case boost::filesystem::directory_file: std::cout << "DIRECTORY "; break; default: std::cout << "OTHER "; break; } if (fs.permissions() & boost::filesystem::owner_write) { std::cout << "W "; } else { std::cout << " "; }
-
最后一步将是输出文件名:
std::cout << *begin << '\n'; } /*for*/ } /*main*/
就这样。现在,如果我们运行程序,它将输出类似以下内容:
FILE W "./main.o"
FILE W "./listing_files"
DIRECTORY W "./some_directory"
FILE W "./Makefile"
它是如何工作的...
Boost.Filesystem
的函数和类只是围绕系统特定的函数来处理文件。
注意第 2 步中/
的使用。POSIX 系统使用斜杠来指定路径;默认情况下,Windows 使用反斜杠。然而,Windows 也理解正斜杠,所以./
在所有流行的操作系统上都会工作,它表示“当前目录”。
看看第 3 步,我们正在默认构造boost::filesystem::directory_iterator
类。它就像一个std::istream_iterator
类,在默认构造时充当end
迭代器。
第 4 步有点棘手,不是因为这个函数难以理解,而是因为发生了许多转换。取消引用 begin
迭代器返回 boost::filesystem::directory_entry
,它被隐式转换为 boost::filesystem::path
,用作 boost::filesystem::status
函数的参数。实际上,我们可以做得更好:
boost::filesystem::file_status fs = begin->status();
小贴士
仔细阅读参考文档以避免不必要的隐式转换。
第 5 步很明显,所以我们正在转向第 6 步,在那里再次发生隐式路径转换。一个更好的解决方案将是以下:
std::cout << begin->path() << '\n';
在这里,begin->path()
返回一个对包含在 boost::filesystem::directory_entry
中的 boost::filesystem::path
变量的常量引用。
还有更多...
不幸的是,Boost.Filesystem
不是 C++11 的一部分,但它被提议包含在下一个 C++ 标准中。Boost.Filesystem
目前缺少对右值引用的支持,但仍然是最简单和最可移植的文件系统交互库之一。
参考以下内容
-
删除和创建文件及目录 菜谱将展示
Boost.Filesystem
的另一个使用示例。 -
读取 Boost 的官方文档以获取关于
Boost.Filesystem
更多功能的信息;它可在以下链接找到:www.boost.org/doc/libs/1_53_0/libs/filesystem/doc/index.htm
。 -
Boost.Filesystem
库被提议包含在 C++1y 中。草案可在www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3399.html
找到。
删除和创建文件及目录
让我们考虑以下代码行:
std::ofstream ofs("dir/subdir/file.txt");
ofs << "Boost.Filesystem is fun!";
在这些行中,我们尝试在 dir/subdir
目录中的 file.txt
写入一些内容。如果没有这样的目录,这个尝试将会失败。与文件系统交互的能力对于编写良好的工作代码是必要的。
在这个菜谱中,我们将构建一个目录和子目录,将一些数据写入文件,并尝试创建symlink
,如果创建符号链接失败,将删除创建的文件。我们还将避免使用异常作为错误报告的机制,而更倾向于使用某种形式的返回代码。
让我们看看如何使用 Boost 以优雅的方式做到这一点。
准备工作
为了这个菜谱,需要具备 C++ 和 std::ofstream
类的基本知识。Boost.Filesystem
不是一个仅包含头文件的库,因此这个菜谱中的代码需要链接到 boost_system
和 boost_filesystem
库。
如何做到这一点...
我们继续处理文件系统的可移植包装,在这个菜谱中,我们将看到如何修改目录内容:
-
和往常一样,我们需要包含一些头文件:
#include <boost/filesystem/operations.hpp> #include <cassert> #include <fstream>
-
现在我们需要一个变量来存储错误(如果有):
int main() { boost::system::error_code error;
-
如果需要,我们还将创建目录,如下所示:
boost::filesystem::create_directories( "dir/subdir", error); assert(!error);
-
然后我们将向文件写入数据:
std::ofstream ofs("dir/subdir/file.txt"); ofs << "Boost.Filesystem is fun!"; assert(ofs); ofs.close();
-
我们需要尝试创建
symlink
:boost::filesystem::create_directory_symlink("dir/subdir", "symlink", error);
-
然后我们需要检查文件是否可以通过
symlink
访问:if (!error) { std::cerr << "Symlink created\n"; assert(boost::filesystem::exists("symlink/file.txt"));
-
或者,如果
symlink
创建失败,则删除创建的文件:} else { std::cerr << "Failed to create a symlink\n"; boost::filesystem::remove("dir/subdir/file.txt", error); assert(!error); } /*if (!error)*/ } /*main*/
它是如何工作的...
我们在几乎所有关于操作任务的配方中看到了boost::system::error_code
的使用。它可以存储有关错误的信息,并在 Boost 库中得到广泛使用。
注意
如果你没有向Boost.Filesystem
函数提供一个boost::system::error_code
实例,代码将编译良好,但发生错误时,将抛出异常。通常,除非你在分配内存方面遇到问题,否则会抛出boost::filesystem::filesystem_error
异常。
仔细查看第 3 步。我们使用了boost::filesystem::create_directories
函数,而不是boost::filesystem::create_directory
,因为后者不能创建子目录。
剩余步骤很容易理解,不应引起任何麻烦。
还有更多...
boost::system::error_code
类是 C++11 的一部分,可以在std::
命名空间中的<system_error>
头文件中找到。Boost.Filesystem
的类不是 C++11 的一部分,但它们被提议包含在 C++1y 中,预计将在 2014 年准备好。
最后,对那些将要使用Boost.Filesystem
的人有一个小建议;当文件系统操作中发生的错误是常规的,使用boost::system::error_codes
。否则,捕获异常更可取且更可靠。
另请参阅
- 列出目录中的文件配方还包含有关
Boost.Filesystem
的信息。阅读 Boost 的官方文档以获取更多信息及示例,请访问www.boost.org/doc/libs/1_53_0/libs/filesystem/doc/index.htm
。
快速从一个进程传递数据到另一个进程
有时我们编写的程序将大量相互通信。当程序在不同的机器上运行时,使用套接字是通信最常见的技术。但如果多个进程在单个机器上运行,我们可以做得更好!
让我们看看如何使用Boost.Interprocess
库使单个内存片段在不同进程间可用。
准备工作
此配方需要具备 C++的基本知识。还需要了解原子变量(有关原子变量的更多信息,请参阅另请参阅部分)。某些平台需要链接到运行时库。
如何操作...
在此示例中,我们将在进程间共享一个单一的原子变量,使其在新的进程启动时增加,在进程终止时减少:
-
我们需要包含以下头文件以进行进程间通信:
#include <boost/interprocess/managed_shared_memory.hpp>
-
在头文件、
typedef
和检查之后,将帮助我们确保原子变量可用于此示例:#include <boost/atomic.hpp> typedef boost::atomic<int> atomic_t; #if (BOOST_ATOMIC_INT_LOCK_FREE != 2) #error "This code requires lock-free boost::atomic<int>" #endif
-
创建或获取共享内存段:
boost::interprocess::managed_shared_memory segment(boost::interprocess::open_or_create, "shm-cache", 1024);
-
获取或构造一个
原子
变量:atomic_t& atomic = *segment.find_or_construct<atomic_t> //1 ("shm-counter") // 2 (0) // 3 ;
-
以通常的方式处理
原子
变量:std::cout << "I have index " << ++ atomic << "\nPress any key..."; std::cin.get();
-
销毁
原子
变量:int snapshot = -- atomic; if (!snapshot) { segment.destroy<atomic_t>("shm-counter"); boost::interprocess::shared_memory_object ::remove("shm-cache"); } } /*main*/
那就结束了!现在如果我们同时运行这个程序的多实例,我们会看到每个新的实例都会增加它的索引值。
它是如何工作的...
这个方法的核心理念是获取一个对所有进程可见的内存段,并在其中放置一些数据。让我们看看第 3 步,我们在这里检索这样一个内存段。在这里,shm-cache
是段的名称(不同的段名称不同);你可以给段起任何你喜欢的名字。第一个参数是boost::interprocess::open_or_create
,这意味着boost::interprocess::managed_shared_memory
将打开一个名为shm-cache
的现有段,或者如果不存在,则构造它。最后一个参数是段的尺寸。
注意
分段的尺寸必须足够大,以便能够容纳Boost.Interprocess
库特定的数据。这就是为什么我们使用了1024
而不是sizeof(atomic_t)
。但实际上这并不重要,因为操作系统会将这个值四舍五入到最近的更大的支持值,这通常等于或大于 4 千字节。
第 4 步是一个棘手的部分,因为我们在这里同时执行多个任务。在这一步的2
部分,我们将在段中查找或构造一个名为shm-counter
的变量。在第 4 步的3
部分,我们将提供一个参数,如果它在第 2 步中没有找到,这个参数将被用来初始化变量。只有在变量没有找到并且需要构造时,这个参数才会被使用,否则它将被忽略。仔细看看第二行(1
部分)。看到对解引用操作符*
的调用。我们这样做是因为segment.find_or_construct<atomic_t>
返回一个指向atomic_t
的指针,而在 C++中使用裸指针是一种不良风格。
注意
注意,我们正在使用共享内存中的原子变量!这是必需的,因为两个或多个进程可以同时操作同一个shm-counter
原子变量。
当你与共享内存中的对象一起工作时,你必须非常小心;不要忘记销毁它们!在第 6 步,我们正在使用它们的名称销毁对象和段。
还有更多...
仔细看看第 2 步,我们在这里检查BOOST_ATOMIC_INT_LOCK_FREE != 2
。我们检查atomic_t
不会使用互斥锁。这非常重要,因为通常互斥锁在共享内存中不会工作。所以如果BOOST_ATOMIC_INT_LOCK_FREE
不等于2
,我们将得到未定义的行为。
不幸的是,C++11 没有提供跨进程类,据我所知,Boost.Interprocess
没有被提议包含在 C++1y 中。
注意
一旦创建了一个管理段,它就不能增加大小!确保你创建的段足够大以满足你的需求,或者查看*另请参阅*
部分以获取有关增加管理段的信息。
共享内存是进程间通信最快的方式,适用于可以共享内存的进程。这通常意味着这些进程必须在同一主机上运行或在对称多处理(SMP)集群上运行。
参见
-
同步进程间通信配方将向您介绍更多关于共享内存、进程间通信以及同步访问共享内存中的资源。
-
参见第五章,多线程中的使用原子操作快速访问公共资源配方,以获取有关原子的更多信息。
-
Boost 的官方文档
Boost.Interprocess
也可能有所帮助;它可在www.boost.org/doc/libs/1_53_0/doc/html/interprocess.html
找到。 -
如何增加管理段在
www.boost.org/doc/libs/1_53_0/doc/html/interprocess/managed_memory_segments.html#interprocess.managed_memory_segments.managed_memory_segment_advanced_features.growin
g_managed_memory中描述。
同步进程间通信
在上一个配方中,我们看到了如何创建共享内存以及如何将一些对象放入其中。现在是我们做一些有用的事情的时候了。让我们从一个例子开始,这个例子来自第五章的创建 work_queue 类配方,多线程,并使其适用于多个进程。在这个例子的最后,我们将得到一个可以存储不同任务并在进程间传递它们的类。
准备工作
本配方使用前一个配方中的技术。您还需要阅读第五章的创建 work_queue 类配方,多线程,并理解其主要思想。这个例子需要在某些平台上链接到运行时库。
如何做到这一点...
被认为是在线程之外创建单独的子进程而不是线程可以使程序更可靠,因为子进程的终止不会终止主进程。我们不会对这个假设进行争论,只是看看进程间数据共享如何实现。
-
本配方需要很多头文件:
#include <boost/interprocess/managed_shared_memory.hpp> #include <boost/interprocess/containers/deque.hpp> #include <boost/interprocess/allocators/allocator.hpp> #include <boost/interprocess/sync/interprocess_mutex.hpp> #include <boost/interprocess/sync/interprocess_condition.hpp> #include <boost/interprocess/sync/scoped_lock.hpp> #include <boost/optional.hpp>
-
现在我们需要定义我们的结构,
task_structure
,它将被用来存储任务:struct task_structure { // ... };
-
让我们开始编写
work_queue
类:class work_queue { public: typedef task_structure task_type; typedef boost::interprocess::managed_shared_memory managed_shared_memory_t; typedef boost::interprocess::allocator< task_type, managed_shared_memory_t::segment_manager > allocator_t;
-
按照以下方式编写
work_queue
的成员:private: managed_shared_memory_t segment_; const allocator_t allocator_; typedef boost::interprocess::deque<task_type, allocator_t> deque_t; typedef boost::interprocess::interprocess_mutex mutex_t; typedef boost::interprocess::interprocess_condition condition_t; typedef boost::interprocess::scoped_lock<mutex_t> scoped_lock_t; deque_t& tasks_; mutex_t& mutex_; boost::interprocess::interprocess_condition& cond_;
-
成员的初始化应如下所示:
public: explicit work_queue() : segment_( boost::interprocess::open_or_create, "work-queue", 1024 * 1024 * 64 ) , allocator_(segment_.get_segment_manager()) , tasks_( *segment_.find_or_construct<deque_t> ("work-queue:deque")(allocator_) ) , mutex_( *segment_.find_or_construct<mutex_t> ("work-queue:mutex")() ) , cond_( *segment_.find_or_construct<condition_t> ("work-queue:condition")() ) {}
-
我们需要对
work_queue
的成员函数做一些小的修改,例如使用scoped_lock_t
而不是原始的唯一锁:boost::optional<task_type> try_pop_task() { boost::optional<task_type> ret; scoped_lock_t lock(mutex_); if (!tasks_.empty()) { ret = tasks_.front(); tasks_.pop_front(); } return ret; }
它是如何工作的...
在这个菜谱中,我们几乎与第五章中“创建工作队列类”菜谱中做的是完全相同的事情,即多线程,但是在我们在共享内存中分配数据时,进行内存分配或使用同步原语时必须格外小心。
在存储具有指针或引用作为成员字段的共享内存对象时,请格外小心。我们将在下一菜谱中看到如何处理指针。
看看步骤 2。我们没有使用boost::function
作为任务类型,因为它包含指针,所以在共享内存中不会工作。
第 3 步之所以有趣,是因为allocator_t
。它是一种所有容器都必须用来分配元素的分配器类型。它是一个有状态的分配器,这意味着它将与容器一起复制。此外,它不能被默认构造。
如果没有从共享内存段分配内存,它将不可用于其他进程;这就是为什么需要一个特定于容器的分配器的原因。
第 4 步相当简单,除了我们只有对tasks_
、mutex_
和cond_
的引用。这样做是因为对象本身是在共享内存中构建的。因此,work_queue
只能存储它们的引用。
在第 5 步中,我们正在初始化成员。这段代码对你来说很熟悉;我们在前一个菜谱中做了完全相同的事情。请注意,我们在构建时向tasks_
提供了一个分配器实例。这是因为allocator_t
不能由容器本身构造。
注意
共享内存不会在进程的退出事件中被销毁,因此我们可以运行程序一次,将任务发布到工作队列中,停止程序,启动其他程序,并获取由程序的第一实例存储的任务。共享内存只有在重启时或显式调用segment.deallocate("work-queue");
时才会被销毁。
还有更多...
如前一个菜谱中提到的,C++11 没有Boost.Interprocess
中的类。此外,你不得在共享内存段中使用 C++11 或 C++03 容器。其中一些容器可能可以工作,但这种行为是不可移植的。
如果你查看一些<boost/interprocess/containers/*.hpp>
头文件,你会发现它们只是使用了Boost.Containers
库中的容器:
namespace boost { namespace interprocess {
using boost::container::vector;
}}
Boost.Interprocess
的容器具有Boost.Containers
库的所有优点,包括右值引用及其在较旧编译器上的模拟。
Boost.Interprocess
是同一台机器上运行的进程之间通信的最快解决方案。
参见
-
使用共享内存中的指针菜谱
-
有关同步原语和多线程的更多信息,请参阅第五章,多线程。
-
请参阅 Boost 官方文档中的
Boost.Interprocess
库以获取更多示例和信息;它可在以下链接中找到:
在共享内存中使用指针
很难想象在不使用指针的情况下编写一些 C++核心类。指针和引用在 C++中无处不在,它们在共享内存中不起作用!所以如果我们有一个这样的结构在共享内存中,并将共享内存中某个整数的地址赋给pointer_
,我们不会在尝试从with_pointer
的这个实例使用pointer_
的其他进程中得到正确的地址:
struct with_pointer {
int* pointer_;
// ...
int value_holder_;
};
我们如何修复这个问题?
准备工作
之前的配方是理解这个配方所必需的。示例在某些平台上需要链接到运行时系统库。
如何做到这一点...
修复它非常简单;我们只需要用offset_ptr<>
替换指针:
#include <boost/interprocess/offset_ptr.hpp>
struct correct_struct {
boost::interprocess::offset_ptr<int> pointer_;
// ...
int value_holder_;
};
现在我们可以像使用普通指针一样使用它:
correct_struct& ref = *segment
.construct<correct_struct>("structure")();
ref.pointer_ = &ref.value_holder_;
assert(ref.pointer_ == &ref.value_holder_);
assert(*ref.pointer_ == ref.value_holder_);
ref.value_holder_ = ethalon_value;
assert(*ref.pointer_ == ethalon_value);
它是如何工作的...
我们不能在共享内存中使用指针,因为当一块共享内存被映射到进程的地址空间时,其地址仅对该进程有效。当我们获取变量的地址时,它只是该进程的一个局部地址;其他进程将共享内存映射到不同的基本地址,因此变量地址将不同。
那么,我们如何处理一个始终变化的地址呢?有一个技巧!因为指针和结构在同一个共享内存段中,它们之间的距离不会改变。boost::interprocess::offset_ptr
背后的想法是记住这个距离,在解引用时,将距离值添加到offset_ptr
变量的进程相关地址。
偏移指针模仿指针的行为,因此它是一个可以快速应用的即插即用替换品。
小贴士
不要将可能包含指针或引用的类放入共享内存!
还有更多...
偏移指针比常规指针稍微慢一些,因为每次解引用时都需要计算地址。但这种差异通常不足以让你烦恼。
C++11 没有偏移指针。
参见
-
Boost 的官方文档包含了大量示例和更高级的
Boost.Interprocess
功能;它可在www.boost.org/doc/libs/1_53_0/doc/html/interprocess.html
找到。 -
“最快读取文件”配方中包含了关于
Boost.Interprocess
库一些非传统用法的信息。
最快读取文件的方法
在整个互联网上,人们都在问“最快读取文件的方法是什么?”。让我们使这个配方的任务更加困难:“最快且最便携的读取二进制文件的方法是什么?”
准备工作
为了这个配方,需要具备 C++和std::fstream
容器的基本知识。
如何做到这一点...
来自这个配方的技术被许多对输入输出性能至关重要的应用程序广泛使用。
-
我们需要包含来自
Boost.Interprocess
库的两个头文件:#include <boost/interprocess/file_mapping.hpp> #include <boost/interprocess/mapped_region.hpp>
-
现在我们需要打开一个文件:
const boost::interprocess::mode_t mode = boost::interprocess::read_only; boost::interprocess::file_mapping fm(filename, mode);
-
这个菜谱的主要部分是将所有文件映射到内存中:
boost::interprocess::mapped_region region(fm, mode, 0, 0);
-
获取文件中数据的指针:
const char* begin = reinterpret_cast<const char*>(region.get_address());
就这样!现在我们可以像使用正常内存一样处理文件:
const char* pos = std::find(begin, begin + region.get_size(), '\1');
它是如何工作的...
所有流行的操作系统都具有将文件映射到进程地址空间的能力。完成此类映射后,进程可以像使用正常内存一样使用这些地址。操作系统将负责所有的文件操作,例如缓存和预读。
为什么它比传统的读写操作更快?那是因为在大多数情况下,读写操作被实现为内存映射和将数据复制到用户指定的缓冲区。所以读取通常要做更多的工作。
正如 STL 的情况一样,我们在打开文件时必须提供一个打开模式。请参阅第 2 步,其中我们提供了boost::interprocess::read_only
模式。
请参阅第 3 步,其中我们一次性映射了整个文件。这个操作实际上非常快,因为操作系统不会从磁盘读取数据,而是等待请求成为映射区域的一部分。在请求映射区域的一部分后,操作系统将从磁盘加载该部分的文件。正如我们所见,内存映射操作是懒加载的,映射区域的大小不会影响性能。
注意
然而,32 位操作系统无法内存映射大文件,因此您需要分块映射它们。POSIX(Linux)操作系统要求定义_FILE_OFFSET_BITS=64
,以便整个项目能够在 32 位平台上处理大文件。否则,操作系统将无法映射超过 4GB 的文件部分。
现在是时候测量性能了:
$ TIME="%E" time ./reading_files m
mapped_region: 0:00.08
$ TIME="%E" time ./reading_files r
ifstream: 0:00.09
$ TIME="%E" time ./reading_files a
C:
0:00.09
正如预期的那样,内存映射文件比传统的读取操作略快。我们还可以看到,纯 C 方法与 C++的std::ifstream
类的性能相同,所以请勿在 C++中使用与FILE*
相关的函数。这些函数仅适用于 C,不适用于 C++!
为了std::ifstream
的最佳性能,请务必以二进制模式打开文件并按块读取数据:
std::ifstream f(filename, std::ifstream::binary);
// ...
char c[kilobyte];
f.read(c, kilobyte);
还有更多...
不幸的是,内存映射文件的类不是 C++11 的一部分,而且看起来它们也不会在 C++14 中出现。
向内存映射区域写入也是一种非常快速的操作。操作系统将缓存写入操作,并且不会立即将修改刷新到磁盘。操作系统和std::ofstream
数据缓存之间有一个区别。如果std::ofstream
数据被应用程序缓存并且应用程序终止,那么缓存的数据可能会丢失。当数据被操作系统缓存时,应用程序的终止不会导致数据丢失。电源故障和系统崩溃在这两种情况下都会导致数据丢失。
如果多个进程映射同一个文件,并且其中一个进程修改了映射区域,那么这些更改将立即对其他进程可见。
另请参阅
-
Boost
.Interprocess
库包含了许多与系统一起工作的有用功能;本书并未涵盖所有这些功能。您可以在官方网站上了解更多关于这个伟大库的信息:
协程 – 保存状态和推迟执行
现在,许多嵌入式设备仍然只有单核。开发者为这些设备编写代码,试图从中榨取最大性能。对于这样的设备使用Boost.Threads
或其他线程库并不有效;操作系统将被迫调度线程以执行,管理资源等等,因为硬件无法并行运行它们。
那么,我们如何在主程序等待某些资源时切换程序到子程序的执行?
准备工作
为了使用这个配方,需要具备 C++和模板的基本知识。阅读一些关于Boost.Function
的配方也可能有所帮助。
如何实现...
这个配方是关于协程,允许有多个入口点的子程序。多个入口点使我们能够在特定位置暂停和恢复程序的执行,切换到/从其他子程序。
-
Boost.Coroutine
库将负责几乎所有的事情。我们只需要包含其头文件:#include <boost/coroutine/coroutine.hpp>
-
使用所需的签名创建一个协程类型:
typedef boost::coroutines::coroutine< std::string&(std::size_t max_characters_to_process) > corout_t;
-
创建一个协程:
void coroutine_task(corout_t::caller_type& caller); int main() { corout_t coroutine(coroutine_task);
-
现在我们可以执行子程序,同时在主程序中等待事件:
// Doing some work // ... while (!spinlock.try_lock()) { // We may do some useful work, before // attempting to lock a spinlock once more coroutine(10); // Small delays } // Spinlock is locked // ... while (!port.block_ready()) { // We may do some useful work, before // attempting to get block of data once more coroutine(300); // Bigger delays std::string& s = coroutine.get(); // ... }
-
协程方法应该看起来像这样:
void coroutine_task(corout_t::caller_type& caller) { std::string result; // Returning back to main program caller(result); while (1) { std::size_t max_characters_to_process = caller.get(); // Do process some characters // ... // Returning result, switching back // to main program caller(result); } /*while*/ }
工作原理...
在第 2 步,我们使用函数签名std::string& (std::size_t)
作为模板参数来描述我们的子程序签名。这意味着子程序接受std::size_t
并返回一个字符串的引用。
第 3 步之所以有趣,是因为coroutine_task
的签名。请注意,这个签名适用于所有协程任务。caller
是用于从调用者获取参数并将执行结果返回给调用者的变量。
第 3 步需要额外的注意,因为corout_t
的构造函数将自动启动协程执行。这就是为什么我们在协程任务开始时调用caller(result)
(它将我们带回到main
方法)。
当我们在第 4 步中调用coroutine(10)
时,我们正在导致协程程序执行。执行将在第一个caller(result)
方法之后跳转到第 5 步,在那里我们将从caller.get()
获取一个值10
并继续我们的执行,直到caller(result)
。之后,执行将返回到第 4 步,紧随coroutine(10)
调用之后。接下来,对coroutine(10)
或coroutine(300)
的调用将继续从第 5 步中第二个caller(result)
方法之后的地点继续子程序的执行。
在第 4 步中查看std::string& s = coroutine.get()
。在这里,我们将从第 5 步中描述的coroutine_task
的开始获取std::string
的结果。我们甚至可以修改它,coroutine_task
将看到修改后的值。让我描述一下协程和线程之间的主要区别。当一个协程执行时,主任务什么都不做。当主任务执行时,协程任务什么都不做。你无法对线程有这种保证。使用协程,你可以明确指定何时开始子任务以及何时结束它。在单核环境中,线程可以在任何时刻切换;你无法控制这种行为。
注意
不要使用线程的局部存储,不要在同一协程内部调用boost::coroutines::coroutine<>::operator()
;当协程任务完成时,不要调用boost::coroutines::coroutine<>::get()
。这些操作会导致未定义的行为。
还有更多...
在切换线程时,操作系统会做很多工作,因此这不是一个很快的操作。然而,使用协程,你可以完全控制任务切换;此外,你不需要执行任何特定于操作系统的内核工作。切换协程比切换线程要快得多,但它的速度不如调用boost::function
。
Boost.Coroutine
库将负责调用协程任务中的变量的析构函数,因此无需担心泄漏。
注意
协程使用boost::coroutines::detail::forced_unwind
异常来释放非std::exception
派生的资源。你必须注意不要在协程任务中捕获该异常。
C++11 没有协程。但协程尽可能使用 C++11 的特性,甚至在 C++03 编译器上模拟右值引用。你不能复制boost::coroutines::coroutine<>
,但你可以使用Boost.Move
来移动它们。
参见
-
Boost 的官方文档包含了
Boost.Coroutines
库的更多示例、性能注释、限制和使用案例;它可在以下链接找到:www.boost.org/doc/libs/1_53_0/libs/coroutine/doc/html/index.htm
-
查看第三章(Chapter 3. Managing Resources)的食谱,Managing Resources,以及第五章(Chapter 5. Multithreading)的Multithreading,以了解
Boost.Coroutine
、Boost.Thread
和Boost.Function
库之间的区别
第十二章。冰山一角
在本章中,我们将涵盖:
-
处理图
-
可视化图
-
使用真随机数生成器
-
使用可移植的数学函数
-
编写测试用例
-
在一个测试模块中组合多个测试用例
-
操作图像
简介
Boost 是一个庞大的库集合。其中一些库很小,适合日常使用,而其他一些则需要单独的书籍来描述它们的所有功能。本章致力于介绍这些大型库,并为你提供一些基础知识以开始使用。
前两个菜谱将解释 Boost.Graph
的用法。这是一个拥有大量算法的大库。我们将看到一些基础知识,以及它最重要的部分——图的可视化。
我们还将看到一个非常有用的生成真随机数的菜谱。这对于编写安全的加密系统来说是一个非常重要的要求。
一些 C++ 标准库缺少数学函数。我们将看到如何使用 Boost 来解决这个问题。但本书的格式没有足够的空间来描述所有这些函数。
编写测试用例在 编写测试用例 和 在一个测试模块中组合多个测试用例 菜谱中描述。这对于任何生产级系统来说都很重要。
最后一个菜谱是关于一个在我大学期间帮助我在许多课程中取得成功的库。可以使用它创建和修改图像。我本人用它来可视化不同的算法、在图像中隐藏数据、签名图像和生成纹理。
不幸的是,即使是这一章也无法告诉你关于所有 Boost 库的信息。也许有一天我会写另一本书...然后是几本更多。
处理图
一些任务需要数据的图形表示。Boost.Graph
是一个库,旨在提供一种灵活的方式来在内存中构建和表示图。它还包含许多用于处理图的算法,例如拓扑排序、广度优先搜索、深度优先搜索和 Dijkstra 最短路径。
好吧,让我们用 Boost.Graph
执行一些基本任务!
准备工作
对于这个菜谱,只需要具备基本的 C++ 和模板知识。
如何做...
在这个菜谱中,我们将描述一个图类型,创建该类型的图,向图中添加一些顶点和边,并搜索特定的顶点。这应该足以开始使用 Boost.Graph
。
-
我们首先描述图类型:
#include <boost/graph/adjacency_list.hpp> #include <string> typedef std::string vertex_t; typedef boost::adjacency_list< boost::vecS , boost::vecS , boost::bidirectionalS , vertex_t > graph_type;
-
现在我们来构建它:
graph_type graph;
-
让我们使用一个非可移植的技巧来加速图构建:
static const std::size_t vertex_count = 5; graph.m_vertices.reserve(vertex_count);
-
现在我们已经准备好向图中添加顶点了:
typedef boost::graph_traits<graph_type> ::vertex_descriptor descriptor_t; descriptor_t cpp = boost::add_vertex(vertex_t("C++"), graph); descriptor_t stl = boost::add_vertex(vertex_t("STL"), graph); descriptor_t boost = boost::add_vertex(vertex_t("Boost"), graph); descriptor_t guru = boost::add_vertex(vertex_t("C++ guru"), graph); descriptor_t ansic = boost::add_vertex(vertex_t("C"), graph);
-
是时候用边连接顶点了:
boost::add_edge(cpp, stl, graph); boost::add_edge(stl, boost, graph); boost::add_edge(boost, guru, graph); boost::add_edge(ansic, guru, graph);
-
我们编写一个搜索顶点的函数:
template <class GraphT> void find_and_print(const GraphT& g, boost::string_ref name) {
-
现在我们将编写代码来获取所有顶点的迭代器:
typedef typename boost::graph_traits<graph_type> ::vertex_iterator vert_it_t; vert_it_t it, end; boost::tie(it, end) = boost::vertices(g);
-
是时候运行搜索以查找所需的顶点了:
typedef boost::graph_traits<graph_type>::vertex_descriptor desc_t; for (; it != end; ++ it) { desc_t desc = *it; if (boost::get(boost::vertex_bundle, g)[desc] == name.data()) { break; } } assert(it != end); std::cout << name << '\n'; } /* find_and_print */
它是如何工作的...
在第 1 步,我们描述了我们的图必须看起来像什么以及它必须基于什么类型。boost::adjacency_list
是一个表示图作为二维结构的类,其中第一个维度包含顶点,第二个维度包含该顶点的边。boost::adjacency_list
必须是表示图的默认选择;它适用于大多数情况。
第一个模板参数boost::adjacency_list
描述了用于表示每个顶点的边列表的结构;第二个描述了存储顶点的结构。我们可以使用特定的选择器为这些结构选择不同的 STL 容器,如下表所示:
选择器 | STL 容器 |
---|---|
boost::vecS |
std::vector |
boost::listS |
std::list |
boost::slistS |
std::slist |
boost::setS |
std::set |
boost::multisetS |
std::multiset |
boost::hash_setS |
std::hash_set |
第三个模板参数用于创建无向、有向或双向图。分别使用boost::undirectedS
、boost::directedS
和boost::bidirectionalS
选择器。
第五个模板参数描述了将用作顶点的数据类型。在我们的例子中,我们选择了std::string
。我们也可以支持边的数据类型,并将其作为模板参数提供。
第 2 步和第 3 步是微不足道的,但在第 4 步,您将看到一种非可移植的方式来加快图构建。在我们的例子中,我们使用std::vector
作为存储顶点的容器,因此我们可以强制它为所需数量的顶点保留内存。这导致在将顶点插入图时,内存分配/释放和复制操作更少。这一步是非可移植的,因为它高度依赖于boost::adjacency_list
的当前实现以及存储顶点的所选容器类型。
在第 4 步,我们看到如何将顶点添加到图中。注意boost::graph_traits<graph_type>
的使用。boost::graph_traits
类用于获取特定于图类型的类型。我们将在本章后面看到其用法和一些特定于图类型的描述。第 5 步展示了我们需要做什么来通过边连接顶点。
注意
如果我们提供了边的数据类型,添加边的样子如下:
boost::add_edge(ansic, guru, edge_t(initialization_parameters), graph)
注意,在第 6 步中,图类型是一个template
参数。这建议为了实现更好的代码重用并使此函数能够与其它图类型一起工作。
在第 7 步,我们看到如何遍历图中的所有顶点。顶点迭代器的类型来自boost::graph_traits
。函数boost::tie
是Boost.Tuple
的一部分,用于从元组中获取值到变量中。因此,调用boost::tie(it, end) = boost::vertices(g)
将begin
迭代器放入it
变量中,将end
迭代器放入end
变量中。
这可能让你感到惊讶,但顶点迭代器的解引用并不返回顶点数据。相反,它返回顶点描述符 desc
,可以在 boost::get(boost::vertex_bundle, g)[desc]
中使用以获取顶点数据,就像我们在第 8 步中所做的那样。顶点描述符类型在许多 Boost.Graph
函数中使用;我们在第 5 步的边构造函数中看到了它的使用。
注意
如前所述,Boost.Graph
库包含了众多算法的实现。你将发现许多搜索策略已经实现,但在这本书中我们不会讨论它们。我们将仅限于介绍图库的基础知识。
还有更多...
Boost.Graph
库不是 C++11 的一部分,也不会成为 C++1y 的一部分。当前的实现不支持 C++11 功能。如果我们使用的是难以复制的顶点,我们可以使用以下技巧来提高速度:
vertex_descriptor desc = boost::add_vertex(graph);boost::get(boost::vertex_bundle, g_)[desc] = std::move(vertex_data);
它避免了 boost::add_vertex(vertex_data, graph)
的复制构造,而是使用带有移动赋值的默认构造。
Boost.Graph
的效率取决于多个因素,例如底层容器类型、图表示、边和顶点数据类型。
相关内容
-
阅读关于 可视化图 的食谱可以帮助你更轻松地处理图。你也可以考虑阅读以下链接中的官方文档:
www.boost.org/doc/libs/1_53_0/libs/graph/doc/table_of_contents.html
可视化图
由于可视化问题,制作操作图的程序从未容易过。当我们使用 STL 容器,如 std::map
和 std::vector
时,我们总能打印容器的内容并查看内部发生的情况。但是,当我们处理复杂的图时,很难以清晰的方式可视化内容:顶点太多,边太多。
在这个食谱中,我们将探讨使用 Graphviz 工具对 Boost.Graph
的可视化。
准备工作
要可视化图,你需要一个 Graphviz 可视化工具。还需要了解前面的食谱。
如何操作...
可视化分为两个阶段。在第一阶段,我们让程序以文本格式输出图的描述;在第二阶段,我们将第一步的输出导入到某个可视化工具中。本食谱中的编号步骤都是关于第一阶段的内容。
-
让我们像前一个食谱中那样为
graph_type
编写std::ostream
操作符:#include <boost/graph/graphviz.hpp> std::ostream& operator<<(std::ostream& out, const graph_type& g) { detail::vertex_writer<graph_type> vw(g); boost::write_graphviz(out, g, vw); return out; }
-
在前面的步骤中使用到的
detail::vertex_writer
结构必须定义为以下内容:namespace detail { template <class GraphT> class vertex_writer { const GraphT& g_; public: explicit vertex_writer(const GraphT& g) : g_(g) {} template <class VertexDescriptorT> void operator()(std::ostream& out, const VertexDescriptorT& d) const { out << " [label=\"" << boost::get(boost::vertex_bundle, g_)[d] << "\"]"; } }; // vertex_writer } // namespace detail
就这些了。现在,如果我们使用 std::cout << graph;
命令可视化前一个食谱中的图,输出可以被用来使用 dot
命令行工具创建图形图片:
$ dot -Tpng -o dot.png
digraph G {
0 [label="C++"];
1 [label="STL"];
2 [label="Boost"];
3 [label="C++ guru"];
4 [label="C"];
0->1 ;
1->2 ;
2->3 ;
4->3 ;
}
前一个命令的输出如图所示:
如果命令行让你感到害怕,我们也可以使用 Gvedit 或 XDot 程序进行可视化。
它是如何工作的...
Boost.Graph
库包含将图输出为 Graphviz (DOT) 格式的函数。如果我们按步骤 1 使用两个参数写入 boost::write_graphviz(out, g)
,该函数将输出一个顶点从 0
开始编号的图图片。这并不很有用,因此我们提供了一个 vertex_writer
类的实例,该实例输出顶点名称。
正如我们在第二步中看到的,输出格式必须是 DOT 格式,这是 Graphviz 工具可以理解的。你可能需要阅读 Graphviz 文档以获取有关 DOT 格式的更多信息。
如果你想在可视化过程中向边添加一些数据,我们需要将边可视化实例作为第四个参数提供给 boost::write_graphviz
。
还有更多...
C++11 不包含 Boost.Graph
或图形可视化的工具。但你不必担心——有很多其他的图形格式和可视化工具,Boost.Graph
可以与它们中的很多一起工作。
参见
-
与图一起工作 的配方包含有关
Boost.Graphs
构造的信息。 -
你可以在
www.graphviz.org/
找到关于 DOT 格式和 Graphviz 的很多信息。 -
Boost 的官方文档
Boost.Graph
库包含多个示例和有用的信息,可以在www.boost.org/doc/libs/1_53_0/libs/graph/doc/table_of_
contents.html 找到。
使用真正的随机数生成器
我知道很多商业产品使用错误的方法来获取随机数。遗憾的是,一些公司仍然在密码学和银行软件中使用 rand()
。
让我们看看如何使用 Boost.Random
获取一个完全随机的均匀分布,这对于银行软件来说是合适的。
准备工作
对于这个配方,需要具备基本的 C++ 知识。了解不同类型的分布也将有所帮助。这个配方中的代码需要链接到 boost_random
库。
如何做到这一点...
要创建真正的随机数,我们需要从操作系统或处理器那里得到一些帮助。这是使用 Boost 可以做到的:
-
我们需要包含以下头文件:
#include <boost/config.hpp> #include <boost/random/random_device.hpp> #include <boost/random/uniform_int_distribution.hpp>
-
高级随机数提供者在不同的平台上有不同的名称:
static const std::string provider = #ifdef BOOST_WINDOWS "Microsoft Strong Cryptographic Provider" #else "/dev/urandom" #endif ;
-
现在我们已经准备好使用
Boost.Random
初始化生成器:boost::random_device device(provider);
-
让我们获取一个在 1000 到 65535 之间返回值的均匀分布:
boost::random::uniform_int_distribution<unsigned short> random(1000);
就这样。现在我们可以使用 random(device)
调用来获取真正的随机数。
它是如何工作的...
为什么 rand()
函数不适合银行?因为它生成伪随机数,这意味着黑客可以预测下一个生成的数字。这是所有伪随机数算法的问题。一些算法更容易预测,而一些则更难预测,但仍然有可能。
这就是为什么我们在本例中使用 boost::random_device
(参见第 3 步)。该设备从整个操作系统中收集关于随机事件的信息,以构建一个不可预测的硬件生成的数字。此类事件的例子包括按键之间的延迟、某些硬件中断之间的延迟以及内部 CPU 随机数生成器。
操作系统可能拥有多个此类随机数生成器。在我们的 POSIX 系统示例中,我们使用了 /dev/urandom
而不是更安全的 /dev/random
,因为后者在捕获足够的随机事件之前会保持阻塞状态。等待熵值可能需要几秒钟,这对于应用程序通常是不合适的。使用 /dev/random
来创建长期有效的 GPG/SSL/SSH
密钥。
现在我们已经完成了生成器的设置,是时候进入第 4 步,讨论分布类。如果生成器只是生成数字(通常是均匀分布),分布类将一个分布映射到另一个。在第 4 步中,我们创建了一个均匀分布,它返回一个无符号短整型的随机数。参数 1000
表示该分布必须返回大于或等于 1000
的数字。我们还可以提供一个最大数字作为第二个参数,默认情况下等于返回类型可以存储的最大值。
还有更多...
Boost.Random
为不同的需求提供了大量的真/伪随机生成器和分布。避免复制分布和生成器;这可能会变成一个昂贵的操作。
C++11 支持不同的分布类和生成器。您将在 std::
命名空间中的 <random>
头文件中找到本示例中的所有类。Boost.Random
库不使用 C++11 功能,并且对于该库来说也不是必需的。您应该使用 Boost 实现,还是 STL?Boost 提供了跨系统的更好可移植性;然而,某些 STL 实现可能有汇编优化的实现,并可能提供一些有用的扩展。
参见
-
官方文档包含了一个完整的生成器和分布列表及其描述;它可在以下链接中找到:
使用可移植的数学函数
一些项目需要特定的三角函数、用于数值求解常微分方程的库以及与分布和常量一起工作。所有这些 Boost.Math
的部分都很难放入甚至是一本书中。一个单独的配方肯定是不够的。所以让我们专注于处理浮点类型的基本日常使用函数。
我们将编写一个可移植的函数,用于检查输入值是否为无穷大和不是数字(NaN)值,并在值为负时更改其符号。
准备工作
对于此食谱,需要具备 C++ 的基本知识。那些了解 C99 标准的人会发现本食谱中有许多共同之处。
如何做...
执行以下步骤以检查输入值是否为无穷大和 NaN 值,并在值为负时更改符号:
-
我们需要以下头文件:
#include <boost/math/special_functions.hpp> #include <cassert>
-
断言无穷大和 NaN 可以这样做:
template <class T> void check_float_inputs(T value) { assert(!boost::math::isinf(value)); assert(!boost::math::isnan(value));
-
使用以下代码来更改符号:
if (boost::math::signbit(value)) { value = boost::math::changesign(value); } // ... } // check_float_inputs
就这些!现在我们可以检查 check_float_inputs(std::sqrt(-1.0))
和 check_float_inputs(std::numeric_limits<double>::max() * 2.0)
将导致断言。
它是如何工作的...
实数类型有特定的值,无法使用相等运算符进行检查。例如,如果变量 v
包含 NaN,assert(v!=v)
可能通过或不通过,这取决于编译器。
对于此类情况,Boost.Math
提供了可以可靠地检查无穷大和 NaN 值的函数。
第 3 步包含 boost::math::signbit
函数,需要澄清。此函数返回一个有符号位,当数字为负时为 1,当数字为正时为 0。换句话说,如果值为负,则返回 true
。
看到第 3 步,一些读者可能会问:“为什么我们不能直接乘以 -1
而不是调用 boost::math::changesign
?”。我们可以。但是乘法可能比 boost::math::changesign
慢,并且对于特殊值不起作用。例如,如果你的代码可以处理 nan
,第 3 步中的代码将能够改变 -nan
的符号并将 nan
写入变量。
注意
Boost.Math
库维护者建议将此示例中的数学函数用圆括号括起来,以避免与 C 宏冲突。最好写成 (boost::math::isinf)(value)
而不是 boost::math::isinf(value)
。
还有更多...
C99 包含了本食谱中描述的所有函数。为什么在 Boost 中需要它们呢?嗯,一些编译器供应商认为程序员不需要它们,所以你不会在一个非常流行的编译器中找到它们。另一个原因是 Boost.Math
函数可以用于像数字一样行为的类。
Boost.Math
是一个非常快速、便携、可靠的库。
参见
- Boost 的官方文档包含许多有趣的示例和教程,这些可以帮助你熟悉
Boost.Math
;浏览到www.boost.org/doc/libs/1_53_0/libs/math/doc/html/index.html
编写测试用例
本食谱和下一个食谱致力于自动测试 Boost.Test
库,该库被许多 Boost 库使用。让我们动手实践,为我们的类编写一些测试。
#include <stdexcept>
struct foo {
int val_;
operator int() const;
bool is_not_null() const;
void throws() const; // throws(std::logic_error)
};
准备工作
对于此食谱,需要具备 C++ 的基本知识。本食谱的代码需要链接到 boost_unit_test_framework
库的静态版本。
如何做...
说实话,Boost 中有不止一个测试库。我们将查看功能最强大的一款。
-
要使用它,我们需要定义宏并包含以下头文件:
#define BOOST_TEST_MODULE test_module_name #include <boost/test/unit_test.hpp>
-
每组测试都必须在测试用例中编写:
BOOST_AUTO_TEST_CASE(test_no_1) {
-
检查某个函数是否返回
true
的结果如下:foo f1 = {1}, f2 = {2}; BOOST_CHECK(f1.is_not_null());
-
检查不等性的实现方式如下:
BOOST_CHECK_NE(f1, f2);
-
检查抛出异常的代码如下:
BOOST_CHECK_THROW(f1.throws(), std::logic_error); } // BOOST_AUTO_TEST_CASE(test_no_1)
就这样!编译和链接后,我们将得到一个可执行文件,该文件将自动测试foo
并以人类可读的格式输出测试结果。
它是如何工作的...
编写单元测试很容易;你知道函数是如何工作的,以及在特定情况下它应该产生什么结果。所以你只需检查预期的结果是否与函数的实际输出相同。这就是我们在步骤 3 中所做的。我们知道f1.is_not_null()
将返回true
,并进行了检查。在步骤 4 中,我们知道f1
不等于f2
,因此也进行了检查。调用f1.throws()
将产生std::logic_error
异常,并检查是否抛出了预期类型的异常。
在步骤 2 中,我们正在创建一个测试用例——一组检查以验证foo
结构的正确行为。在单个源文件中我们可以有多个测试用例。例如,如果我们添加以下代码:
BOOST_AUTO_TEST_CASE(test_no_2) {
foo f1 = {1}, f2 = {2};
BOOST_REQUIRE_NE(f1, f2);
// ...
} // BOOST_AUTO_TEST_CASE(test_no_2)
此代码将与test_no_1
测试用例一起运行。传递给BOOST_AUTO_TEST_CASE
宏的参数只是测试用例的唯一名称,在出错时会显示。
Running 2 test cases...
main.cpp(15): error in "test_no_1": check f1.is_not_null() failed
main.cpp(17): error in "test_no_1": check f1 != f2 failed [0 == 0]
main.cpp(19): error in "test_no_1": exception std::logic_error is expected
main.cpp(24): fatal error in "test_no_2": critical check f1 != f2 failed [0 == 0]
*** 4 failures detected in test suite "test_module_name"
BOOST_REQUIRE_*
和BOOST_CHECK_*
宏之间有一个小的区别。如果BOOST_REQUIRE_*
宏检查失败,当前测试用例的执行将停止,Boost.Test
将运行下一个测试用例。然而,失败的BOOST_CHECK_*
不会停止当前测试用例的执行。
步骤 1 需要额外的注意。注意BOOST_TEST_MODULE
宏定义。这个宏必须在包含Boost.Test
头文件之前定义,否则程序链接将失败。更多信息可以在本食谱的“也见”部分找到。
还有更多...
一些读者可能会想,“为什么我们在步骤 4 中写BOOST_CHECK_NE(f1, f2)
而不是BOOST_CHECK(f1 != f2)
?”答案很简单:步骤 4 中的宏提供了更易读和更详细的输出。
C++11 缺乏对单元测试的支持。然而,可以使用Boost.Test
库来测试 C++11 代码。记住,你拥有的测试越多,你得到的代码就越可靠!
也见
-
“在一个测试模块中组合多个测试用例”食谱中包含有关测试和
BOOST_TEST_MODULE
宏的更多信息 -
请参阅 Boost 的官方文档以获取完整的测试宏列表和
Boost.Test
高级特性的信息;它可在以下链接中找到:
在一个测试模块中组合多个测试用例
编写自动测试对你的项目很有好处。但是,当项目很大并且许多开发者都在工作时,管理测试用例就变得很困难。在这个菜谱中,我们将探讨如何运行单个测试以及如何在单个模块中组合多个测试用例。
让我们假设有两个开发者正在测试foo.hpp
头文件中声明的foo
结构,我们希望给他们分别提供源文件来编写测试。这样,开发者就不会相互打扰,可以并行工作。然而,默认的测试运行必须执行两个开发者的测试。
准备工作
此菜谱需要具备基本的 C++知识。此菜谱部分重用了前一个菜谱中的代码,并且还需要链接到boost_unit_test_framework
库的静态版本。
如何做到这一点...
此菜谱使用前一个菜谱中的代码。这是一个非常有用的菜谱,用于测试大型项目;不要低估它。
-
在前一个菜谱的
main.cpp
中的所有头文件中,只留下这两行:#define BOOST_TEST_MODULE test_module_name #include <boost/test/unit_test.hpp>
-
让我们将前一个示例中的测试用例移动到两个不同的源文件中:
// developer1.cpp #include <boost/test/unit_test.hpp> #include "foo.hpp" BOOST_AUTO_TEST_CASE(test_no_1) { // ... } /////////////////////////////////////////////////////////// // developer2.cpp #include <boost/test/unit_test.hpp> #include "foo.hpp" BOOST_AUTO_TEST_CASE(test_no_2) { // ... }
就这样!因此,编译和链接所有源文件和两个测试用例将在程序执行时工作。
如何工作...
所有魔法都是由BOOST_TEST_MODULE
宏完成的。如果它在<boost/test/unit_test.hpp>
之前定义,Boost.Test
就会认为这个源文件是主要的,并且所有辅助测试基础设施都必须放在里面。否则,只有测试宏将从<boost/test/unit_test.hpp>
中包含。
如果你将它们与包含BOOST_TEST_MODULE
宏的源文件链接,则会运行所有的BOOST_AUTO_TEST_CASE
测试。当在一个大项目上工作时,每个开发者可能只启用自己的源文件的编译和链接。这给了开发者独立性,并提高了开发速度——在调试时不需要编译外部源代码和运行外部测试。
还有更多...
Boost.Test
库之所以好,是因为它能够选择性地运行测试。我们可以选择要运行的测试,并将它们作为命令行参数传递。例如,以下命令将只运行test_no_1
测试用例:
./testing_advanced –run=test_no_1
以下命令将运行两个测试用例:
./testing_advanced –run=test_no_1,test_no_2
不幸的是,C++11 标准没有内置的测试支持,而且看起来 C++1y 也不会采用Boost.Test
的类和方法。
相关内容
-
编写测试用例 菜单包含有关
Boost.Test
库的更多信息。有关Boost.Test
的更多信息,请阅读 Boost 的官方文档,网址为www.boost.org/doc/libs/1_53_0/libs/test/doc/html/utf.html
。 -
勇敢的读者可以查看 Boost 库中的一些测试用例。这些测试用例位于
boost
文件夹中的libs
子文件夹中。例如,Boost.LexicalCast
测试用例位于boost_1_53_0\libs\conversion\test
。
操作图像
我给你留了一些真正美味的东西作为甜点——Boost 的通用图像库(GIL),它允许你操作图像而无需过多关注图像格式。
让我们用它做一些简单而有趣的事情;让我们写一个程序,将任何图片取反。
准备工作
这个配方需要基本的 C++、模板和Boost.Variant
知识。示例需要链接 PNG 库。
如何做...
为了简单起见,我们将只处理 PNG 图像。
-
让我们从包含头文件开始:
#include <boost/gil/gil_all.hpp> #include <boost/gil/extension/io/png_dynamic_io.hpp> #include <string>
-
现在我们需要定义我们希望与之工作的图像类型:
typedef boost::mpl::vector< boost::gil::gray8_image_t, boost::gil::gray16_image_t, boost::gil::rgb8_image_t, boost::gil::rgb16_image_t > img_types;
-
以这种方式实现打开现有 PNG 图像:
std::string file_name(argv[1]); boost::gil::any_image<img_types> source; boost::gil::png_read_image(file_name, source);
-
我们需要将操作应用于图片,如下所示:
boost::gil::apply_operation( view(source), negate() );
-
以下代码行将帮助你写入图像:
boost::gil::png_write_view("negate_" + file_name, const_view(source));
-
让我们看看修改操作:
struct negate { typedef void result_type; // required template <class View> void operator()(const View& source) const { // ... } }; // negate
-
operator()
的主体包括获取通道类型:typedef typename View::value_type value_type; typedef typename boost::gil::channel_type<value_type>::type channel_t;
-
它也遍历像素:
const std::size_t channels = boost::gil::num_channels<View>::value; const channel_t max_val = (std::numeric_limits<channel_t>::max)(); for (unsigned int y = 0; y < source.height(); ++y) { for (unsigned int x = 0; x < source.width(); ++x) { for (unsigned int c = 0; c < channels; ++c) { source(x, y)[c] = max_val - source(x, y)[c]; } } }
现在我们来看看我们程序的结果:
上一张图片是下一张图片的负片:
它是如何工作的...
在第 2 步中,我们正在描述我们希望与之工作的图像类型。这些图像是每像素 8 位和 16 位的灰度图像以及每像素 8 位和 16 位的 RGB 图片。
boost::gil::any_image<img_types>
类是一种Boost.Variant
,可以持有img_types
变量之一的图像。正如你可能已经猜到的,boost::gil::png_read_image
将图像读取到图像变量中。
第 4 步中的boost::gil::apply_operation
函数几乎等于Boost.Variant
库中的boost::apply_visitor
。注意view(source)
的使用。boost::gil::view
函数在图像周围构建一个轻量级包装器,将其解释为二维像素数组。
你还记得我们为Boost.Variant
从boost::static_visitor
派生访问者吗?当我们使用 GIL 的变体版本时,我们需要在visitor
内部创建一个result_type
类型定义。你可以在第 6 步中看到它。
一点理论:图像由称为像素的点组成。单个图像具有相同类型的像素。然而,不同图像的像素可能在通道数和单通道颜色位上有所不同。通道表示一种主颜色。在 RGB 图像的情况下,我们将有一个由三个通道组成的像素——红色、绿色和蓝色。在灰度图像的情况下,我们将有一个表示灰度的单个通道。
回到我们的图像。在第 2 步中,我们描述了我们希望与之工作的图像类型。在第 3 步中,其中一种图像类型从文件中读取并存储在源变量中。在第 4 步中,为所有图像类型实例化了negate
访问者的operator()
方法。
在第 7 步中,我们可以看到如何从图像视图中获取通道类型。
在第 8 步中,我们遍历像素和通道并将它们取反。取反是通过max_val - source(x, y)[c]
完成的,并将结果写回图像视图。
我们在步骤 5 中写回一个图像。
还有更多...
C++11 没有内置处理图像的方法。
Boost.GIL
库运行速度快且效率高。编译器对其代码进行了很好的优化,我们甚至可以使用一些Boost.GIL
方法来帮助优化器展开循环。但本章只讨论了库的一些基础知识,所以现在是时候停止了。
参见
-
关于
Boost.GIL
的更多信息可以在 Boost 的官方文档中找到;请访问www.boost.org/doc/libs/1_53_0/libs/gil/doc/index.html
-
参见第一章中的在变量/容器中存储多个选定的类型配方,以获取有关
Boost.Variant
库的更多信息