Boost-C++-应用开发秘籍第二版(全)
Boost C++ 应用开发秘籍第二版(全)
原文:
annas-archive.org/md5/8a1821d22bcd421390c328e6f1d92500
译者:飞龙
前言
如果您想充分利用 Boost 和 C++的真正力量,并避免在不同情况下使用哪个库的困惑,那么这本书适合您。
从 Boost C++的基础知识开始,您将学习如何利用 Boost 库简化应用程序开发。您将学会将数据转换,例如将字符串转换为数字,数字转换为字符串,数字转换为数字等。资源管理将变得轻而易举。您将了解可以在编译时完成哪些工作以及 Boost 容器的功能。您将学会为高质量、快速和可移植的应用程序开发所需的一切。只需编写一次程序,然后就可以在 Linux、Windows、macOS 和 Android 操作系统上使用。从操作图像到图形、目录、定时器、文件和网络,每个人都会找到一个有趣的主题。
请注意,本书的知识不会过时,因为越来越多的 Boost 库成为 C++标准的一部分。
本书涵盖的内容
第一章,“开始编写您的应用程序”,介绍了日常使用的库。我们将看到如何从不同来源获取配置选项,以及使用 Boost 库作者引入的一些数据类型可以做些什么。
第二章,“资源管理”,涉及由 Boost 库引入的数据类型,主要关注指针的使用。我们将看到如何轻松管理资源,以及如何使用能够存储任何功能对象、函数和 lambda 表达式的数据类型。阅读完本章后,您的代码将变得更加可靠,内存泄漏将成为历史。
第三章,“转换和强制转换”,描述了如何将字符串、数字和用户定义的类型相互转换,如何安全地转换多态类型,以及如何在 C++源文件中编写小型和大型解析器。涵盖了日常使用和罕见情况下数据转换的多种方式。
第四章,“编译时技巧”,描述了 Boost 库的一些基本示例,可以用于调整算法的编译时检查,以及其他元编程任务。没有理解这些内容,就无法理解 Boost 源代码和其他类似 Boost 的库。
第五章,“多线程”,着重介绍了多线程编程的基础知识以及与之相关的所有内容。
第六章,“任务操作”,展示了将功能对象称为任务。本章的主要思想是,我们可以将所有处理、计算和交互分解为函数对象(任务),并几乎独立地处理每个任务。此外,我们可以不阻塞一些慢操作(例如从套接字接收数据或等待超时),而是提供一个回调任务,并继续处理其他任务。一旦操作系统完成慢操作,我们的回调将被执行。
第七章,“字符串操作”,展示了改变、搜索和表示字符串的不同方面。我们将看到如何使用 Boost 库轻松完成一些常见的与字符串相关的任务。它涉及非常常见的字符串操作任务。
第八章,“元编程”,介绍了一些酷而难以理解的元编程方法。在本章中,我们将深入了解如何将多种类型打包成单个类似元组的类型。我们将创建用于操作类型集合的函数,看到如何改变编译时集合的类型,以及如何将编译时技巧与运行时混合使用。
第九章《容器》介绍了 boost 容器及与之直接相关的内容。本章提供了关于 Boost 类的信息,这些类可以在日常编程中使用,可以使您的代码运行速度更快,开发新应用程序更容易。
第十章《收集平台和编译器信息》描述了用于检测编译器、平台和 Boost 特性的不同辅助宏--这些宏广泛用于 boost 库,并且对于编写能够使用任何编译器标志的可移植代码至关重要。
第十一章《与系统一起工作》提供了对文件系统的更详细的了解,以及如何创建和删除文件。我们将看到数据如何在不同的系统进程之间传递,如何以最大速度读取文件,以及如何执行其他技巧。
第十二章《触类旁通》致力于一些大型库,并为您提供一些入门基础知识。
您需要为本书做好准备
您需要一个现代的 C++编译器,Boost 库(任何版本都可以,建议使用 1.65 或更高版本),以及 QtCreator/qmake,或者只需访问apolukhin.GitHub.io/Boost-Cookbook/
在线运行和实验示例。
这本书适用对象
这本书适用于希望提高对 Boost 的了解并希望简化其应用程序开发流程的开发人员。假定具有先前的 C++知识和标准库的基本知识。
章节
在本书中,您将经常看到几个标题(准备工作,如何做...,它是如何工作的...,还有更多...,另请参阅)。为了清晰地说明如何完成配方,我们使用这些部分如下:
准备工作
本节告诉您配方中可以期望的内容,并描述了为配方设置任何软件或任何预备设置所需的步骤。
如何做...
本节包含遵循该配方所需的步骤。
它是如何工作...
本节通常包括对前一节发生的事情的详细解释。
还有更多...
本节包含有关配方的其他信息,以使读者更加了解配方。
另请参阅
本节提供了有关配方的其他有用信息的链接。
约定
在本书中,您将找到一些区分不同类型信息的文本样式。以下是这些样式的一些示例及其含义的解释。
文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:
“请记住,这个库不仅仅是一个头文件,所以您的程序必须链接到libboost_program_options
库”。
代码块设置如下:
#include <boost/program_options.hpp>
#include <iostream>
namespace opt = boost::program_options;
int main(int argc, char *argv[])
{
当我们希望引起您对代码块的特定部分的注意时,相关行或项目以粗体显示:
#include <boost/program_options.hpp>
#include <iostream>
namespace opt = boost::program_options;
int main(int argc, char *argv[])
任何命令行输入或输出都以以下方式编写:
$ ./our_program.exe --apples=10 --oranges=20
Fruits count: 30
新术语和重要单词以粗体显示。
警告或重要说明会以这样的形式出现在一个框中。
提示和技巧会出现在这样的形式中。
第一章:开始编写你的应用程序
在本章中,我们将涵盖:
-
获取配置选项
-
将任何值存储在一个容器/变量中
-
将多个选择的类型存储在一个容器/变量中
-
使用更安全的方式处理存储多个选择类型的容器
-
在没有值的情况下返回一个值或标志
-
从函数返回数组
-
将多个值合并为一个
-
绑定和重新排序函数参数
-
获取可读的类型名称
-
使用 C++11 移动模拟
-
创建一个不可复制的类
-
创建一个不可复制但可移动的类
-
使用 C++14 和 C++11 算法
介绍
Boost是一个 C++库集合。每个库在被 Boost 接受之前都经过许多专业程序员的审查。库在多个平台上使用多个编译器和多个 C++标准库实现进行测试。在使用 Boost 时,您可以确信您正在使用一个最具可移植性、快速和可靠的解决方案之一,该解决方案在商业和开源项目中都适用的许可证下分发。
Boost 的许多部分已经包含在 C++11、C++14 和 C++17 中。此外,Boost 库将包含在 C++的下一个标准中。您将在本书的每个配方中找到特定于 C++标准的注释。
不需要长篇介绍,让我们开始吧!
在本章中,我们将看到一些日常使用的配方。我们将看到如何从不同来源获取配置选项,以及使用 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
库。
如何做...
让我们从一个简单的程序开始,该程序接受apples
和oranges
的数量作为输入,并计算水果的总数。我们希望实现以下结果:
$ ./our_program.exe --apples=10 --oranges=20 Fruits count: 30
执行以下步骤:
- 包括
boost/program_options.hpp
头文件,并为boost::program_options
命名空间创建一个别名(它太长了!)。我们还需要一个<iostream>
头文件:
#include <boost/program_options.hpp>
#include <iostream>
namespace opt = boost::program_options;
- 现在,我们准备在
main()
函数中描述我们的选项:
int main(int argc, char *argv[])
{
// Constructing an options describing variable and giving
// it a textual description "All options".
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")
("help", "produce help message")
;
- 让我们解析命令行:
// 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);
// Must be called after all the parsing and storing.
opt::notify(vm);
- 让我们为处理
help
选项添加一些代码:
if (vm.count("help")) {
std::cout << desc << "\n";
return 1;
}
- 最后一步。计算水果可以以以下方式实现:
std::cout << "Fruits count: "
<< vm["apples"].as<int>() + vm["oranges"].as<int>()
<< std::endl;
} // end of `main`
现在,如果我们用help
参数调用我们的程序,我们将得到以下输出:
All options:
--apples arg how many apples do you have
--oranges arg how many oranges do you have
--help produce help message
如你所见,我们没有为help
选项的值提供类型,因为我们不希望向其传递任何值。
它是如何工作的...
这个例子从代码和注释中很容易理解。运行它会产生预期的结果:
$ ./our_program.exe --apples=100 --oranges=20 Fruits count: 120
还有更多...
C++标准采用了许多 Boost 库;然而,即使在 C++17 中,你也找不到Boost.ProgramOptions
。目前,没有计划将其纳入 C++2a。
ProgramOptions
库非常强大,具有许多功能。以下是如何做的:
- 将配置选项值直接解析到一个变量中,并将该选项设置为必需的:
int oranges_var = 0;
desc.add_options()
// ProgramOptions stores the option value into
// the variable that is passed by pointer. Here value of
// "--oranges" option will be stored into 'oranges_var'.
("oranges,o", opt::value<int>(&oranges_var)->required(),
"oranges you have")
- 获取一些必需的字符串选项:
// 'name' option is not marked with 'required()',
// so user may not provide it.
("name", opt::value<std::string>(), "your name")
- 为苹果添加简称,将
10
设置为apples
的默认值:
// 'a' is a short option name for apples. Use as '-a 10'.
// If no value provided, then the default value is used.
("apples,a", opt::value<int>()->default_value(10),
"apples that you have");
- 从配置文件获取缺失的选项:
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. Just use
// 'opt::store with' 'opt::parse_environment' function.
// Adding missing options from "apples_oranges.cfg" config file.
try {
opt::store(
opt::parse_config_file<char>("apples_oranges.cfg", desc),
vm
);
} catch (const opt::reading_file& e) {
std::cout << "Error: " << e.what() << std::endl;
}
配置文件语法与命令行语法不同。我们不需要在选项前加上减号。因此,我们的apples_oranges.cfg
文件必须如下所示:
oranges=20
- 验证是否设置了所有必需的选项:
try {
// `opt::required_option` exception is thrown if
// one of the required options was not set.
opt::notify(vm);
} catch (const opt::required_option& e) {
std::cout << "Error: " << e.what() << std::endl;
return 2;
}
如果我们将所有提到的提示组合成一个可执行文件,那么它的help
命令将产生以下输出:
$ ./our_program.exe --help
All options:
-o [ --oranges ] arg oranges that you have
--name arg your name
-a [ --apples ] arg (=10) apples that you have
--help produce help message
如果没有配置文件运行,将产生以下输出:
$ ./our_program.exe
Error: can not read options configuration file 'apples_oranges.cfg'
Error: the option '--oranges' is required but missing
在配置文件中以oranges=20
运行程序将生成++,因为 apples 的默认值是10
:
$ ./our_program.exe
Fruits count: 30
另请参阅
-
Boost 的官方文档包含了更多的例子,并告诉我们关于
Boost.ProgramOptions
更高级的特性,比如位置相关的选项,非常规的语法等;可以在boost.org/libs/program_options
找到。 -
你可以在
apolukhin.github.io/Boost-Cookbook
上修改并运行本书中的所有示例。
在容器/变量中存储任何值
如果你一直在使用 Java、C#或 Delphi 进行编程,你肯定会想念在 C++中使用Object
值类型创建容器的能力。在这些语言中,Object
类是几乎所有类型的基本类,因此你可以随时将几乎任何值赋给它。想象一下,如果 C++中有这样的功能会多么棒:
typedef std::unique_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;
准备工作
我们将使用这个仅包含头文件的库。这个示例只需要基本的 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);
some_values.push_back("Hello there!");
some_values.push_back(std::string("Wow!"));
std::string& s = boost::any_cast<std::string&>(some_values.back());
s += " That is great!";
std::cout << s;
}
很棒,不是吗?顺便说一句,它有一个空状态,可以使用empty()
成员函数进行检查(就像标准库容器一样)。
你可以使用两种方法从boost::any
中获取值:
void example() {
boost::any variable(std::string("Hello world!"));
// 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);
// Never throws. 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
的值,并且派生自某个基本类型placeholder
:
template<typename ValueType>
struct holder : public placeholder {
virtual const std::type_info& type() const {
return typeid(ValueType);
}
ValueType held;
};
placeholder
类型有虚函数,用于获取存储类型T
的std::type_info
和克隆存储类型:
struct placeholder {
virtual ~placeholder() {}
virtual const std::type_info& type() const = 0;
};
boost::any
存储ptr
-- 指向placeholder
的指针。当使用any_cast<T>()
时,boost::any
会检查调用ptr->type()
是否给出std::type_info
等于typeid(T)
,并返回static_cast<holder<T>*>(ptr)->held
。
还有更多...
这种灵活性并非没有代价。对boost::any
的实例进行复制构造、值构造、复制赋值和赋值操作都会进行动态内存分配;所有类型转换都会进行运行时类型信息(RTTI)检查;boost::any
大量使用虚函数。如果你对性能很敏感,下一个示例将让你了解如何在不使用动态分配和 RTTI 的情况下实现几乎相同的结果。
boost::any
使用右值引用,但不能在constexpr中使用。
Boost.Any
库已被接受到 C++17 中。如果你的编译器兼容 C++17,并且希望避免使用boost
来使用any
,只需将boost
命名空间替换为std
命名空间,并包含<any>
而不是<boost/any.hpp>
。如果你在std::any
中存储小对象,你的标准库实现可能会稍微更快。
std::any
具有reset()
函数,而不是clear()
,还有has_value()
而不是empty()
。Boost 中几乎所有的异常都源自std::exception
类或其派生类,例如,boost::bad_any_cast
源自std::bad_cast
。这意味着你几乎可以使用catch (const std::exception& e)
捕获所有 Boost 异常。
另请参阅
-
Boost 的官方文档可能会给你一些更多的例子;可以在
boost.org/libs/any
找到。 -
有关此主题的更多信息,请参阅使用更安全的方式处理存储多种选择类型的容器的示例
在容器/变量中存储多种选择类型
C++03 联合体只能容纳称为POD(Plain Old Data)的极其简单的类型。例如,在 C++03 中,你不能在联合体中存储std::string
或std::vector
。
你是否了解 C++11 中不受限制的联合体的概念?让我简要地告诉你。C++11 放宽了对联合体的要求,但你必须自己管理非 POD 类型的构造和销毁。你必须调用就地构造/销毁,并记住联合体中存储的类型。这是一项巨大的工作,不是吗?
我们是否可以在 C++03 中拥有一个像变量一样管理对象生命周期并记住其类型的不受限制的联合体?
准备工作
我们将使用这个只有头文件的库,它很容易使用。这个配方只需要基本的 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;
}
很棒,不是吗?
Boost.Variant
没有空状态,但有一个无用且总是返回false
的empty()
函数。如果你需要表示一个空状态,只需在Boost.Variant
库支持的类型列表的第一个位置添加一些简单的类型。当Boost.Variant
包含该类型时,将其解释为空状态。以下是一个例子,我们将使用boost::blank
类型来表示一个空状态:
void example1() {
// Default constructor constructs an instance of boost::blank.
boost::variant<
boost::blank, int, const char*, std::string
> var;
// 'which()' method returns an index of a type
// currently held by variant.
assert(var.which() == 0); // boost::blank
var = "Hello, dear reader";
assert(var.which() != 0);
}
- 你可以使用两种方法从变体中获取值:
void example2() {
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 NULL.
int* s2 = boost::get<int>(&variable);
}
它是如何工作的...
boost::variant
类持有一个字节数组并在该数组中存储值。数组的大小是通过在编译时应用sizeof()
和函数来获取每个模板类型的对齐方式来确定的。在赋值或构造boost::variant
时,先前的值将就地销毁,并且新值将在字节数组的顶部构造,使用就地新放置。
还有更多...
Boost.Variant
变量通常不会动态分配内存,也不需要启用 RTTI。Boost.Variant
非常快速,并被其他 Boost 库广泛使用。为了实现最大的性能,确保在支持的类型列表的第一个位置有一个简单的类型。如果你的编译器支持 C++11 的右值引用,boost::variant
将会利用它。
Boost.Variant
是 C++17 标准的一部分。std::variant
与boost::variant
略有不同:
-
std::variant
声明在<variant>
头文件中,而不是在<boost.variant.hpp>
中。 -
std::variant
永远不会分配内存 -
std::variant
可用于 constexpr -
你不再需要写
boost::get<int>(&variable)
,而是需要为std::variant
写std::get_if<int>(&variable)
-
std::variant
不能递归地持有自身,并且缺少一些其他高级技术 -
std::variant
可以就地构造对象 -
std::variant
有index()
而不是which()
另请参阅
-
使用更安全的方式来处理存储多种选择类型的容器配方
-
Boost 的官方文档包含了更多的例子和对
Boost.Variant
的一些其他特性的描述,可以在boost.org/libs/variant
找到 -
在
apolukhin.github.io/Boost-Cookbook
上在线尝试这段代码
使用更安全的方式来处理存储多种选择类型的容器
想象一下,你正在创建一个围绕某个 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 "Type
// 'reference to string'" 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 {
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;
}
如果你编译并运行这个例子,它将输出一个正确的答案:
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<>
获取值更安全。这种模式强制程序员注意 variant 中的每种类型,否则代码将无法编译。您可以通过boost::apply_visitor
函数使用此模式,该函数将visitor
函数对象作为第一个参数,将variant
作为第二个参数。如果您使用的是 C++14 之前的编译器,则visitor
函数对象必须派生自boost::static_visitor<T>
类,其中T
是visitor
返回的类型。visitor
对象必须对 variant 存储的每种类型重载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 recipe "Type 'reference to string'"
// 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 a 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;
for (auto it = row.begin(), end = row.end(); it != end; ++it) {
res += boost::apply_visitor(db_sum_visitor(), *it);
}
std::cout << "Sum of arithmetic types in database row is: "
<< res << std::endl;
}
工作原理
在编译时,Boost.Variant
库生成一个大的switch
语句,每个 case 都调用 variant 类型列表中的单个类型的visitor
。在运行时,使用which()
检索存储类型的索引,并跳转到switch
语句中的正确 case。对于boost::variant<int, float, std::string>
,将生成类似于以下内容:
switch (which())
{
case 0 /*int*/:
return visitor(*reinterpret_cast<int*>(address()));
case 1 /*float*/:
return visitor(*reinterpret_cast<float*>(address()));
case 2 /*std::string*/:
return visitor(*reinterpret_cast<std::string*>(address()));
default: assert(false);
}
在这里,address()
函数返回一个指向boost::variant<int, float, std::string>
内部存储的指针。
还有更多...
如果我们将这个例子与本教程中的第一个例子进行比较,我们会看到boost::variant
的以下优点:
-
我们知道变量可以存储哪些类型。
-
如果 SQL 接口的库编写者添加或修改了
variant
持有的类型,我们将得到编译时错误而不是不正确的行为
C++17 中的std::variant
也支持访问。只需使用std::visit
而不是boost::apply_visitor
即可。
您可以从您在www.PacktPub.com
的帐户中下载您购买的所有 Packt 图书的示例代码文件。如果您在其他地方购买了本书,可以访问www.PacktPub.com/
support,并注册以直接通过电子邮件接收文件。
另请参阅
-
阅读第四章的一些教程后,即编译时技巧,即使底层类型发生变化,您也能够正确地编写通用的
visitor
对象 -
Boost 的官方文档包含更多示例和
Boost.Variant
的一些其他特性的描述;可以在以下链接找到:boost.org/libs/variant
在没有值的情况下返回值或标志
假设我们有一个不会抛出异常并返回值或指示发生错误的函数。在 Java 或 C#编程语言中,通过将函数值与null
指针进行比较来处理这种情况。如果函数返回了null
,则发生了错误。在 C++中,从函数返回指针会使库用户感到困惑,并且通常需要缓慢的动态内存分配。
做好准备
本教程只需要基本的 C++知识。
如何做到...
女士们先生们,让我通过以下示例向您介绍Boost.Optional
库:
try_lock_device()
函数尝试获取设备的锁,可能成功也可能不成功,这取决于不同的条件(在我们的示例中,这取决于一些try_lock_device_impl()
函数的调用):
#include <boost/optional.hpp>
#include <iostream>
class locked_device {
explicit locked_device(const char* /*param*/) {
// We have unique access to device.
std::cout << "Device is locked\n";
}
static bool try_lock_device_impl();
public:
void use() {
std::cout << "Success!\n";
}
static boost::optional<locked_device> try_lock_device() {
if (!try_lock_device_impl()) {
// Failed to lock device.
return boost::none;
}
// Success!
return locked_device("device name");
}
~locked_device(); // Releases device lock.
};
该函数返回可转换为bool
的boost::optional
变量。如果返回值等于true
,则锁已获取,并且可以通过解引用返回的可选变量获得用于处理设备的类的实例:
int main() {
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
,不得解引用,因为这样的optional
没有构造的基础类型。
工作原理...
boost::optional<T>
在内部有一个正确对齐的字节数组,可以在其中就地构造类型为T
的对象。它还有一个bool
变量来记住对象的状态(它是否被构造了?)。
还有更多...
Boost.Optional
类不使用动态分配,也不需要基础类型的默认构造函数。当前的boost::optional
实现可以使用 C++11 的右值引用,但不能在 constexpr 中使用。
如果你有一个类T
,它没有空状态,但你的程序逻辑需要一个空状态或未初始化的T
,那么你必须想出一些解决方法。传统上,用户会创建一些指向类T
的智能指针,在其中保留一个nullptr
,并在需要非空状态时动态分配T
。别这样做!使用boost::optional<T>
。这是一个更快、更可靠的解决方案。
C++17 标准包括std::optional
类。只需将<boost/optional.hpp>
替换为<optional>
,将boost::
替换为std::
即可使用此类的标准版本。std::optional
可在 constexpr 中使用。
另请参阅
Boost 的官方文档包含了更多例子,并描述了Boost.Optional
的高级特性(比如就地构造)。文档可在以下链接找到:boost.org/libs/optional.
从函数返回数组
让我们来玩一个猜谜游戏!你能从以下函数中得出什么?
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>
只是一个围绕四个char
元素的数组的简单包装器。
这段代码回答了我们第一个例子中的所有问题,并且比第二个例子中的代码更易读。
工作原理...
boost::array
是一个固定大小的数组。boost::array
的第一个模板参数是元素类型,第二个是数组的大小。如果需要在运行时更改数组大小,可以使用std::vector
、boost::container::small_vector
、boost::container::stack_vector
或boost::container::vector
。
boost::array<>
类没有手写的构造函数,所有成员都是公共的,因此编译器会将其视为 POD 类型。
还有更多...
让我们看一些更多关于boost::array
的用法的例子:
#include <boost/array.hpp>
#include <algorithm>
typedef boost::array<char, 4> array4_t;
array4_t& vector_advance(array4_t& val) {
// C++11 lambda function
const auto inc = [](char& c){ ++c; };
// boost::array has begin(), cbegin(), end(), cend(),
// rbegin(), size(), empty() and other functions that are
// common for standard library containers.
std::for_each(val.begin(), val.end(), inc);
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}};
array4_t val_res; // it is default constructible
val_res = vector_advance(val); // it is assignable
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 'Check sizes at compile-time'
assert(sizeof(val) == sizeof(char) * array4_t::static_size);
}
boost::array
最大的优势之一是它不分配动态内存,并且提供与普通 C 数组完全相同的性能。C++标准委员会的人员也很喜欢它,因此它被接受为 C++11 标准。尝试包含<array>
头文件,并检查std::array
的可用性。std::array
自 C++17 以来对 constexpr 的使用支持更好。
参见
-
Boost 的官方文档提供了
Boost.Array
方法的完整列表,包括方法的复杂性和抛出行为的描述。可以在以下链接找到:boost.org/libs/array.
-
boost::array
函数在许多配方中被广泛使用;例如,参考将值绑定为函数参数配方。
将多个值组合成一个
对于那些喜欢std::pair
的人来说,这是一个非常好的礼物。Boost 有一个名为Boost.Tuple
的库。它就像std::pair
,但也可以处理三元组、四元组甚至更大的类型集合。
准备工作
此配方只需要基本的 C++知识和标准库。
如何做...
执行以下步骤将多个值组合成一个:
- 要开始使用元组,您需要包含适当的头文件并声明一个变量:
#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
是所需值的基于零的索引:
#include <boost/tuple/tuple.hpp>
void sample1() {
const int i = boost::get<0>(almost_a_pair);
const std::string& str = boost::get<1>(almost_a_pair);
const double d = boost::get<2>(quad);
}
boost::get<>
函数有许多重载,在 Boost 中被广泛使用。我们已经看到它如何与其他库一起在将多个选择的类型存储在容器/变量中配方中使用。
- 您可以使用
boost::make_tuple()
函数构造元组,这样写起来更短,因为不需要完全限定元组类型:
#include <boost/tuple/tuple.hpp>
#include <boost/tuple/tuple_comparison.hpp>
#include <set>
void sample2() {
// Tuple comparison operators are
// defined in header "boost/tuple/tuple_comparison.hpp"
// Don't forget to include it!
std::set<boost::tuple<int, double, int> > s;
s.insert(boost::make_tuple(1, 1.0, 2));
s.insert(boost::make_tuple(2, 10.0, 2));
s.insert(boost::make_tuple(3, 100.0, 2));
// Requires C++11
const auto t = boost::make_tuple(0, -1.0, 2);
assert(2 == boost::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 引用。这样的元组可以用于从另一个元组中获取值到变量。可以从以下示例更好地理解:
#include <boost/tuple/tuple.hpp>
#include <cassert>
void sample3() {
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);
}
它是如何工作的...
一些读者可能会想知道为什么我们需要元组,当我们总是可以编写自己的结构并使用更好的名称;例如,我们可以创建一个结构,而不是写boost::tuple<int, std::string>
:
struct id_name_pair {
int id;
std::string name;
};
嗯,这个结构肯定比boost::tuple<int, std::string>
更清晰。元组库的主要思想是简化模板编程。
还有更多...
元组的工作速度与std::pair
一样快(它不在堆上分配内存,也没有虚函数)。C++委员会发现这个类非常有用,因此它被包含在标准库中。您可以在头文件<tuple>
中找到与 C++11 兼容的实现(不要忘记用std::
替换所有boost::
命名空间)。
元组的标准库版本必须具有多个微优化,并通常提供略好的用户体验。但是,不能保证元组元素的构造顺序,因此,如果需要一个从第一个元素开始构造其元素的元组,必须使用boost::tuple
:
#include <boost/tuple/tuple.hpp>
#include <iostream>
template <int I>
struct printer {
printer() { std::cout << I; }
};
int main() {
// Outputs 012
boost::tuple<printer<0>, printer<1>, printer<2> > t;
}
当前的 Boost 元组实现不使用可变模板,不支持右值引用,不支持 C++17 结构化绑定,并且不支持 constexpr。
参见
-
Boost 的官方文档包含了更多关于
Boost.Tuple
的示例、性能信息和能力。可以在以下链接找到:boost.org/libs/tuple
。 -
在第八章的元编程中,将所有元组元素转换为字符串配方展示了元组的一些高级用法。
绑定和重新排序函数参数
如果您经常使用标准库并使用<algorithm>
头文件,那么您肯定会编写很多功能对象。在 C++14 中,您可以使用通用 lambda 来实现。在 C++11 中,您只能使用非通用 lambda。在较早版本的 C++标准中,您可以使用适配器函数(如bind1st
、bind2nd
、ptr_fun
、mem_fun
、mem_fun_ref
),或者您可以手动编写它们(因为适配器函数看起来很可怕)。好消息是:Boost.Bind
可以代替丑陋的适配器函数,并提供更易读的语法。
准备工作
熟悉标准库函数和算法将会有所帮助。
如何做...
让我们看一些使用Boost.Bind
与 C++11 lambda 类的例子:
- 所有示例都需要以下头文件:
// Contains boost::bind and placeholders.
#include <boost/bind.hpp>
// Utility stuff required by samples.
#include <boost/array.hpp>
#include <algorithm>
#include <functional>
#include <string>
#include <cassert>
- 按照以下代码显示的方式计算大于 5 的值:
void sample1() {
const boost::array<int, 12> v = {{
1, 2, 3, 4, 5, 6, 7, 100, 99, 98, 97, 96
}};
const std::size_t count0 = std::count_if(v.begin(), v.end(),
[](int x) { return 5 < x; }
);
const std::size_t count1 = std::count_if(v.begin(), v.end(),
boost::bind(std::less<int>(), 5, _1)
);
assert(count0 == count1);
}
- 这是我们如何计算空字符串的方法:
void sample2() {
const boost::array<std::string, 3> v = {{
"We ", "are", " the champions!"
}};
const std::size_t count0 = std::count_if(v.begin(), v.end(),
[](const std::string& s) { return s.empty(); }
);
const std::size_t count1 = std::count_if(v.begin(), v.end(),
boost::bind(&std::string::empty, _1)
);
assert(count0 == count1);
}
- 现在,让我们计算长度小于
5
的字符串:
void sample3() {
const boost::array<std::string, 3> v = {{
"We ", "are", " the champions!"
}};
const std::size_t count0 = std::count_if(v.begin(), v.end(),
[](const std::string& s) { return s.size() < 5; }
);
const std::size_t count1 = std::count_if(v.begin(), v.end(),
boost::bind(
std::less<std::size_t>(),
boost::bind(&std::string::size, _1),
5
)
);
assert(count0 == count1);
}
- 比较字符串:
void sample4() {
const boost::array<std::string, 3> v = {{
"We ", "are", " the champions!"
}};
std::string s(
"Expensive copy constructor is called when binding"
);
const std::size_t count0 = std::count_if(v.begin(), v.end(),
&s { return x < s; }
);
const std::size_t count1 = std::count_if(v.begin(), v.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>
void sample5() {
const boost::array<std::string, 3> v = {{
"We ", "are", " the champions!"
}};
std::string s(
"Expensive copy constructor is NOT called when binding"
);
const std::size_t count1 = std::count_if(v.begin(), v.end(),
boost::bind(std::less<std::string>(), _1, boost::cref(s))
);
// ...
}
您还可以使用bind
重新排序、忽略和复制函数参数:
void sample6() {
const auto twice = boost::bind(std::plus<int>(), _1, _1);
assert(twice(2) == 4);
const auto minus_from_second = boost::bind(std::minus<int>(), _2, _1);
assert(minus_from_second(2, 4) == 2);
const auto sum_second_and_third = boost::bind(
std::plus<int>(), _2, _3
);
assert(sum_second_and_third(10, 20, 30) == 50);
}
ref
、cref
和bind
函数被 C++11 标准接受,并在std::
命名空间的<functional>
头文件中定义。所有这些函数都不会动态分配内存,也不会使用虚函数。它们返回的对象易于优化,适用于良好的编译器。
这些函数的标准库实现可能具有额外的优化,以减少编译时间或仅仅是特定于编译器的优化。您可以使用bind
、ref
、cref
函数的标准库版本与任何 Boost 库一起使用,甚至混合使用 Boost 和标准库版本。
如果您使用的是 C++14 编译器,那么请使用通用 lambda 代替std::bind
和boost::bind
,因为它们更不晦涩,更容易理解。C++17 的 lambda 可以与 constexpr 一起使用,而std::bind
和boost::bind
不行。
另请参阅
官方文档包含更多示例和高级功能的描述,网址为boost.org/libs/bind.
获取可读的类型名称
通常需要在运行时获取可读的类型名称:
#include <iostream>
#include <typeinfo>
template <class T>
void do_something(const T& x) {
if (x == 0) {
std::cout << "Error: x == 0\. T is " << typeid(T).name()
<< std::endl;
}
// ...
}
然而,之前的例子并不是很通用。当禁用 RTTI 时,它无法工作,并且并不总是产生一个漂亮的可读名称。在一些平台上,之前的代码将只输出i
或d
。
如果我们需要一个不带const
、volatile
和引用的类型名称,情况会变得更糟:
void sample1() {
auto&& x = 42;
std::cout << "x is "
<< typeid(decltype(x)).name()
<< std::endl;
}
不幸的是,前面的代码在最好的情况下输出int
,这不是我们期望的结果。
准备工作
这个配方需要对 C++有基本的了解。
如何做
在第一种情况下,我们需要一个不带限定符的可读类型名称。Boost.TypeIndex
库将帮助我们:
#include <iostream>
#include <boost/type_index.hpp>
template <class T>
void do_something_again(const T& x) {
if (x == 0) {
std::cout << "x == 0\. T is " << boost::typeindex::type_id<T>()
<< std::endl;
}
// ...
}
在第二种情况下,我们需要保留限定符,因此我们需要从同一库中调用一个略有不同的函数:
#include <boost/type_index.hpp>
void sample2() {
auto&& x = 42;
std::cout << "x is "
<< boost::typeindex::type_id_with_cvr<decltype(x)>()
<< std::endl;
}
它是如何工作的...
Boost.TypeIndex
库为不同的编译器提供了许多解决方法,并且知道为类型生成可读名称的最有效方式。如果你将类型作为模板参数提供,该库保证所有可能的类型相关计算将在编译时执行,并且即使禁用了 RTTI,代码也能正常工作。
boost::typeindex::type_id_with_cvr
中的cvr
代表const
、volatile
和引用。这可以确保类型不会被衰减。
还有更多...
所有boost::typeindex::type_id*
函数返回boost::typeindex::type_index
的实例。它与std::type_index
非常接近;另外,它还有一个raw_name()
方法用于获取原始类型名称,以及一个pretty_name()
用于获取可读的类型名称。
即使在 C++17 中,std::type_index
和std::type_info
返回的是平台特定的类型名称表示,而这些表示相当难以解码或在可移植性上使用。
与标准库的typeid()
不同,Boost.TypeIndex
的一些类可用于 constexpr。这意味着如果你使用特定的boost::typeindex::ctti_type_index
类,你可以在编译时获取类型的文本表示。
用户可以使用Boost.TypeIndex
库发明自己的 RTTI 实现。这对于嵌入式开发人员和需要针对特定类型进行极其高效的 RTTI 的应用程序非常有用。
另请参阅
高级特性和更多示例的文档可在boost.org/libs/type_index
找到。
使用 C++11 移动模拟
C++11 标准的最大特点之一是右值引用。这个特性允许我们修改临时对象,从中窃取资源。你可以猜到,C++03 标准没有右值引用,但是使用Boost.Move
库,你可以编写一个模拟它们的可移植代码。
准备就绪
强烈建议您至少熟悉 C++11 右值引用的基础知识。
如何做...
- 假设你有一个类,其中包含多个字段,其中一些是标准库容器:
namespace other {
class characteristics{};
}
struct person_info {
std::string name_;
std::string second_name_;
other::characteristics characteristic_;
// ...
};
-
现在是时候为其添加移动赋值和移动构造函数了!只需记住,在 C++03 标准库中,容器既没有移动运算符也没有移动构造函数。
-
移动赋值的正确实现与移动构造对象并与
this
交换的方式相同。移动构造函数的正确实现接近于默认构造和swap
。因此,让我们从swap
成员函数开始:
#include <boost/swap.hpp>
void person_info::swap(person_info& rhs) {
name_.swap(rhs.name_);
second_name_.swap(rhs.second_name_);
boost::swap(characteristic_, rhs.characteristic_);
}
- 现在,在
private
部分放入以下宏:
BOOST_COPYABLE_AND_MOVABLE(person_info)
-
编写一个拷贝构造函数。
-
编写一个拷贝赋值,参数为:
BOOST_COPY_ASSIGN_REF(person_info)
。 -
编写一个
move
构造函数和一个移动赋值,参数为BOOST_RV_REF(person_info)
:
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)
: 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) {
person_info tmp(person);
swap(tmp);
return *this;
}
person_info& operator=(BOOST_RV_REF(person_info) person) {
person_info tmp(boost::move(person));
swap(tmp);
return *this;
}
void swap(person_info& rhs);
};
- 现在,我们有了
person_info
类的可移植快速实现的移动赋值和移动构造运算符。
工作原理...
以下是移动赋值的示例用法:
int main() {
person_info vasya;
vasya.name_ = "Vasya";
vasya.second_name_ = "Snow";
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 编译器时,所有用于模拟右值的宏都会扩展为 C++11 特定的特性,否则(在 C++03 编译器上),右值将被模拟。
还有更多...
你注意到了boost::swap
的调用吗?这是一个非常有用的实用函数,它首先在变量的命名空间中搜索swap
函数(在我们的示例中是other::
命名空间),如果没有匹配的交换函数,则使用std::swap
。
另请参阅
-
有关模拟实现的更多信息可以在 Boost 网站上找到,并且在
Boost.Move
库的源代码中找到boost.org/libs/move
。 -
Boost.Utility
库包含boost::swap
,并且拥有许多有用的函数和类。请参考boost.org/libs/utility
获取其文档。 -
在第二章的通过派生类的成员初始化基类食谱中,管理资源
-
创建一个不可复制类食谱。
-
在创建一个不可复制但可移动的类食谱中,有关
Boost.Move
的更多信息以及如何以便携和高效的方式在容器中使用可移动对象的一些示例。
创建一个不可复制的类
您几乎肯定遇到过某些情况,其中一个类拥有一些由于技术原因不能被复制的资源:
class descriptor_owner {
void* descriptor_;
public:
explicit descriptor_owner(const char* params);
~descriptor_owner() {
system_api_free_descriptor(descriptor_);
}
};
在前面的示例中,C++编译器生成了一个复制构造函数和一个赋值运算符,因此descriptor_owner
类的潜在用户将能够创建以下糟糕的事情:
void i_am_bad() {
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 {
// ...
现在,用户将无法做坏事:
void i_am_good() {
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
的复制构造函数和赋值运算符设为私有 -
定义它们而不实际实现
-
使用 C++11 语法
= delete;
显式删除它们
是的,你是正确的。根据你的编译器的能力,boost::noncopyable
类选择了使类不可复制的最佳方式。
boost::noncopyable
也可以作为您的类的良好文档。它永远不会引发诸如“复制构造函数体在其他地方定义吗?”或“它有一个非标准的复制构造函数(带有非 const 引用参数)吗?”等问题。
另请参阅
-
创建一个不可复制但可移动的类食谱将为您提供如何通过移动来允许在 C++03 中独占资源的想法
-
您可以在
Boost.Core
库的官方文档boost.org/libs/core
中找到许多有用的函数和类 -
在第二章的通过派生类的成员初始化基类食谱中,管理资源
-
使用 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);
}
然而,这样的变通方法不允许我们在容器中使用descriptor_owner
。顺便说一句,这看起来很糟糕!
准备工作
强烈建议您至少熟悉 C++11 右值引用的基础知识。阅读使用 C++11 移动模拟食谱也是推荐的。
如何做...
那些使用 C++11 的读者已经知道移动唯一类(如std::unique_ptr
或std::thread
)。使用这种方法,我们可以创建一个仅移动的descriptor_owner
类:
class descriptor_owner1 {
void* descriptor_;
public:
descriptor_owner1()
: descriptor_(nullptr)
{}
explicit descriptor_owner1(const char* param);
descriptor_owner1(descriptor_owner1&& param)
: descriptor_(param.descriptor_)
{
param.descriptor_ = nullptr;
}
descriptor_owner1& operator=(descriptor_owner1&& param) {
descriptor_owner1 tmp(std::move(param));
std::swap(descriptor_, tmp.descriptor_);
return *this;
}
void clear() {
free(descriptor_);
descriptor_ = nullptr;
}
bool empty() const {
return !descriptor_;
}
~descriptor_owner1() {
clear();
}
};
// GCC compiles the following in C++11 and later modes.
descriptor_owner1 construct_descriptor2() {
return descriptor_owner1("Construct using this string");
}
void foo_rv() {
std::cout << "C++11n";
descriptor_owner1 desc;
desc = construct_descriptor2();
assert(!desc.empty());
}
这只适用于 C++11 兼容的编译器。这是Boost.Move
的正确时机!让我们修改我们的示例,以便在 C++03 编译器上使用。
根据文档,要以便携的语法编写一个可移动但不可复制的类型,我们需要遵循这些简单的步骤:
- 将
BOOST_MOVABLE_BUT_NOT_COPYABLE(classname)
宏放在private
部分:
#include <boost/move/move.hpp>
class descriptor_owner_movable {
void* descriptor_;
BOOST_MOVABLE_BUT_NOT_COPYABLE(descriptor_owner_movable
- 编写一个移动构造函数和一个移动赋值,将参数作为
BOOST_RV_REF(classname)
:
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
) BOOST_NOEXCEPT
: descriptor_(param.descriptor_)
{
param.descriptor_ = NULL;
}
descriptor_owner_movable& operator=(
BOOST_RV_REF(descriptor_owner_movable) param) BOOST_NOEXCEPT
{
descriptor_owner_movable tmp(boost::move(param));
std::swap(descriptor_, tmp.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>
#include <your_project/descriptor_owner_movable.h>
int main() {
// 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 标准库容器仍然无法使用它(这就是为什么我们在前面的示例中使用了来自Boost.Containers
的 vector)。
还有更多...
如果您想在 C++03 编译器上使用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
// Contains BOOST_NO_CXX11_RVALUE_REFERENCES macro.
#include <boost/config.hpp>
#if !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_CXX11_RVALUE_REFERENCES)
#endif // YOUR_PROJECT_VECTOR_HPP
现在,您可以包含<your_project/vector.hpp>
并使用命名空间your_project_namespace
中的向量:
int main() {
your_project_namespace::vector<descriptor_owner_movable> v;
v.resize(10);
v.push_back(construct_descriptor3());
v.back() = boost::move(v.front());
}
但是,要注意编译器和标准库实现特定的问题!例如,只有在 GCC 4.7 的 C++11 模式下,如果您使用noexcept
或BOOST_NOECEPT
标记移动构造函数、析构函数和移动赋值运算符,此代码才会编译。
参见
-
第十章中的C++11 中减少代码大小和增加用户定义类型性能食谱提供了有关
noexcept
和BOOST_NOEXCEPT
的更多信息。 -
有关
Boost.Move
的更多信息可以在 Boost 的网站上找到boost.org/libs/move.
使用 C++14 和 C++11 算法
C++11 在<algorithm>
头文件中有一堆新的酷算法。C++14 有更多的算法。如果您被困在 C++11 之前的编译器上,您必须从头开始编写这些算法。例如,如果您希望输出从 65 到 125 的字符编码点,您必须在 C++11 之前的编译器上编写以下代码:
#include <boost/array.hpp>
boost::array<unsigned char, 60> chars_65_125_pre11() {
boost::array<unsigned char, 60> res;
const unsigned char offset = 65;
for (std::size_t i = 0; i < res.size(); ++i) {
res[i] = i + offset;
}
return res;
}
准备工作
本食谱需要基本的 C++知识以及对Boost.Array
库的基本了解。
如何做...
Boost.Algorithm
库具有所有新的 C++11 和 C++14 算法。使用它,您可以按照以下方式重写前面的示例:
#include <boost/algorithm/cxx11/iota.hpp>
#include <boost/array.hpp>
boost::array<unsigned char, 60> chars_65_125() {
boost::array<unsigned char, 60> res;
boost::algorithm::iota(res.begin(), res.end(), 65);
return res;
}
工作原理...
您可能已经知道,Boost.Algorithm
为每个算法都有一个头文件。只需包含头文件并使用所需的函数。
还有更多...
拥有一个仅实现 C++标准算法的库是无聊的。那不是创新的;那不是 Boost 的方式!这就是为什么在Boost.Algorithm
中,您可以找到不是 C++一部分的函数。例如,这里有一个将输入转换为十六进制表示的函数:
#include <boost/algorithm/hex.hpp>
#include <iterator>
#include <iostream>
void to_hex_test1() {
const std::string data = "Hello word";
boost::algorithm::hex(
data.begin(), data.end(),
std::ostream_iterator<char>(std::cout)
);
}
前面的代码输出如下:
48656C6C6F20776F7264
更有趣的是,所有函数都有额外的重载,接受范围作为第一个参数,而不是两个迭代器。Range是Ranges TS的概念。具有.begin()
和.end()
函数的数组和容器满足范围概念。有了这个知识,前面的示例可以被缩短:
#include <boost/algorithm/hex.hpp>
#include <iterator>
#include <iostream>
void to_hex_test2() {
const std::string data = "Hello word";
boost::algorithm::hex(
data,
std::ostream_iterator<char>(std::cout)
);
}
C++17 将具有来自Boost.Algorithm
的搜索算法。Boost.Algorithm
库将很快扩展为具有新算法和 C++20 功能,如可用的 constexpr 算法。密切关注该库,因为有一天,它可能会为您正在处理的问题提供现成的解决方案。
参见
-
Boost.Algorithm
的官方文档包含了所有函数的完整列表以及它们的简短描述,网址为boost.org/libs/algorithm
第二章:管理资源
在本章中,我们将涵盖以下主题:
-
管理不离开作用域的类的本地指针
-
对跨函数使用的类指针进行引用计数
-
管理不离开作用域的数组的本地指针
-
对跨函数使用的数组指针进行引用计数
-
在变量中存储任何功能对象
-
在变量中传递函数指针
-
在变量中传递 C++11 lambda 函数
-
指针的容器
-
在作用域退出时执行!
-
通过派生类的成员初始化基类
介绍
在本章中,我们将继续处理 Boost 库引入的数据类型,主要关注指针的处理。我们将看到如何轻松管理资源,如何使用能够存储任何功能对象、函数和 lambda 表达式的数据类型。阅读完本章后,你的代码将变得更加可靠,内存泄漏将成为历史。
管理不离开作用域的类的本地指针
有时,我们需要动态分配内存并在该内存中构造一个类。问题就出在这里。看一下以下代码:
bool foo1() {
foo_class* p = new foo_class("Some data");
const 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
不会被删除。让我们以以下方式修复它:
bool foo2() {
foo_class* p = new foo_class("Some data");
try {
const 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() {
const boost::scoped_ptr<foo_class> p(new foo_class("Some data"));
const bool something_else_happened = some_function1(*p);
if (something_else_happened) {
return false;
}
some_function2(p.get());
return true;
}
现在,资源不会泄漏,源代码也更清晰。
如果你可以控制some_function2(foo_class*)
,你可能希望将其重写为接受foo_class
的引用而不是指针。具有引用的接口比具有指针的接口更直观,除非你的公司有一个特殊的约定,即输出参数只能通过指针传递。
顺便说一句,Boost.Move
还有一个boost::movelib::unique_ptr
,你可以用它来代替boost::scoped_ptr
:
#include <boost/move/make_unique.hpp>
bool foo3_1() {
const boost::movelib::unique_ptr<foo_class> p
= boost::movelib::make_unique<foo_class>("Some data");
const bool something_else_happened = some_function1(*p);
if (something_else_happened) {
return false;
}
some_function2(p.get());
return true;
}
它是如何工作的...
boost::scoped_ptr<T>
和boost::movelib::unique_ptr
是典型的RAII类。当抛出异常或变量超出作用域时,堆栈被展开并调用析构函数。在析构函数中,scoped_ptr<T>
和unique_ptr<T>
调用delete
来删除它们存储的指针。因为这两个类默认调用delete
,所以如果基类的析构函数是虚拟的,通过指向base
类的指针持有derived
类是安全的:
#include <iostream>
#include <string>
struct base {
virtual ~base(){}
};
class derived: public base {
std::string str_;
public:
explicit derived(const char* str)
: str_(str)
{}
~derived() /*override*/ {
std::cout << "str == " << str_ << '\n';
}
};
void base_and_derived() {
const boost::movelib::unique_ptr<base> p1(
boost::movelib::make_unique<derived>("unique_ptr")
);
const boost::scoped_ptr<base> p2(
new derived("scoped_ptr")
);
}
运行base_and_derived()
函数将产生以下输出:
str == scoped_ptr
str == unique_ptr
在 C++中,对象的析构函数是按照相反的构造顺序调用的。这就是为什么在scoped_ptr
的析构函数之前调用了unique_ptr
的析构函数。
boost::scoped_ptr<T>
类模板既不可复制也不可移动。boost::movelib::unique_ptr
类是一个只能移动的类,并且在 C++11 之前的编译器上使用移动模拟。这两个类都存储指向它们拥有的资源的指针,并且不要求T
是一个完整类型(T
可以被前向声明)。
有些编译器在删除不完整类型时不会发出警告,这可能导致难以检测的错误。幸运的是,Boost 类具有特定的编译时断言来处理这种情况。这使得scoped_ptr
和unique_ptr
非常适合实现Pimpl习惯用法:
// In header file:
struct public_interface {
// ...
private:
struct impl; // Forward declaration.
boost::movelib::unique_ptr<impl> impl_;
};
还有更多...
这些类非常快。编译器会将使用scoped_ptr
和unique_ptr
的代码优化为机器代码,与手动编写的内存管理代码相比,几乎没有额外的开销。
C++11 有一个std::unique_ptr<T, D>
类,它独占资源,并且与boost::movelib::unique_ptr<T, D>
的行为完全相同。
C++标准库没有boost::scoped_ptr<T>
,但您可以使用const std::unique_ptr<T>
代替。唯一的区别是boost::scoped_ptr<T>
仍然可以调用reset()
,而const std::unique_ptr<T>
不行。
另请参阅
-
Boost.SmartPtr
库的文档包含了许多关于所有智能指针类的示例和其他有用信息。您可以在boost.org/libs/smart_ptr
上阅读有关它们的信息。 -
如果您使用
boost::movelib::unique_ptr
进行移动模拟,Boost.Move
文档可能会帮助您boost.org/libs/move
。
跨函数使用的类指针的引用计数
假设您有一些包含数据的动态分配的结构,并且您希望在不同的执行线程中处理它。要执行此操作的代码如下:
#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 'Parallel execution of different tasks'
// 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
,因为它仍然可以被运行process
函数的线程使用。这些process
函数不能删除p
,因为它们不知道其他线程不再使用它。
准备工作
此示例使用Boost.Thread
库,这不是一个仅头文件的库。您的程序必须链接到boost_thread
、boost_chrono
和boost_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
类内部有一个原子引用计数器。当您复制它时,引用计数会增加,当调用其析构函数
时,引用计数会减少。当引用计数等于零时,将为shred_ptr
指向的对象调用delete
。
现在,让我们看看在boost::thread (boost::bind(&process_sp1, p))
的情况下发生了什么。函数process_sp1
以引用参数作为参数,那么当我们退出while
循环时为什么它不会被释放?答案很简单。bind()
返回的函数对象包含shared
指针的副本,这意味着p
指向的数据直到函数对象被销毁才会被释放。函数对象被复制到线程中,并在线程执行时保持活动状态。
回到boost::make_shared
,让我们看看shared_ptr<std::string> ps(new int(0))
。在这种情况下,我们有两个调用new
:
-
通过
new int(0)
构造一个指向整数的指针 -
在构造
shared_ptr
类内部引用计数器分配在堆上
使用make_shared<T>
只需一次调用new
。make_shared<T>
分配一个单一的内存块,并在该内存块中构造原子计数器和T
对象。
还有更多...
原子引用计数器保证了shared_ptr
在线程之间的正确行为,但您必须记住原子操作不如非原子操作快。shared_ptr
在赋值、复制构造和未移动的shared_ptr
销毁时会触及原子变量。这意味着在兼容 C++11 的编译器上,您可以尽可能使用移动构造和移动赋值来减少原子操作的次数。如果您不打算再使用p
变量,只需使用shared_ptr<T> p1(std::move(p))
。如果您不打算修改指向的值,建议将其设置为const
。只需将const
添加到智能指针的模板参数中,编译器将确保您不会修改内存:
void process_cstr1(boost::shared_ptr<const std::string> p);
void process_cstr2(const boost::shared_ptr<const std::string>& p);
void foo3_const() {
boost::shared_ptr<const std::string> ps
= boost::make_shared<const std::string>(
"Some immutable string"
);
boost::thread(boost::bind(&process_cstr1, ps))
.detach();
boost::thread(boost::bind(&process_cstr2, ps))
.detach();
// *ps = "qwe"; // Compile time error, string is const!
}
对const
感到困惑?以下是智能指针 constness 到简单指针 constness 的映射:
shared_ptr<T> |
T* |
---|---|
shared_ptr<const T> |
const T* |
const shared_ptr<T> |
T* const |
const shared_ptr<const T> |
const T* const |
shared_ptr
调用和make_shared
函数是 C++11 的一部分,它们在std::
命名空间的头文件<memory>
中声明。它们几乎具有与 Boost 版本相同的特性。
另请参阅
-
有关
Boost.Thread
和原子操作的更多信息,请参阅第五章, 多线程。 -
有关如何绑定和重新排序函数参数的信息,请参阅第一章的开始编写您的应用程序中的绑定和重新排序函数参数食谱,了解更多关于
Boost.Bind
的信息。 -
有关如何将
shared_ptr<U>
转换为shared_ptr<T>
的信息,请参阅第三章的转换智能指针。 -
Boost.SmartPtr
库的文档包含了许多关于所有智能指针类的示例和其他有用信息。请参阅链接boost.org/libs/smart_ptr
了解相关内容。
管理不离开作用域的数组指针
我们已经看到如何在管理不离开作用域的类的指针食谱中管理指向资源的指针。但是,当我们处理数组时,我们需要调用delete[]
而不是简单的delete
。否则,将会发生内存泄漏。请看下面的代码:
void may_throw1(char ch);
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];
// Oops. Here comes some code, that may throw.
// It was a bad idea to use raw pointer as the memory may leak!!
may_throw1(buffer[0]);
may_throw2(buffer);
delete[] buffer;
}
准备就绪
此食谱需要了解 C++异常和模板的知识。
如何做...
Boost.SmartPointer
库不仅有scoped_ptr<>
类,还有scoped_array<>
类:
#include <boost/scoped_array.hpp>
void foo_fixed() {
// We allocate array 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[0]);
may_throw2(buffer.get());
// destructor of 'buffer' variable will call delete[]
}
Boost.Move
库的boost::movelib::unique_ptr<>
类也可以与数组一起使用。您只需要在模板参数的末尾提供[]
来指示它存储的是数组。
#include <boost/move/make_unique.hpp>
void foo_fixed2() {
// We allocate array on heap
const boost::movelib::unique_ptr<char[]> buffer
= boost::movelib::make_unique<char[]>(1024 * 1024 * 10);
// Here comes some code, that may throw,
// but now exception won't cause a memory leak
may_throw1(buffer[0]);
may_throw2(buffer.get());
// destructor of 'buffer' variable will call delete[]
}
工作原理...
scoped_array<>
的工作原理与scoped_ptr<>
类完全相同,但在析构函数中调用delete[]
而不是delete
。unique_ptr<T[]>
也是这样做的。
还有更多...
scoped_array<>
类与scoped_ptr<>
具有相同的保证和设计。它既没有额外的内存分配,也没有虚函数的调用。它不能被复制,也不是 C++11 的一部分。std::unique_ptr<T[]>
是 C++11 的一部分,具有与boost::movelib::unique_ptr<T[]>
类相同的保证和性能。
实际上,make_unique<char[]>(1024)
与new char[1024]
不同,因为第一个进行值初始化,而第二个进行默认初始化。默认初始化的等效函数是boost::movelib::make_unique_definit
。
请注意,Boost 版本也可以在 C++11 之前的编译器上工作,甚至在这些编译器上模拟 rvalues,使boost::movelib::unique_ptr
成为仅移动类型。如果您的标准库没有提供std::make_unique
,那么Boost.SmartPtr
可能会帮助您。它提供了boost::make_unique
,在头文件boost/smart_ptr/make_unique.hpp
中返回一个std::unique_ptr
。它还提供了boost::make_unique_noinit
,用于在相同的头文件中进行默认初始化。C++17 没有make_unique_noinit
函数。
在 C++中使用new
进行内存分配和手动内存管理是一种不好的习惯。尽可能使用make_unique
和make_shared
函数。
另请参阅
-
Boost.SmartPtr
库的文档包含了许多关于所有智能指针类的示例和其他有用信息,您可以在boost.org/libs/smart_ptr.
上阅读相关内容。 -
如果您希望使用
boost::movelib::unique_ptr
进行移动模拟,Boost.Move
文档可能会对您有所帮助,请阅读boost.org/libs/move.
引用计数的指向跨函数使用的数组的指针
我们继续处理指针,我们的下一个任务是对数组进行引用计数。让我们看一下从流中获取一些数据并在不同的线程中处理它的程序。代码如下:
#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();
boost::thread(boost::bind(&do_process, data_cpy, size))
.detach();
// Oops!!! We cannot delete[] data_cpy, because
// do_process() function may still work with it.
}
与跨函数使用类指针的引用计数示例中发生的相同问题。
准备工作
这个示例使用了Boost.Thread
库,这不是一个仅包含头文件的库,所以你的程序需要链接boost_thread
、boost_chrono
和boost_system
库。在继续阅读之前,请确保你理解了线程的概念。
你还需要一些关于boost::bind
或std::bind
的基本知识,它们几乎是一样的。
如何做...
有四种解决方案。它们之间的主要区别在于data_cpy
变量的类型和构造方式。所有这些解决方案都完全做了本示例开头描述的相同的事情,但没有内存泄漏。这些解决方案如下:
- 第一个解决方案适用于在编译时已知数组大小的情况:
#include <boost/shared_ptr.hpp>
#include <boost/make_shared.hpp>
template <std::size_t Size>
void do_process_shared(const boost::shared_ptr<char[Size]>& data);
template <std::size_t Size>
void do_process_in_background_v1(const char* data) {
// Same speed as in 'First solution'.
boost::shared_ptr<char[Size]> data_cpy
= boost::make_shared<char[Size]>();
std::memcpy(data_cpy.get(), data, Size);
// Starting threads of execution to process data.
boost::thread(boost::bind(&do_process_shared<Size>, data_cpy))
.detach();
boost::thread(boost::bind(&do_process_shared<Size>, data_cpy))
.detach();
// data_cpy destructor will deallocate data when
// reference count is 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);
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 threads of execution to process data.
boost::thread(boost::bind(&do_process_shared_ptr, data_cpy, size))
.detach();
boost::thread(boost::bind(&do_process_shared_ptr, data_cpy, size))
.detach();
// data_cpy destructor will deallocate data when
// reference count is zero.
}
- 第三个解决方案:
#include <boost/shared_ptr.hpp>
void do_process_shared_ptr2(
const boost::shared_ptr<char>& data,
std::size_t 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();
boost::thread(boost::bind(&do_process_shared_ptr2, data_cpy, size))
.detach();
// data_cpy destructor will deallocate data when
// reference count is zero.
}
- 最后一个解决方案自 Boost 1.65 以来已经被弃用,但在古老的 Boost 版本中可能会有用:
#include <boost/shared_array.hpp>
void do_process_shared_array(
const boost::shared_array<char>& data,
std::size_t size);
void do_process_in_background_v4(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_process_shared_array, data_cpy, size)
).detach();
boost::thread(
boost::bind(&do_process_shared_array, data_cpy, size)
).detach();
// No need to call delete[] for data_cpy, because
// data_cpy destructor will deallocate data when
// reference count is zero.
}
它是如何工作的...
在所有示例中,智能指针类计算引用并在引用计数变为零时调用delete[]
释放指针。第一个和第二个示例很简单。在第三个示例中,我们为shared
指针提供了一个自定义的deleter
对象。当智能指针决定释放资源时,智能指针的deleter
对象被调用。当智能指针在没有显式deleter
的情况下构造时,会构造默认的deleter
,它根据智能指针的模板类型调用delete
或delete[]
。
还有更多...
第四个解决方案是最保守的,因为在 Boost 1.53 之前,第二个解决方案的功能没有在shared_ptr
中实现。第一个和第二个解决方案是最快的,因为它们只使用了一次内存分配调用。第三个解决方案可以与较旧版本的 Boost 和 C++11 标准库的std::shared_ptr<>
一起使用(只需不要忘记将boost::checked_array_deleter<T>()
更改为std::default_delete<T[]>()
)。
实际上,boost::make_shared<char[]>(size)
并不等同于new char[size]
,因为它涉及到所有元素的值初始化。默认初始化的等效函数是boost::make_shared_noinit
。
注意!C++11 和 C++14 版本的std::shared_ptr
不能处理数组!只有在 C++17 中std::shared_ptr<T[]>
才能正常工作。如果你计划编写可移植的代码,请考虑使用boost::shared_ptr
、boost::shared_array
,或者显式地将deleter
传递给std::shared_ptr
。
boost::shared_ptr<T[]>
、boost::shared_array
和 C++17 的std::shared_ptr<T[]>
都有operator[](std::size_t index)
,允许你通过索引访问共享数组的元素。boost::shared_ptr<T>
和带有自定义deleter
的std::shared_ptr<T>
没有operator[]
,这使它们不太有用。
另请参阅
Boost.SmartPtr
库的文档包含了许多关于所有智能指针类的示例和其他有用信息。你可以在boost.org/libs/smart_ptr
上阅读相关内容。
将任何功能对象存储在变量中
考虑这样一种情况,当你开发一个库,它的 API 在头文件中声明,而在源文件中实现。这个库应该有一个接受任何功能对象的函数。看一下下面的代码:
// 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 {
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;
}
}
};
如何修改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);
}
它是如何工作的...
fobject_t
对象在自身中存储函数对象并擦除它们的确切类型。使用boost::function
来存储有状态的对象是安全的:
bool g_is_triggered = false;
void set_functional_object(fobject_t& f) {
// Local variable
int_processor fo( 100, 200, g_is_triggered);
f = fo;
// now 'f' holds a copy of 'fo'
// 'fo' leavs scope and will be destroyed,
// but it's OK to use 'f' in outer scope.
}
boost::function
是否记得boost::any
类?那是因为它使用相同的技术类型擦除来存储任何函数对象。
还有更多...
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
// ...
}
}
Boost.Function
库有大量的优化。它可以存储小型函数对象而无需额外的内存分配,并且具有优化的移动赋值运算符。它被接受为 C++11 标准库的一部分,并且在std::
命名空间的<functional>
头文件中定义。
boost::function
对存储在其中的对象使用 RTTI。如果禁用 RTTI,库将继续工作,但会大幅增加编译后的二进制文件的大小。
另请参阅
-
Boost.Function 的官方文档包含更多示例、性能测量和类参考文档。请参考链接
boost.org/libs/function
进行阅读。 -
在变量中传递函数指针教程。
-
在变量中传递 C++11 lambda 函数教程。
在变量中传递函数指针
我们将继续使用之前的示例,现在我们想在process_integers()
方法中传递一个函数指针。我们应该为函数指针添加一个重载,还是有更加优雅的方法?
准备工作
这个教程是前一个的延续。你必须先阅读前一个教程。
如何做...
不需要做任何事情,因为boost::function<>
也可以从函数指针中构造:
void my_ints_function(int i);
int main() {
process_integers(&my_ints_function);
}
它是如何工作的...
指向my_ints_function
的指针将被存储在boost::function
类中,并且对boost::function
的调用将被转发到存储的指针。
还有更多...
Boost.Function
库为函数指针提供了良好的性能,并且不会在堆上分配内存。标准库std::function
也有效地存储函数指针。自 Boost 1.58 以来,Boost.Function
库可以存储具有 rvalue 引用调用签名的函数和函数对象:
boost::function<int(std::string&&)> f = &something;
f(std::string("Hello")); // Works
另请参阅
-
Boost.Function 的官方文档包含更多示例、性能测量和类参考文档。请访问
boost.org/libs/function
进行阅读。 -
在变量中传递 C++11 lambda 函数教程。
在变量中传递 C++11 lambda 函数
我们将继续使用之前的示例,现在我们想在process_integers()
方法中使用一个 lambda 函数。
准备工作
这个教程是前两个教程的延续。你必须先阅读它们。你还需要一个兼容 C++11 的编译器,或者至少支持 C++11 lambda 的编译器。
如何做...
不需要做任何事情,因为boost::function<>
也可以用于任何难度的 lambda 函数:
#include <deque>
//#include "your_project/process_integers.h"
void sample() {
// lambda function with no parameters that does nothing
process_integers([](int /*i*/){});
// lambda function that stores a reference
std::deque<int> ints;
process_integers(&ints{
ints.push_back(i);
});
// lambda function that modifies its content
std::size_t match_count = 0;
process_integers(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
才会分配堆内存。移动实例不会分配和释放内存。
请记住,boost::function
意味着对编译器的优化障碍。这意味着:
std::for_each(v.begin(), v.end(), [](int& v) { v += 10; });
通常由编译器优化得更好:
const boost::function<void(int&)> f0(
[](int& v) { v += 10; }
);
std::for_each(v.begin(), v.end(), f0);
这就是为什么在不真正需要时应该尽量避免使用Boost.Function
。在某些情况下,C++11 的auto
关键字可能更方便:
const auto f1 = [](int& v) { v += 10; };
std::for_each(v.begin(), v.end(), f1);
另请参阅
有关性能和Boost.Function
的其他信息,请访问官方文档页面www.boost.org/libs/function
。
指针容器
有这样的情况,当我们需要在容器中存储指针。示例包括:在容器中存储多态数据,强制在容器中快速复制数据,以及对容器中的数据操作有严格的异常要求。在这种情况下,C++程序员有以下选择:
- 在容器中存储指针并使用
delete
来处理它们的销毁:
#include <set>
#include <algorithm>
#include <cassert>
template <class T>
struct ptr_cmp {
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);
// ...
// Oops! Any exception in the above code leads to
// memory leak.
// Deallocating resources.
std::for_each(s.begin(), s.end(), [](int* p) { delete p; });
}
这种方法容易出错,需要大量编写。
- 在容器中存储 C++11 智能指针:
#include <memory>
#include <set>
void example2_cpp11() {
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 中使用,而且您仍然需要编写一个比较器函数对象。
C++14 有一个std::make_unique
函数用于构造std::uniue_ptr
。使用它而不是new
是一个很好的编码风格!
- 在容器中使用
Boost.SmartPtr
:
#include <boost/shared_ptr.hpp>
#include <boost/make_shared.hpp>
void example3() {
typedef boost::shared_ptr<int> int_sptr_t;
std::set<int_sptr_t, ptr_cmp<int> > s;
s.insert(boost::make_shared<int>(1));
s.insert(boost::make_shared<int>(0));
// ...
assert(**s.begin() == 0);
// ...
// Resources will be deallocated by shared_ptr<>.
}
这种解决方案是可移植的,但会增加性能损失(原子计数器需要额外的内存,并且其增量/减量不如非原子操作快),而且您仍然需要编写比较器。
准备工作
更好地理解本配方需要了解标准库容器的知识。
如何做...
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);
中不需要额外的解引用)。
还有更多...
当我们想要克隆一些数据时,我们需要在要克隆的对象的命名空间中定义一个独立的函数T*new_clone(const T& r)
。此外,如果您包含头文件<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>
#include <cassert>
void theres_more_example() {
// Creating vector of 10 elements with values 100
boost::ptr_vector<int> v;
int value = 100;
v.resize(10, &value); // Beware! No ownership of pointer!
assert(v.size() == 10);
assert(v.back() == 100);
}
C++标准库没有指针容器,但您可以使用std::unique_ptr
的容器来实现相同的功能。顺便说一句,自 Boost 1.58 以来,有一个boost::movelib::unique_ptr
类,可在 C++03 中使用。您可以将其与Boost.Container
库中的容器混合使用,以获得存储指针的 C++11 功能:
#include <boost/container/set.hpp>
#include <boost/move/make_unique.hpp>
#include <cassert>
void example2_cpp03() {
typedef boost::movelib::unique_ptr<int> int_uptr_t;
boost::container::set<int_uptr_t, ptr_cmp<int> > s;
s.insert(boost::movelib::make_unique<int>(1));
s.insert(boost::movelib::make_unique<int>(0));
// ...
assert(**s.begin() == 0);
}
并非所有开发人员都很了解 Boost 库。使用具有 C++标准库替代品的函数和类更加友好,因为开发人员通常更了解标准库的特性。因此,如果对您来说没有太大区别,请使用Boost.Container
与boost::movelib::unique_ptr
。
另请参阅
-
官方文档包含了每个类的详细参考,请访问链接
boost.org/libs/ptr_container
了解更多信息。 -
本章的前四个配方为您提供了一些智能指针使用的示例。
-
第九章,容器中的多个食谱描述了
Boost.Container
库的特性。查看该章节,了解有用的快速容器。
在作用域退出时执行它!
如果您处理的是 Java、C#或 Delphi 等语言,显然您正在使用try {} finally{}
结构。让我简要地描述一下这些语言结构的作用。
当程序通过返回或异常离开当前作用域时,finally
块中的代码将被执行。这种机制用作 RAII 模式的替代:
// Some pseudo code (suspiciously similar to Java code)
try {
FileWriter f = new FileWriter("example_file.txt");
// Some code that may throw 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++知识。了解在抛出异常时代码的行为将会很有帮助。
如何做...
C++使用 RAII 模式而不是try {} finally{}
结构。Boost.ScopeExit
库旨在允许用户在函数体内定义 RAII 包装器:
#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 outer 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_) { // Capturing `this` as 'this_'
this_->close(f);
} BOOST_SCOPE_EXIT_END
}
};
Boost.ScopeExit
库在堆上不分配额外的内存,也不使用虚函数。使用默认语法,不要定义BOOST_SCOPE_EXIT_CONFIG_USE_LAMBDAS
,否则将使用boost::function
来实现作用域退出,这可能会分配额外的内存并意味着一个优化障碍。您可以通过指定自定义的deleter
,使用boost::movelib::unique_ptr
或std::unique_ptr
来实现接近BOOST_SCOPE_EXIT
结果:
#include <boost/move/unique_ptr.hpp>
#include <cstdio>
void unique_ptr_example() {
boost::movelib::unique_ptr<std::FILE, int(*)(std::FILE*)> f(
std::fopen("example_file.txt", "w"), // open file
&std::fclose // specific deleter
);
// ...
}
如果您为BOOST_SCOPE_EXIT
编写了两个或更多类似的代码块,那么现在是时候考虑一些重构,并将代码移动到一个完全功能的 RAII 类中。
另请参阅
官方文档包含更多示例和用例。您可以在boost.org/libs/scope_exit.
上阅读相关内容
通过派生类的成员初始化基类
让我们看一个例子。我们有一个基类,它有虚函数,并且必须用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! It is still 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;
virtual void do_process() {
logger_t::member << "Fake processor processed!";
}
public:
fake_tasks_processor_fixed()
: logger_t()
, tasks_processor(logger_t::member)
{}
};
它是如何工作的...
直接基类在非静态数据成员之前进行初始化,并且按照它们在基类指定符列表中出现的顺序进行初始化。如果我们需要用something初始化基类B
,我们需要将something作为在B
之前声明的基类A
的一部分。换句话说,boost::base_from_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++标准库中没有base_from_member
。
另请参阅
-
Boost.Utility
库包含许多有用的类和函数;有关更多信息的文档,请访问boost.org/libs/utility
-
在第一章 开始编写您的应用程序中的制作不可复制的类示例中,包含了来自
Boost.Utility
的更多类的示例 -
此外,在第一章 开始编写您的应用程序中的使用 C++11 移动模拟示例中,包含了更多来自
Boost.Utility
的类的示例。
第三章:转换和转型
在本章中,我们将涵盖:
-
将字符串转换为数字
-
将数字转换为字符串
-
将数字转换为数字
-
将用户定义的类型转换为/从字符串
-
转换智能指针
-
转换多态对象
-
解析简单输入
-
解析复杂输入
介绍
现在,我们知道了一些基本的 Boost 类型,是时候了解数据转换函数了。在本章中,我们将看到如何将字符串、数字、指针和用户定义的类型相互转换,如何安全地转换多态类型,以及如何在 C++源文件中编写小型和大型解析器。
将字符串转换为数字
在 C++中将字符串转换为数字会让很多人感到沮丧,因为它们效率低下且不友好。看看如何将字符串100
转换为int
:
#include <sstream>
void sample1() {
std::istringstream iss("100");
int i;
iss >> i;
// ...
}
最好不要去想,之前的转换中发生了多少不必要的操作、虚函数调用、原子操作和内存分配。顺便说一句,我们不再需要iss
变量,但它会一直存在直到作用域结束。
C 方法也不好:
#include <cstdlib>
void sample2() {
char * end;
const int i = std::strtol ("100", &end, 10);
// ...
}
它是否将整个值转换为int
,还是在中途停止了?要理解这一点,我们必须检查end
变量的内容。之后,我们将有一个无用的end
变量一直存在直到作用域结束。而我们想要一个int
,但strtol
返回long int
。转换后的值是否适合int
?
做好准备
此配方只需要基本的 C++和标准库知识。
如何做...
Boost 中有一个库,可以帮助您应对令人沮丧的字符串到数字转换困难。它被称为Boost.LexicalCast
,包括一个boost::bad_lexical_cast
异常类和一些boost::lexical_cast
和boost::conversion::try_lexical_convert
函数:
#include <boost/lexical_cast.hpp>
void sample3() {
const int i = boost::lexical_cast<int>("100");
// ...
}
它甚至可以用于非零终止的字符串:
#include <boost/lexical_cast.hpp>
void sample4() {
char chars[] = {'x', '1', '0', '0', 'y' };
const int i = boost::lexical_cast<int>(chars + 1, 3);
assert(i == 100);
}
它是如何工作的...
boost::lexical_cast
函数接受字符串作为输入,并将其转换为三角括号中指定的类型。boost::lexical_cast
函数甚至会为您检查边界:
#include <boost/lexical_cast.hpp>
#include <iostream>
void sample5() {
try {
// short usually may not store values greater than 32767
const short s = boost::lexical_cast<short>("1000000");
assert(false); // Must not reach this line.
} catch (const boost::bad_lexical_cast& e) {
std::cout << e.what() << '\n';
}
}
前面的代码输出:
bad lexical cast: source type value could not be interpreted as target.
它还检查输入的正确语法,并在输入错误时抛出异常:
#include <boost/lexical_cast.hpp>
#include <iostream>
void sample6() {
try {
const int i = boost::lexical_cast<int>("This is not a number!");
assert(false); // Must not reach this line.
} catch (const boost::bad_lexical_cast& /*e*/) {}
}
自 Boost 1.56 以来,有一个boost::conversion::try_lexical_convert
函数,通过返回代码报告错误。它可能在性能关键的地方很有用,那里经常会出现错误的输入:
#include <boost/lexical_cast.hpp>
#include <cassert>
void sample7() {
int i = 0;
const bool ok = boost::conversion::try_lexical_convert("Bad stuff", i);
assert(!ok);
}
还有更多...
lexical_cast
,就像所有std::stringstream
类一样,使用std::locale
并且可以转换本地化数字,但也具有对C 语言环境和没有数字分组的环境的令人印象深刻的优化集:
#include <boost/lexical_cast.hpp>
#include <locale>
void sample8() {
try {
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);
std::locale::global(std::locale::classic()); // Restoring C locale
} catch (const std::runtime_error&) { /* locale is not supported */ }
}
C++标准库没有lexical_cast
,但自 C++17 以来有std::from_chars
函数,可以用于创建惊人快速的通用转换器。请注意,这些转换器根本不使用区域设置,因此它们具有略有不同的功能。std::from_chars
函数旨在不分配内存,不抛出异常,并且没有原子或其他繁重的操作。
另请参阅
-
有关
boost::lexical_cast
性能的信息,请参阅将数字转换为字符串配方。 -
Boost.LexicalCast
的官方文档包含一些示例、性能测量和常见问题的答案。它可以在boost.org/libs/lexical_cast
上找到。
将数字转换为字符串
在这个配方中,我们将继续讨论词法转换,但现在我们将使用Boost.LexicalCast
将数字转换为字符串。与往常一样,boost::lexical_cast
将提供一种非常简单的方法来转换数据。
做好准备
此配方只需要基本的 C++和标准库知识。
如何做...
让我们使用boost::lexical_cast
将整数100
转换为std::string
:
#include <cassert>
#include <boost/lexical_cast.hpp>
void lexical_cast_example() {
const std::string s = boost::lexical_cast<std::string>(100);
assert(s == "100");
}
与传统的 C++转换方法进行比较:
#include <cassert>
#include <sstream>
void cpp_convert_example() {
std::stringstream ss; // Slow/heavy default constructor.
ss << 100;
std::string s;
ss >> s;
// Variable 'ss' will dangle all the way, till the end
// of scope. Multiple virtual methods and heavy
// operations were called during the conversion.
assert(s == "100");
}
以及与 C 转换方法相比:
#include <cassert>
#include <cstdlib>
void c_convert_example() {
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.
const std::string s = buffer;
// Now we have a 'buffer' variable that is not 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
的实现)。
C++11 有std::to_string
和std::to_wstring
函数,它们声明在<string>
头文件中。这些函数使用区域设置,并且行为非常接近boost::lexical_cast<std::string>
和boost::lexical_cast<std::wstring>
。C++17 有std::to_chars
函数,可以将数字转换为字符数组,速度非常快。std::to_chars
不分配内存,不抛出异常,并且可以使用错误代码报告错误。如果需要真正快速的转换函数而不使用区域设置,那么使用std::to_chars
。
另请参阅
-
Boost 官方文档包含了比较
lexical_cast
性能与其他转换方法的表格。在大多数情况下,lexical_cast
比其他方法更快boost.org/libs/lexical_cast
。 -
将字符串转换为数字食谱。
-
将用户定义的类型转换为/从字符串食谱。
将数字转换为数字
您可能还记得写以下代码的情况:
void some_function(unsigned short param);
int foo();
void do_something() {
// Some compilers may warn, that int is being converted to
// unsigned short and that there is a possibility of loosing
// data.
some_function(foo());
}
通常,程序员会通过将unsigned short
数据类型隐式转换来忽略这些警告,就像以下代码片段中所示:
// Warning suppressed.
some_function(
static_cast<unsigned short>(foo())
);
但是,如果foo()
返回的数字不适合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 42;
}
准备工作
此食谱需要对 C++的基本知识。
如何做...
库Boost.NumericConversion
为这种情况提供了解决方案。只需用boost::numeric_cast
替换static_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 overflow.
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::numeric_cast
检查输入参数的值是否适合新类型,而不会丢失数据,并且如果在转换过程中丢失了数据,则会抛出异常。
Boost.NumericConversion
库有一个非常快的实现。它可以在编译时完成大量工作,例如,当转换为更宽范围的类型时,源将通过static_cast
转换为目标类型。
还有更多...
boost::numeric_cast
函数是通过boost::numeric::converter
实现的,可以调整使用不同的溢出、范围检查和舍入策略。但通常,numeric_cast
就是您需要的。
以下是一个小例子,演示了如何为boost::numeric::cast
制作我们自己的溢出处理程序:
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) {
typedef boost::numeric::conversion_traits<
TargetT, SourceT
> conv_traits;
typedef boost::numeric::converter <
TargetT,
SourceT,
conv_traits, // default conversion traits
mythrow_overflow_handler<SourceT, TargetT> // !!!
> converter;
return converter::convert(in);
}
以下是如何使用我们的自定义转换器:
void example_with_my_numeric_cast() {
short v = 0;
try {
v = my_numeric_cast<short>(100000);
} catch (const std::logic_error& e) {
std::cout << "It works! " << e.what() << std::endl;
}
}
前面的代码输出如下:
It works! Not in range!
即使 C++17 也没有提供安全的数字转换功能。然而,这方面的工作正在进行中。我们有望在 2020 年后看到这样的功能。
参见
Boost 的官方文档包含了对数字转换器模板参数的详细描述;可以在以下链接找到:boost.org/libs/numeric/conversion
将用户定义的类型转换为字符串和从字符串转换为数字
Boost.LexicalCast
中有一个功能,允许用户使用自己的类型进行lexical_cast
。这需要用户为该类型编写正确的std::ostream
和std::istream
操作符。
如何做...
- 你只需要提供
operator<<
和operator>>
流操作符。如果你的类已经可以进行流操作,就不需要做任何事情:
#include <iostream>
#include <stdexcept>
// Negative number that does not store minus sign.
class negative_number {
unsigned short number_;
public:
explicit negative_number(unsigned short number = 0)
: number_(number)
{}
// ...
unsigned short value_without_sign() const {
return number_;
}
};
inline std::ostream&
operator<<(std::ostream& os, const negative_number& num)
{
os << '-' << num.value_without_sign();
return os;
}
inline std::istream& operator>>(std::istream& is, negative_number& num)
{
char ch;
is >> ch;
if (ch != '-') {
throw std::logic_error(
"negative_number class stores ONLY negative values"
);
}
unsigned short s;
is >> s;
num = negative_number(s);
return is;
}
- 现在,我们可以使用
boost::lexical_cast
将negative_number
类转换为字符串和从字符串转换为negative_number
类。这里有一个例子:
#include <boost/lexical_cast.hpp>
#include <boost/array.hpp>
#include <cassert>
void example1() {
const negative_number n
= boost::lexical_cast<negative_number>("-100");
assert(n.value_without_sign() == 100);
const int i = boost::lexical_cast<int>(n);
assert(i == -100);
typedef boost::array<char, 10> arr_t;
const 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 stores ONLY negative values"
);
}
unsigned short s;
is >> s;
num = negative_number(s);
return is;
}
void example2() {
const negative_number n = boost::lexical_cast<negative_number>(L"-1");
assert(n.value_without_sign() == 1);
typedef boost::array<wchar_t, 10> warr_t;
const warr_t arr = boost::lexical_cast<warr_t>(n);
assert(arr[0] == L'-');
assert(arr[1] == L'1');
assert(arr[2] == 0);
}
Boost.LexicalCast
库不是 C++的一部分。许多 Boost 库都使用它,我希望它也能让你的生活变得更轻松。
参见
-
Boost.LexicalCast
文档包含了一些示例、性能测量和常见问题的解答;链接为boost.org/libs/lexical_cast
-
将字符串转换为数字的方法
-
将数字转换为字符串的方法
转换智能指针
这里有一个问题:
- 你有一个名为
some_class
的类:
struct base {
virtual void some_methods() = 0;
virtual ~base();
};
struct derived: public base {
void some_methods() /*override*/;
virtual void derived_method() const;
~derived() /*override*/;
};
- 你有一个第三方 API,返回通过共享指针构造的
derived
,并在其他函数中接受共享指针到const derived
:
#include <boost/shared_ptr.hpp>
boost::shared_ptr<const base> construct_derived();
void im_accepting_derived(boost::shared_ptr<const derived> p);
- 你必须让以下代码工作:
void trying_hard_to_pass_derived() {
boost::shared_ptr<const base> d = construct_derived();
// Oops! Compile time error:
// ‘const struct base; has no member named ‘derived_method;.
d->derived_method();
// Oops! Compile time error:
// could not convert ‘d; to ‘boost::shared_ptr<const derived>;.
im_accepting_derived(d);
}
如何以一种好的方式解决这个问题?
入门
这个配方需要对 C++和智能指针有基本的了解。
如何做...
解决方案是使用智能指针的特殊转换。在这种特殊情况下,我们需要使用dynamic_cast
功能,因此我们使用boost::dynamic_pointer_cast
:
void trying_hard_to_pass_derived2() {
boost::shared_ptr<const derived> d
= boost::dynamic_pointer_cast<const derived>(
construct_derived()
);
if (!d) {
throw std::runtime_error(
"Failed to dynamic cast"
);
}
d->derived_method();
im_accepting_derived(d);
}
它是如何工作的...
Boost 库有很多用于智能指针转换的函数。所有这些函数都接受一个智能指针和一个模板参数T
,其中T
是智能指针的所需模板类型。这些转换操作符模仿了内置转换的行为,同时正确管理引用计数和其他智能指针内部:
-
boost::static_pointer_cast<T>(p)
- 执行static_cast<T*>(p.get())
-
boost::dynamic_pointer_cast<T>(p)
- 执行dynamic_cast<T*>(p.get())
-
boost::const_pointer_cast<T>(p)
- 执行const_cast<T*>(p.get())
-
boost::reinterpret_pointer_cast<T>(p)
- 执行reinterpret_cast<T*>(p.get())
还有更多...
所有的boost::*_pointer_cast
函数都可以使用标准库和 C 指针的智能指针,只要包含<boost/pointer_cast.hpp>
。
在 C++11 中,标准库在<memory>
头文件中定义了std::static_pointer_cast
、std::dynamic_pointer_cast
和std::const_pointer_cast
,但只适用于std::shared_ptr
。C++17 标准库有std::reinterpret_pointer_cast
,但只适用于std::shared_ptr
。
参见
-
Boost.SmartPointer
库文档包含了有关标准库指针转换的更多示例,链接为boost.org/libs/smart_ptr/pointer_cast.html
-
boost::shared_ptr
的转换参考可在boost.org/libs/smart_ptr/shared_ptr.htm
上找到 -
本章中的Casting polymorphic objects食谱将向您展示进行动态转换的更好方法
转换多态对象
想象一下,某个程序员设计了一个如下糟糕的接口(这是接口不应该编写的一个很好的例子):
struct object {
virtual ~object() {}
};
struct banana: public object {
void eat() const {}
virtual ~banana(){}
};
struct penguin: public object {
bool try_to_fly() const {
return false; // penguins do not fly
}
virtual ~penguin(){}
};
object* try_produce_banana();
我们的任务是编写一个吃香蕉的函数,并在有其他东西代替香蕉时抛出异常(try_produce_banana()
可能返回nullptr
),因此如果我们在不检查的情况下解引用它返回的值,我们有可能解引用空指针。
入门
本食谱需要基本的 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
操作。这是一个在性能关键部分使用的好函数,仍然具有在调试编译中检测错误的能力。
自 Boost 1.58 以来,在 Boost.Conversion 库中有一个boost::polymorphic_pointer_downcast
和boost::polymorphic_pointer_cast
函数。这些函数允许您安全地转换智能指针,并具有与boost::polymorphic_cast
和boost::polymorphic_downcast
相同的特性。
C++标准库缺乏polymorphic_cast
和polymorphic_downcast
。
另请参阅
-
最初,
polymorphic_cast
的想法是在书籍The C++ Programming Language,Bjarne Stroustrup中提出的。有关更多信息和一些关于不同主题的好主意,请参考此书。 -
官方文档也可能有所帮助;可在
boost.org/libs/conversion
上找到。 -
有关转换智能指针的更多信息,请参考前一篇食谱。
解析简单输入
解析小文本是一项常见任务。这种情况总是一个两难选择:我们应该使用一些第三方专业和良好的解析工具,如 Bison 或 ANTLR,还是应该尝试仅使用 C++和标准库手动编写?第三方工具适用于处理复杂文本的解析,并且很容易使用它们编写解析器,但它们需要额外的工具来从它们的语法创建 C++或 C 代码,并为您的项目添加更多依赖项。
手写解析器通常难以维护,但除了 C++编译器外,不需要任何东西。
让我们从一个非常简单的任务开始,解析 ISO 格式的日期如下:
YYYY-MM-DD
以下是可能输入的示例:
2013-03-01
2012-12-31 // (woo-hoo, it almost a new year!)
我们需要从以下链接获取解析器的语法www.ietf.org/rfc/rfc333:
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 <cassert>
- 现在,是时候创建一个
date
结构来保存解析的数据了:
struct date {
unsigned short year;
unsigned short month;
unsigned short day;
};
- 让我们来看看解析器(如何工作的逐步说明可以在下一节中找到):
// See recipe "Type 'reference to string'" 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();
const bool success = boost::spirit::qi::parse(first, end,
// Implementation of 'full-date' rule from EBNF grammar.
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() {
const date d = parse_date_time1("2017-12-31");
assert(d.year == 2017);
assert(d.month == 12);
assert(d.day == 31);
}
它是如何工作的...
这是一个非常简单的实现;它不检查数字的位数。解析发生在boost::spirit::qi::parse
函数中。让我们简化一下,去掉成功解析时的动作:
const 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
。如果我们想确保整个字符串被成功解析,我们需要检查解析器的返回值以及end
和修改后的first
迭代器的相等性。
现在,我们需要处理成功解析时的动作,然后这个示例就结束了。Boost.Spirit
中的语义动作写在[]
中,可以使用函数指针、函数对象、boost::bind
、std::bind
(或其他bind()
实现)或 C++11 lambda 函数来编写。
因此,你也可以使用 C++11 lambda 为YYYY
编写规则:
const auto y = &res { res.year = s; };
// ...
ushort_[y] >> char_('-') >> // ...
你不能直接将 lambda 定义放在[]
中,因为 C++编译器会认为它是一个属性。作为一种解决方法,你可以在其中创建一个带有 lambda 函数的auto
变量,并在解析器规则描述中使用该变量(就像在前面的代码片段中所做的那样)。
现在,让我们更仔细地看一下月份的语义动作:
ushort_[ ref(res.month) = _1 ]
对于从头开始阅读本书的人来说,前面的代码提醒了boost::bind
、boost::ref
和占位符。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;
date res;
// 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_;
const char* first = s.data();
const char* const end = first + s.size();
const 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
的内部内容隐藏在该文件中。如果我们调整前面的例子来遵循这个规则,那么头文件将如下所示:
// Header file.
#ifndef MY_PROJECT_PARSE_DATE_TIME
#define MY_PROJECT_PARSE_DATE_TIME
#include <string>
struct date {
unsigned short year;
unsigned short month;
unsigned short day;
};
date parse_date_time2(const std::string& s);
#endif // MY_PROJECT_PARSE_DATE_TIME
还要注意传递给boost::spirit::parse
函数的迭代器类型。你使用的迭代器类型越少,二进制文件的大小就越小。
如果你现在认为手动使用标准库解析日期更简单,那么你是对的!但仅限于现在。看看下一个示例,它将为你提供更多关于Boost.Spirit
的用法,并扩展这个例子,以处理手动编写解析器比使用Boost.Spirit
更困难的情况。
Boost.Spirit
库不是 C++的一部分,并且在不久的将来也不会被提议包含在其中。但是,它与现代 C++特性非常兼容,因此如果您的编译器支持 C++11,请使用它们:
date parse_date_time2_cxx(const std::string& s) {
using boost::spirit::qi::uint_parser;
using boost::spirit::qi::char_;
date res;
uint_parser<unsigned short, 10, 2, 2> u2_;
uint_parser<unsigned short, 10, 4, 4> u4_;
const auto y = &res { res.year = s; };
const auto m = &res { res.month = s; };
const auto d = &res { res.day = s; };
const char* first = s.data();
const char* const end = first + s.size();
const bool success = boost::spirit::qi::parse(first, end,
u4_[y] >> char_('-') >> u2_[m] >> char_('-') >> u2_[d]
);
if (!success || first != end) {
throw std::logic_error("Parsing failed");
}
return res;
}
另请参阅
-
在第一章的开始编写您的应用程序中的重新排列函数参数教程。
-
将值绑定为函数参数教程。
-
Boost.Spirit
是一个庞大的仅头文件库。可以单独编写一本书来介绍它。请随时使用其文档boost.org/libs/spirit
。
解析复杂输入
在上一个教程中,我们编写了一个简单的日期解析器。想象一下,一些时间已经过去,任务已经改变。现在,我们需要编写一个支持多种输入格式和区域偏移的日期时间解析器。我们的解析器必须理解以下输入:
2012-10-20T10:00:00Z // date time with zero zone offset
2012-10-20T10:00:00 // date time without 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
准备工作
我们将使用在解析简单输入教程中描述的Boost.Spirit
库。在开始本教程之前,请先阅读该教程。
如何做...
- 让我们首先编写一个日期时间结构,用于保存解析的结果:
#include <stdexcept>
#include <cassert>
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 in datetime: " + 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: set_year(unsigned short), set_day(unsigned short), ...
//
// void set_*(unsigned short val) {
// Some dt_assert.
// Setting the '*_' to 'val'.
// }
// ...
};
- 现在,让我们编写一个设置区域偏移的函数:
void set_zone_offset(datetime& dt, char sign, unsigned short hours
, unsigned short minutes)
{
dt.set_zone_offset(
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 interates 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, &ret, datetime::OFFSET_Z
) ]
| // OR
// Specific zone offset
((char_('+')|char_('-')) >> u2_ >> ':' >> u2_) [
bind(&set_zone_offset, ref(ret), _1, _2, _3)
]
);
- 让我们通过编写剩余的解析器来完成我们的示例:
boost::spirit::qi::rule<const char*, void()> date_parser =
u4_ [ bind(&datetime::set_year, &ret, _1) ] >> '-'
>> u2_ [ bind(&datetime::set_month, &ret, _1) ] >> '-'
>> u2_ [ bind(&datetime::set_day, &ret, _1) ];
boost::spirit::qi::rule<const char*, void()> time_parser =
u2_ [ bind(&datetime::set_hours, &ret, _1) ] >> ':'
>> u2_ [ bind(&datetime::set_minutes, &ret, _1) ] >> ':'
>> u2_ [ bind(&datetime::set_seconds, &ret, _1) ];
const char* first = s.data();
const char* const end = first + s.size();
const bool success = boost::spirit::qi::parse(first, end,
(
(date_parser >> '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_ = /* ... a lot of parser code ... */;
}
请注意,这个类意味着编译器的优化障碍,因此在不需要时不要使用它。
有时我们使用>> ':'
而不是>> char_(':')
。第一种方法更有限:您无法将操作绑定到它,也无法通过组合字符来创建新规则(例如,您无法只使用char_
就写出char_('+')|char_('-')
)。但是为了更好的性能,请使用第一种方法,因为一些编译器可能会对其进行轻微优化。
还有更多...
通过删除进行类型擦除的rule<>
对象,可以使我们的示例稍微更快。只需用 C++11 的auto
关键字替换它们。
Boost.Spirit
库生成非常快速的解析器;官方网站上有一些性能指标。官方文档包含了编写更快解析器的高级建议。
使用boost::phoenix::bind
并不是强制的,但是如果不使用它,timezone_parser
中解析特定区域偏移的规则将会处理
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
库值得单独编写一本书,无法在几个教程中描述其所有特性,因此参考文档将帮助您获取更多信息。它可以在boost.org/libs/spirit
找到。在那里,您将找到更多示例、准备好的解析器,以及如何使用 Boost 直接在 C++11 代码中编写词法分析器和生成器的信息。
第四章:编译时技巧
在本章中,我们将涵盖以下内容:
-
在编译时检查大小
-
为整数类型启用函数模板使用
-
禁用真实类型的函数模板使用
-
从数字创建一个类型
-
实现一个类型特征
-
为模板参数选择最佳操作符
-
在 C++03 中获取表达式的类型
介绍
在本章中,我们将看到一些基本的例子,说明 Boost 库如何在编译时检查、调整算法和其他元编程任务中使用。
一些读者可能会问,"为什么我们要关心编译时的事情?"那是因为程序的发布版本只编译一次,运行多次。我们在编译时做的越多,运行时剩下的工作就越少,从而产生更快速和可靠的程序。运行时检查只有在执行带有检查的代码部分时才会执行。编译时检查将阻止您的程序编译,理想情况下会有有意义的编译器错误消息。
这一章可能是最重要的之一。如果没有理解 Boost 源码和其他类似 Boost 的库,是不可能的。
在编译时检查大小
假设我们正在编写一些序列化函数,将值存储在指定大小的缓冲区中:
#include <cstring>
#include <boost/array.hpp>
// C++17 has std::byte out of the box!
// Unfortunately this is as C++03 example.
typedef unsigned char byte_t;
template <class T, std::size_t BufSizeV>
void serialize_bad(const T& value, boost::array<byte_t, BufSizeV>& buffer) {
// TODO: check buffer size.
std::memcpy(&buffer[0], &value, sizeof(value));
}
这段代码有以下问题:
-
缓冲区的大小没有被检查,所以可能会溢出
-
这个函数可以用于非平凡可复制类型,这可能导致不正确的行为
我们可以通过添加一些断言来部分修复它,例如:
template <class T, std::size_t BufSizeV>
void serialize_bad(const T& value, boost::array<byte_t, BufSizeV>& buffer) {
// TODO: think of something better.
assert(BufSizeV >= sizeof(value));
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/has_trivial_copy.hpp>
template <class T, std::size_t BufSizeV>
void serialize(const T& value, boost::array<byte_t, BufSizeV>& buffer) {
BOOST_STATIC_ASSERT(BufSizeV >= sizeof(value));
BOOST_STATIC_ASSERT(boost::has_trivial_copy<T>::value);
std::memcpy(&buffer[0], &value, sizeof(value));
}
它是如何工作的...
BOOST_STATIC_ASSERT
宏只能在断言表达式可以在编译时评估并且可以隐式转换为bool
时使用。这意味着您只能在BOOST_STATIC_ASSERT
中使用sizeof()
、静态常量、constexpr 变量、在编译时已知参数的 constexpr 函数和其他常量表达式。如果断言表达式评估为false
,BOOST_STATIC_ASSERT
将停止编译。在serialize
函数的情况下,如果第一个静态断言失败,这意味着用户错误使用了serialize
函数并提供了一个非常小的缓冲区。
这里有一些更多的例子:
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));
template <class T1, class T2>
struct some_template {
BOOST_STATIC_ASSERT(sizeof(T1) == sizeof(T2));
};
如果BOOST_STATIC_ASSERT
宏的断言表达式中有逗号,我们必须用额外的括号将整个表达式包起来。
最后一个例子非常接近我们在serialize()
函数的第二行看到的内容。现在是时候更多地了解Boost.TypeTraits
库了。这个库提供了大量的编译时元函数,允许我们获取有关类型的信息并修改类型。元函数的用法看起来像boost::function_name<parameters>::value
或boost::function_name<parameters>::type
。元函数boost::has_trivial_copy<T>::value
只有在T
是一个简单可复制的类型时才返回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<byte_t, BufSizeV>& buf) {
BOOST_STATIC_ASSERT_MSG(boost::has_trivial_copy<T>::value,
"This serialize2 function may be used only "
"with trivially copyable types."
);
BOOST_STATIC_ASSERT_MSG(BufSizeV >= sizeof(value),
"Can not fit value to buffer. "
"Make the buffer bigger."
);
std::memcpy(&buf[0], &value, sizeof(value));
}
int main() {
// Somewhere in code:
boost::array<unsigned char, 1> buf;
serialize2(std::string("Hello word"), buf);
}
在 C++11 模式下,使用 g++ 编译器编译上述代码将得到以下结果:
boost/static_assert.hpp:31:45: error: static assertion failed: This serialize2 function may be used only with trivially copyable types.
# define BOOST_STATIC_ASSERT_MSG( ... ) static_assert(__VA_ARGS__)
^
Chapter04/01_static_assert/main.cpp:76:5: note: in expansion of macro ‘BOOST_STATIC_ASSERT_MSG;
BOOST_STATIC_ASSERT_MSG(boost::has_trivial_copy<T>::value,
^~~~~~~~~~~~~~~~~~~~~~~
boost/static_assert.hpp:31:45: error: static assertion failed: Can not fit value to buffer. Make the buffer bigger.
# define BOOST_STATIC_ASSERT_MSG( ... ) static_assert(__VA_ARGS__)
^
Chapter04/01_static_assert/main.cpp:81:5: note: in expansion of macro ‘BOOST_STATIC_ASSERT_MSG;
BOOST_STATIC_ASSERT_MSG(BufSizeV >= sizeof(value),
^~~~~~~~~~~~~~~~~~~~~~~
BOOST_STATIC_ASSSERT
,BOOST_STATIC_ASSSERT_MSG
或任何类型特征实体都不会导致运行时惩罚。所有这些函数都在编译时执行,不会向生成的二进制文件添加任何汇编指令。C++11 标准具有static_assert(condition, "message")
,它等效于 Boost 的 BOOST_STATIC_ASSSERT_MSG
。C++17 中提供了在编译时断言而无需用户提供消息的BOOST_STATIC_ASSERT
功能。您不必包含头文件即可使用编译器内置的static_assert
。
Boost.TypeTraits
库部分被接受到 C++11 标准中。因此,您可以在 std::
命名空间的 <type_traits>
头文件中找到特征。C++11 <type_traits>
具有一些在 Boost.TypeTraits
中不存在的函数,但是一些其他元函数只存在于 Boost 中。以has_
开头的元函数在标准库中被重命名为以is_
开头的元函数。因此,has_trivial_copy
变成了 is_trivially_copyable
等等。
C++14 和 Boost 1.65 为所有具有 ::type
成员的类型特征提供了快捷方式。这些快捷方式允许您编写 remove_const_t<T1>
而不是 typename remove_const<T1>::type
。请注意,在 Boost 1.65 的情况下,这些快捷方式需要一个兼容 C++11 的编译器,因为它们只能使用类型别名来实现:
template <class T>
using remove_const_t = typename remove_const<T>::type;
C++17 为具有 ::value
的类型特征添加了 _v
快捷方式。自 C++17 起,您可以只写 std::is_unsigned_v<T1>
而不是 std::is_unsigned<T1>::value
。这个技巧通常是使用变量模板
来实现的:
template <class T>
inline constexpr bool is_unsigned_v = is_unsigned<T>::value;
当 Boost 和标准库中存在类似的特征时,如果您正在编写必须在 C++11 之前的编译器上工作的项目,请选择 Boost 版本。否则,在极少数情况下,标准库版本可能效果稍好。
另请参阅
-
本章的下一个示例将为您提供更多示例和想法,说明静态断言和类型特征可以如何使用。
-
阅读
Boost.StaticAssert
的官方文档,了解更多示例:
为整数类型启用函数模板使用
这是一个常见的情况,当我们有一个实现某些功能的类模板时:
// 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_integral {
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_sse {
double process(double v1, double v2, double v3);
};
现在的问题是:如何使编译器自动为指定类型选择正确的类?
准备工作
本示例需要一些 C++ 模板知识。
如何做...
我们将使用 Boost.Core
和 Boost.TypeTraits
来解决这个问题:
- 让我们从包含头文件开始:
#include <boost/core/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)原则,该原则在模板实例化期间使用。这就是原则的工作方式;如果在函数或类模板的实例化过程中形成了无效的参数或返回类型,则该实例化将从重载解析集中移除,并且不会导致编译错误。现在棘手的部分是,boost::enable_if_c<true>
有一个通过 ::type
访问的成员类型,但 boost::enable_if_c<false>
没有 ::type
。让我们回到我们的解决方案,看看 SFINAE 如何与作为 T
参数传递给 data_processor
类的不同类型一起使用。
如果我们将 int
作为 T
类型传递,首先编译器将尝试从 步骤 3 实例化模板部分特化,然后再使用我们的非特定通用版本。当它尝试实例化一个 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
。
在 Boost 1.56 之前,boost::enable_if
元函数定义在头文件 <boost/utility/enable_if.hpp>
中,而不是 <boost/core/enable_if.hpp>
。
C++11 在 <type_traits>
头文件中定义了 std::enable_if
,它的行为与 boost::enable_if_c
完全相同。它们之间没有区别,只是 Boost 的版本也适用于非 C++11 编译器,提供更好的可移植性。
C++14 中有一个快捷方式 std::enable_if_t
,它必须在没有 typename
和 ::type
的情况下使用:
template <class T>
class data_processor<
T, std::enable_if_t<boost::is_float<T>::value >
>;
所有启用函数仅在编译时执行,不会在运行时增加性能开销。但是,添加额外的模板参数可能会在 typeid(your_class).name()
中产生更大的类名,并在某些平台上比较两个 typeid()
结果时增加极小的性能开销。
另请参阅
-
下一篇文章将为您提供更多关于
enable_if
使用的示例。 -
您还可以查阅
Boost.Core
的官方文档。其中包含许多示例和许多有用的类(在本书中广泛使用)。请访问链接boost.org/libs/core
了解更多信息。 -
您还可以阅读一些关于模板部分特化的文章
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
函数的一个优化版本,但只适用于具有T::operator+=(const T&)
的类型:
template <class T>
T process_data_plus_assign(const T& v1, const T& v2, const T& v3);
你有一个庞大的代码库,可能需要几个月的时间才能手动将process_data
更改为具有正确运算符的process_data_plus_assign
。因此,你不想改变已经编写的代码。相反,你希望强制编译器在可能的情况下自动使用优化函数来替代默认函数。
准备工作
阅读前面的配方,了解boost::enable_if_c
的作用,并理解 SFINAE 的概念。仍然需要基本的模板知识。
如何做...
可以使用 Boost 库进行模板魔术。让我们看看如何做:
- 我们将需要
boost::has_plus_assign<T>
元函数和<boost/enable_if.hpp>
头文件:
#include <boost/core/enable_if.hpp>
#include <boost/type_traits/has_plus_assign.hpp>
- 现在,我们禁用具有
plus assign
运算符的类型的默认实现:
// 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);
- 为具有
plus assign
运算符的类型启用优化版本:
// 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
返回。换句话说,boost::enable_if_c<true, T>::type
与T
相同。
让我们逐步进行process_data(i, i, i)
的案例。我们将int
作为T
类型传递,编译器搜索函数process_data(int, int, int)
。因为没有这样的函数,下一步是实例化process_data
的模板版本。然而,有两个模板process_data
函数。例如,编译器开始实例化我们的第二个(优化)版本的模板;在这种情况下,它成功地评估了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 规则,这不被视为错误。没有更多的模板process_data
函数,所以编译器停止实例化。如你所见,如果没有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 1.56 之前,boost::disable_if
元函数被定义在<boost/utility/enable_if.hpp>
头文件中,而不是<boost/core/enable_if.hpp>
。
在前面的配方中提到,所有的启用和禁用函数都只在编译时执行,并且不会在运行时增加性能开销。
另请参阅
-
从头开始阅读本章,以获取更多编译时技巧的示例。
-
考虑阅读
Boost.TypeTraits
官方文档,了解更多示例和元函数的完整列表,网址为boost.org/libs/type_traits
。 -
Boost.Core
库可能会为你提供更多关于boost::enable_if
的用法示例;在boost.org/libs/core
上了解更多信息。
从数字创建类型
我们已经看到了如何使用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
- 现在,我们准备编写一个处理函数:
// 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_<4>)
签名的函数,并从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++的一部分。然而,C++从该库中重用了许多技巧。C++11 在头文件type_traits
中有一个std::integral_constant<type, value>
类,可以像前面的示例中那样使用。您甚至可以使用它定义自己的类型别名:
template <int Value>
using int_ = std::integral_constant<int, Value>;
另请参阅
-
第八章的配方,“元编程”,将为您提供更多
Boost.MPL
库用法的示例。如果您感到自信,您也可以尝试阅读boost.org/libs/mpl
链接的库文档。 -
在
boost.org/libs/type_traits/doc/html/boost_typetraits/examples/fill.html
和boost.org/libs/type_traits/doc/html/boost_typetraits/examples/copy.html
上阅读标签用法的更多示例。
实现类型特性
我们需要实现一个类型特性,如果将std::vector
类型作为模板参数传递给它,则返回true
,否则返回false
。
准备工作
需要一些关于Boost.TypeTrait
或标准库类型特性的基本知识。
如何做...
让我们看看如何实现类型特性:
#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
库很好地配合。
我们的第一个is_stdvector
结构是一个通用结构,当找不到模板专门化版本时将始终使用它。我们的第二个is_stdvector
结构是std::vector
类型的模板专门化(注意它是从true_type
派生的)。因此,当我们将std::vector
类型传递给is_stdvector
结构时,编译器会选择模板专门化版本。如果我们传递的数据类型不是std::vector
,那么就会使用从false_type
派生的通用版本。
在我们的特性中,在boost::false_type
和boost::true_type
之前没有 public 关键字,因为我们使用了struct
关键字,并且默认情况下它使用公共继承。
还有更多...
那些使用 C++11 兼容编译器的读者可以使用<type_traits>
头文件中声明的true_type
和false_type
类型来创建自己的类型特征。自 C++17 以来,标准库有一个bool_constant<true_or_false>
类型别名,您可以方便地使用它。
通常情况下,Boost 版本的类和函数更具可移植性,因为它们可以在 C++11 之前的编译器上使用。
另请参阅
-
本章中几乎所有的示例都使用了类型特征。请参考
Boost.TypeTraits
文档,了解更多示例和信息,网址为boost.org/libs/type_traits
-
查看前面的示例以获取有关整数常量以及如何从头开始实现
true_type
和false_type
的更多信息。
为模板参数选择最佳操作符
假设我们正在使用来自不同供应商的类,这些类实现了不同数量的算术操作,并且具有从整数构造函数。我们确实希望制作一个函数,它可以递增任何一个传递给它的类。而且,我们希望这个函数是有效的!请看下面的代码:
template <class T>
void inc(T& value) {
// TODO:
// call ++value
// or call value ++
// or value += T(1);
// or value = value + T(1);
}
准备工作
需要一些关于 C++模板和Boost.TypeTrait
或标准库类型特征的基本知识。
如何做...
所有的选择都可以在编译时完成。这可以通过使用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) {
// call ++value
// or call value ++
// or value += T(1);
// or value = value + T(1);
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 construction of the functor.
(value); // Calling operator() of the functor.
}
工作原理...
所有的魔法都是通过conditional<bool Condition, class T1, class T2>
元函数完成的。当true
作为第一个参数传递给元函数时,它通过::type
typedef
返回T1
。当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
typedef
保存的内容是通过类型特征推断出来的。
还有更多...
在std::
命名空间的<type_traits>
头文件中有这个函数的 C++11 版本。Boost 在不同的库中有多个版本的这个函数;例如,Boost.MPL
有函数boost::mpl::if_c
,它的行为与boost::conditional
完全相同。它还有一个版本boost::mpl::if_
(末尾没有c
),它对其第一个模板参数调用::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 construction of the functor.
(value); // Calling operator() of the functor.
}
C++17 有一个if constexpr
结构,使前面的示例变得更简单:
template <class T>
void inc_cpp17(T& value) {
if constexpr (boost::has_pre_increment<T>()) {
++value;
} else if constexpr (boost::has_post_increment<T>()) {
value++;
} else if constexpr(boost::has_plus_assign<T>()) {
value += T(1);
} else {
value = value + T(1);
}
}
标准库中的整数常量,Boost.MPL
和Boost.TypeTraits
具有 constexpr 转换运算符。例如,这意味着std::true_type
的实例可以转换为true
值。在前面的例子中,boost::has_pre_increment<T>
表示一种类型,附加()
,或者 C++11 的大括号{}
创建该类型的实例,可以转换为true
或false
值。
另请参阅
-
启用整数类型的模板函数使用。
-
禁用实数类型的模板函数使用。
-
Boost.TypeTraits
文档中列出了所有可用的元函数。点击链接boost.org/libs/type_traits
阅读更多信息。 -
来自第八章的示例,元编程,将为您提供更多
Boost.MPL
库的使用示例。如果您感到自信,您也可以尝试阅读其文档,链接为boost.org/libs/mpl
。
在 C++03 中获取表达式的类型
在之前的示例中,我们看到了一些boost::bind
的使用示例。它可能是 C++11 之前的一个有用工具,但是在 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++读者会注意到,在 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
,前面的代码可以这样写:
// Long and portable way:
template<class T1, class T2>
struct result_of {
typedef BOOST_TYPEOF_TPL(T1() + T2()) type;
};
template<class T1, class T2>
typename result_of<T1, T2>::type add(const T1& t1, const T2& t2) {
return t1 + t2;
};
// ... or ...
// Shorter version that may crush some compilers.
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
的官方文档有更多示例。点击链接boost.org/libs/typeof
阅读更多信息。
第五章:多线程
在本章中,我们将涵盖:
-
创建执行线程
-
同步访问共享资源
-
使用原子快速访问共享资源
-
创建一个 work_queue 类
-
多读单写锁
-
创建每个线程唯一的变量
-
中断线程
-
操作一组线程
-
安全初始化共享变量
-
多个互斥锁
介绍
在本章中,我们将处理线程和与之相关的所有内容。鼓励具有多线程基础知识。
多线程意味着单个进程中存在多个执行线程。线程可以共享进程资源并拥有自己的资源。这些执行线程可以在不同的 CPU 上独立运行,从而实现更快速和更负责任的程序。Boost.Thread
库提供了跨操作系统接口的统一工作线程。它不是一个仅头文件的库,因此本章中的所有示例都需要链接到libboost_thread
和libboost_system
库。
创建执行线程
在现代多核编译器上,为了实现最大性能(或者只是提供良好的用户体验),程序通常使用多个执行线程。以下是一个激励性的例子,我们需要在一个线程中创建和填充一个大文件,该线程绘制用户界面:
#include <cstddef> // for std::size_t
bool is_first_run();
// Function that executes for a long time.
void fill_file(char fill_char, std::size_t size, const char* filename);
// Called in thread that draws a user interface:
void example_without_threads() {
if (is_first_run()) {
// This will be executing for a long time during which
// users interface freezes...
fill_file(0, 8 * 1024 * 1024, "save_file.txt");
}
}
准备工作
此配方需要了解boost::bind
或std::bind
。
如何做...
启动执行线程从未如此简单:
#include <boost/thread.hpp>
// Called in thread that draws a user interface:
void example_with_threads() {
if (is_first_run()) {
boost::thread(boost::bind(
&fill_file,
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
的析构函数将注意到它仍然持有一个 OS 线程,并将调用std::terminate
。它会在不调用析构函数、释放资源和进行其他清理的情况下终止我们的程序。
默认构造的线程也有一个Not-A-Thread
状态,并且它们不会创建一个独立的执行线程。
还有更多...
如果我们想确保在执行其他工作之前文件已创建并写入,那么我们需要以以下方式加入线程:
void example_with_joining_threads() {
if (is_first_run()) {
boost::thread t(boost::bind(
&fill_file,
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_VERSION=2
,boost::thread
的析构函数调用detach()
,这不会导致std::terminate
。但这样做会破坏与std::thread
的兼容性,并且有一天,当您的项目转移到 C++标准库线程时,或者当BOOST_THREAD_VERSION=2
不受支持时,这将给您带来很多惊喜。Boost.Thread
的第 4 版更加明确和强大,这在 C++语言中通常更可取。
请注意,当任何不是boost::thread_interrupted
类型的异常离开传递给boost::thread
构造函数的功能对象时,将调用std::terminate()
。
有一个非常有用的包装器,它作为一个 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
类被接受为 C++11 标准的一部分,并且您可以在std::
命名空间的<thread>
头文件中找到它。Boost 的版本 4 和 C++11 标准库版本的thread
类之间没有太大的区别。但是,boost::thread
在 C++03 编译器上可用,因此其使用更加灵活。
默认情况下调用std::terminate
而不是加入有很好的理由!C 和 C++语言经常用于生命关键软件。这些软件由其他软件控制,称为看门狗。这些看门狗可以轻松检测到应用程序已终止,但并不总是能够检测到死锁,或者以更长的延迟检测到死锁。例如,对于除颤器软件,终止比等待几秒钟以等待看门狗反应更安全。在设计这类应用程序时请记住这一点。
另请参阅
-
本章中的所有示例都使用了
Boost.Thread
。您可以继续阅读以获取有关该库的更多信息。 -
官方文档中列出了
boost::thread
方法的完整列表,并对它们在 C++11 标准库中的可用性进行了说明。请访问boost.org/libs/thread
获取官方文档。 -
中断线程的示例将让您了解
boost::interrupt_and_join_if_joinable
类的作用。
同步访问共享资源
现在我们知道如何启动执行线程,我们希望从不同的线程访问一些共享资源:
#include <cassert>
#include <cstddef>
#include <iostream>
// In previous recipe we included
// <boost/thread.hpp>, which includes all
// the classes of Boost.Thread.
// Following header includes only boost::thread.
#include <boost/thread/thread.hpp>
int shared_i = 0;
void do_inc() {
for (std::size_t i = 0; i < 30000; ++i) {
const int i_snapshot = ++shared_i;
// Do some work with i_snapshot.
// ...
}
}
void do_dec() {
for (std::size_t i = 0; i < 30000; ++i) {
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(global_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) {
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) {
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
变量是在其中构造的,以便在到达//关键部分结束。
的闭括号时,将调用lock
变量的析构函数并解锁互斥锁。即使在关键部分发生异常,互斥锁也会被正确解锁。
如果初始化一个共享变量,然后构造只读取它的线程,那么不需要互斥锁或其他同步。
还有更多...
锁定互斥锁可能是一个非常慢的操作,可能会使您的代码停止很长时间,直到其他线程释放锁。尝试使关键部分尽可能小;尽量减少代码中的关键部分。
让我们看看一些操作系统如何处理多核 CPU 上的锁定。当运行在 CPU 1 上的线程#1
尝试锁定已被另一个线程锁定的互斥锁时,线程#1
被操作系统停止,直到锁被释放。停止的线程不会占用处理器资源,因此操作系统在 CPU 1 上执行其他线程。现在,我们有一些线程在 CPU 1 上运行;一些其他线程释放了锁,现在操作系统必须恢复线程#1
的执行。因此,它在当前空闲的 CPU 上恢复其执行,例如 CPU2。
这导致 CPU 缓存未命中,因此在释放互斥锁后,代码运行速度略慢。通常情况下,情况并不那么糟糕,因为一个好的操作系统会尽力在相同的 CPU 上恢复线程。不幸的是,这样的操作系统特定优化并不总是可能的。减少关键部分的数量和大小以减少线程暂停和缓存未命中的机会。
不要尝试在同一线程中两次锁定boost::mutex
变量;这将导致死锁。如果需要从单个线程多次锁定互斥锁,请改用<boost/thread/recursive_mutex.hpp>
头文件中的boost::recursive_mutex
。多次锁定它不会导致死锁。boost::recursive_mutex
只有在每次lock()
调用后调用一次unlock()
后才释放锁。当不需要时避免使用boost::recursive_mutex
,因为它比boost::mutex
慢,通常表示糟糕的代码流设计。
boost::mutex
、boost::recursive_mutex
和boost::lock_guard
类已被接受为 C++11 标准库,并且您可以在std::
命名空间的<mutex>
头文件中找到它们。Boost 版本和标准库版本之间没有太大的区别。Boost 版本可能具有一些扩展(在官方文档中标记为EXTENSION),并且提供更好的可移植性,因为它们甚至可以在 C++11 之前的编译器上使用。
另请参阅
-
下一个示例将为您提供有关如何使此示例更快(更短)的想法。
-
阅读本章的第一个示例以获取有关
boost::thread
类的更多信息。官方文档boost.org/libs/thread
也可能对您有所帮助。 -
要获取有关为什么第一个示例失败以及多处理器如何使用共享资源的更多信息,请参阅
www.rdrop.com/users/paulmck/scalability/paper/whymb.2010.07.23a.pdf
上的Memory Barriers: a Hardware View for Software Hackers。请注意,这是一个难题。
使用原子操作快速访问共享资源
在前一个配方中,我们看到了如何安全地从不同线程访问一个共同的资源。但在那个配方中,我们只是做了两个系统调用(在锁定和解锁mutex
中)来获取整数的值:
{ // Critical section begin.
boost::lock_guard<boost::mutex> lock(i_mutex);
i_snapshot = ++ shared_i;
} // Critical section end.
这看起来很糟糕和慢!我们能让前一个配方中的代码更好吗?
准备工作
阅读第一个配方就足够开始这个配方了。一些关于多线程的基本知识将会很有帮助。
如何做...
让我们看看如何改进我们之前的例子:
- 现在,我们需要不同的头文件:
#include <cassert>
#include <cstddef>
#include <iostream>
#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) {
const int i_snapshot = ++ shared_i;
// Do some work with i_snapshot.
// ...
}
}
void do_dec() {
for (std::size_t i = 0; i < 30000; ++i) {
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;
assert(shared_i.is_lock_free());
}
它是如何工作的...
处理器提供特定的原子操作,不会受到其他处理器或处理器核心的干扰。这些操作对系统来说似乎是瞬间发生的。Boost.Atomic
提供了包装系统特定原子操作的类,与编译器合作以禁用可能破坏变量的多线程工作的优化,并提供一个统一的可移植接口来处理原子操作。如果两个不同线程同时开始对同一内存位置的原子操作,其中一个操作会等待直到另一个操作完成,然后重用前一个操作的结果。
换句话说,可以安全地同时从不同线程使用boost::atomic<>
变量。系统中对原子变量的每个操作都被视为单个事务。系统中对原子变量的一系列操作被视为一系列独立的事务:
--shared_i; // Transaction #1
// Some other may change value of `shared_i`!!
++shared_i; // Transaction #2
永远不要避免对多个线程修改的变量进行同步。即使变量是bool
,你所做的只是读取或写入true
/false
!编译器有权利优化掉所有的存储和读取,以一百万种无法想象的方式破坏你的代码。猜猜一个好的雇主会惩罚谁?(编译器不是这个问题的正确答案!)
还有更多...
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 秒),我们会看到差异(在 30,0000 次迭代中进行测试)。
所有已知的标准库实现都存在原子操作的问题。所有的都有!不要编写自己的原子操作。如果你认为你自己的原子操作实现会更好,而且你希望浪费一些时间--写下来,使用特殊工具进行检查,然后再考虑。直到你明白你错了。
符合 C++11 标准的编译器应该在std::
命名空间的<atomic>
头文件中具有所有原子类、typedefs
和宏。如果编译器正确支持 C++11 内存模型并且专门训练以优化std::atomic
变量,那么特定于编译器的std::atomic
实现可能比 Boost 的版本更快。
另请参阅
boost.org/libs/atomic
的官方文档可能会给你更多例子和一些关于这个主题的理论信息。
创建 work_queue 类
让我们称没有参数的函数对象为任务。
typedef boost::function<void()> task_t;
现在,想象一种情况,我们有一些负责发布任务的线程和一些负责执行发布的任务的线程。我们需要设计一个可以安全地被这两种类型的线程使用的类。这个类必须具有以下函数:
-
获取一个任务或等待任务直到被另一个线程发布
-
检查并获取任务(如果有任务则返回一个空任务)
-
发布任务
准备就绪
确保你对boost::thread
或std::thread
感到舒适,了解互斥锁的基础知识,并且了解boost::function
或std::function
。
如何做...
我们要实现的类与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;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 some_task();
const std::size_t tests_tasks_count = 3000 /*000*/;
void pusher() {
for (std::size_t i = 0; i < tests_tasks_count; ++i) {
g_queue.push_task(&some_task);
}
}
void popper_sync() {
for (std::size_t i = 0; i < tests_tasks_count; ++i) {
work_queue::task_type t = g_queue.pop_task();
t(); // 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 push.
push1.join();
push2.join();
push3.join();
g_queue.flush();
// Waiting for all the tasks to pop.
pop_sync1.join();
pop_sync2.join();
pop_sync3.join();
// Asserting that no tasks remained,
// and falling though without blocking.
assert(!g_queue.try_pop_task());
g_queue.push_task(&some_task);
// 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
循环中退出,解锁<mutex>
(lock
变量超出了作用域),并返回一个任务。
你必须在循环中检查条件,而不仅仅是在if
语句中!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()
来通知所有等待特定条件变量的线程。
一些极端的操作系统在 Boost 1.64 版本之前(https://github.com/boostorg/thread/pull/105)在临界区外(没有持有锁)调用notify_one()
时可能会出现极为罕见的问题。你很少会遇到这种情况。但是,为了避免在这些平台上出现问题,你可以在work_queue
类中添加一个flush()
函数,它持有一个锁并调用notify_all()
:
void flush() {
boost::lock_guard<boost::mutex> lock(tasks_mutex_);
cond_.notify_all();
}
当你完成了将任务推入队列的操作时,请调用flush()
来强制唤醒所有线程。
C++11 标准在<condition_variable>
头文件中声明了std::condition_variable
,在<mutex>
头文件中声明了std::unique_lock
。如果你使用 C++03 编译器,可以使用 Boost 版本,或者使用 Boost 的一些扩展。
通过添加对右值引用的支持并调用std::move(tasks_.front())
,work_queue
类可以得到显着改进。这将使关键部分的代码更快,减少线程、挂起和唤醒,减少缓存未命中,从而提高性能。
另请参阅
-
本章的前三个配方提供了关于
Boost.Thread
的许多有用信息 -
官方文档可能会给您更多的例子和一些关于这个主题的理论信息;它可以在
boost.org/libs/thread
找到
多读者单写者锁
想象一下,我们正在开发一些在线服务。我们有一个无序映射的注册用户,每个用户都有一些属性。这个集合被许多线程访问,但很少被修改。对以下集合的所有操作都是线程安全的:
#include <unordered_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::unordered_map<std::string, user_info> users_;
public:
bool is_online(const std::string& username) const {
boost::lock_guard<mutex_t> lock(users_mutex_);
return users_.find(username) != users_.end();
}
std::string get_address(const std::string& username) const {
boost::lock_guard<mutex_t> lock(users_mutex_);
return users_.at(username).address;
}
void set_online(const std::string& username, user_info&& data) {
boost::lock_guard<mutex_t> lock(users_mutex_);
users_.emplace(username, std::move(data));
}
// Other methods:
// ...
};
不幸的是,我们的在线服务在某种程度上很慢,分析器显示问题出在users_online
类中。任何操作都会在mutex_
变量上获得独占锁,因此即使获取资源也会导致在锁定的互斥锁上等待。由于一些资源很难复制,关键部分消耗了大量时间,从而减慢了对users_online
类的任何操作。
不幸的是,项目要求不允许我们重新设计类。我们能否在不更改接口的情况下加快速度?
准备就绪
确保您对boost::thread
或std::thread
感到满意,并了解互斥锁的基础知识。
如何做...
这可能会有所帮助:
用boost::shared_mutex
替换boost::mutex
。对于不修改数据的方法,用boost::shared_lock
替换boost::unique_locks
:
#include <boost/thread/shared_mutex.hpp>
class users_online {
typedef boost::shared_mutex mutex_t;
mutable mutex_t users_mutex_;
std::unordered_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();
}
std::string get_address(const std::string& username) const {
boost::shared_guard<mutex_t> lock(users_mutex_);
return users_.at(username).address;
}
void set_online(const std::string& username, user_info&& data) {
boost::lock_guard<mutex_t> lock(users_mutex_);
users_.emplace(username, std::move(data));
}
// Other methods:
// ...
};
工作原理...
如果这些线程不修改数据,我们可以允许多个线程同时从中获取数据。我们只需要独占拥有互斥锁,如果我们要修改由它保护的数据。在所有其他情况下,允许对数据进行同时访问。这就是boost::shared_mutex
的设计目的。它允许共享锁定(读锁定),允许对资源进行多个同时访问。
当我们尝试对共享锁定的资源进行独占锁定时,操作将被阻塞,直到没有剩余的读锁,然后才能对该资源进行独占锁定,迫使新的共享锁等待直到独占锁被释放。boost::shared_lock
用于读取和写入的锁定比通常的boost::mutex
锁定要慢得多。除非您确定没有好的方法重新设计您的代码,并且确定boost::shared_lock
会加快速度,否则不要使用boost::shared_lock
。
一些读者可能是第一次看到mutable
关键字。这个关键字可以应用于非静态和非常量的类成员。mutable
数据成员可以在常量成员函数中修改,通常用于互斥锁和其他与类逻辑无直接关系的辅助变量。
还有更多...
当您只需要独占锁时,不要使用boost::shared_mutex
,因为它比通常的boost::mutex
类更慢。
直到 C++14 之前,C++中还没有共享互斥锁。shared_timed_mutex
和shared_lock
在std::
命名空间的<shared_mutex>
头文件中定义。它们的性能特征接近 Boost 版本,因此应用所有前面的性能注意事项。
C++17 有一个shared_mutex
,可能比shared_timed_mutex
稍快,因为它不提供定时锁定的手段。这可能会节省一些宝贵的纳秒。
另请参阅
-
还有一个
boost::upgrade_mutex
类,对于需要将共享锁提升为独占锁的情况可能会有用。有关更多信息,请参阅boost.org/libs/thread
上的Boost.Thread
文档。 -
有关可变关键字的更多信息,请参阅
herbsutter.com/2013/01/01/video-you-dont-know-const-and-mutable/
。
创建每个线程唯一的变量
让我们来看看创建 work_queue 类的配方。那里的每个任务都可以在许多线程中的一个中执行,我们不知道在哪一个中执行。想象一下,我们想使用某个连接发送执行任务的结果:
#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
// ...
};
我们有以下解决方案:
-
在需要发送数据时打开新连接(这非常慢)
-
每个线程只有一个连接,并在互斥体中包装它们(这也很慢)
-
拥有一个连接池,在线程安全的方式下从中获取连接并使用它(需要大量编码,但这种解决方案很快)
-
每个线程只有一个连接(快速且简单实现)
那么,我们如何实现最后的解决方案?
准备工作
需要基本的线程知识。
如何做...
是时候创建一个线程本地变量了。在connection
类定义之后的头文件中声明一个函数:
connection& get_connection();
使您的源文件看起来像这样:
#include <boost/thread/tss.hpp>
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 the result:
get_connection().send_result(result);
}
工作原理...
boost::thread_specific_ptr
变量为每个线程保存一个单独的指针。最初,此指针等于nullptr
;这就是为什么我们检查!p
并在其为nullptr
时打开连接。
因此,当我们从已经初始化指针的线程进入get_connection()
时,!p
返回值为false
,我们返回已经打开的连接。
在线程退出时调用存储在connection_ptr
变量中的指针的delete
,因此我们不需要担心内存泄漏。
还有更多...
您可以提供自己的清理函数,该函数将在线程退出时调用,而不是调用delete
。清理函数必须具有void (*cleanup_function)(T*)
签名,并且必须在boost::thread_specific_ptr
构造期间传递。
C++11 有一个特殊的关键字thread_local
,用于声明具有线程本地存储期的变量。C++11 没有thread_specific_ptr
类,但您可以在支持thread_local
的编译器上使用thread_local T
或thread_local std::unique_ptr<T>
来实现相同的行为。boost::thread_specific_ptr
适用于 C++11 之前的编译器,而thread_local
则不适用。
C++17 有inline
变量,您可以在头文件中使用inline
和thread_local
声明线程本地变量。
另请参阅
-
Boost.Thread
文档提供了许多不同情况下的良好示例;可以在boost.org/libs/thread
找到。 -
阅读此主题
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
),但我们已经从中获取了所需数量的数据,因此可以停止解析。这是存根:
int main() {
boost::thread parser_thread(&do_parse);
// ...
if (stop_parsing) {
// No more parsing required.
// TODO: Stop the parser!
}
// ...
parser_thread.join();
}
我们如何做?
准备工作
这个配方几乎不需要任何东西。您只需要至少有基本的线程知识。
如何做...
我们可以通过中断来停止线程:
if (stop_parsing) {
// No more parsing required.
parser_thread.interrupt();
}
工作原理...
Boost.Thread
在其中提供了一些预定义的中断点,线程通过interrupt()
调用来检查是否被中断。如果线程被中断,将抛出异常boost::thread_interrupted
。当异常通过do_parse()
内部传播时,它会调用所有资源的析构函数,就像典型的异常一样。boost::thread_interrupted
异常在Boost.Thread
库中被特殊对待,对于该异常,允许离开线程函数(例如我们的示例中的do_parse()
)。当异常离开线程函数时,它被boost::thread
内部捕获,并被视为取消线程的请求。
boost::thread_interrupted
不是从std::exception
派生的!如果通过类型或引用std::exception
捕获异常,中断将有效。但是,如果通过catch (...)
捕获异常并且不重新抛出它,中断将无效。
正如我们从本章的第一个示例中所知道的,如果传递给线程的函数没有捕获异常并且异常离开函数范围,应用程序将终止。boost::thread_interrupted
是唯一的例外;它可以离开函数范围,并且不会std::terminate()
应用程序。
还有更多...
Boost.Thread
库的中断点在官方文档中列出。一般来说,一切阻塞都会检查中断。
我们也可以在任何地方手动添加中断点。我们只需要调用boost::this_thread::interruption_point()
:
void do_parse() {
while (not_end_of_parsing) {
// If current thread was interrupted, the following
// line will throw an boost::thread_interrupted.
boost::this_thread::interruption_point();
// Some parsing goes here.
// ...
}
}
如果项目不需要中断,定义BOOST_THREAD_DONT_PROVIDE_INTERRUPTIONS
会提供一点性能提升,并完全禁用线程中断。
C++11 没有线程中断,但可以使用原子操作部分模拟它们:
-
创建一个原子
bool
变量 -
在线程中检查原子变量,如果发生变化,则抛出异常
-
不要忘记在传递给线程的函数中捕获异常(否则你的应用程序将终止)
但是,如果代码在条件变量或睡眠方法中等待,这将无济于事。
另请参阅
-
Boost.Thread
的官方文档提供了预定义的中断点列表,网址为www.boost.org/doc/libs/1_64_0/doc/html/thread/thread_management.html#thread.thread_management.tutorial.interruption.predefined_interruption_points
-
作为练习,查看本章的其他示例,并考虑在哪些地方添加额外的中断点会改善代码
-
阅读
Boost.Thread
文档的其他部分可能会有用;请访问boost.org/libs/thread
操作一组线程
那些试图自己重复所有示例或者试验线程的读者,可能已经厌倦了编写以下代码来启动和加入线程:
#include <boost/thread.hpp>
void some_function();
void sample() {
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
变量:
#include <boost/thread.hpp>
int main() {
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
的官方文档可能会给你带来很多其他有用的类,这些类在本章节中没有描述;请访问boost.org/libs/thread
。
安全地初始化共享变量
想象一下,我们正在设计一个安全关键的类,该类从多个线程中使用,从服务器接收答案,对其进行后处理,并输出响应:
struct postprocessor {
typedef std::vector<std::string> answer_t;
// Concurrent calls on the same variable are safe.
answer_t act(const std::string& in) const {
if (in.empty()) {
// Extremely rare condition.
return read_defaults();
}
// ...
}
};
注意return read_defaults();
这一行。可能会出现服务器由于网络问题或其他问题而无法响应的情况。在这种情况下,我们尝试从文件中读取默认值:
// Executes for a long time.
std::vector<std::string> read_defaults();
从前面的代码中,我们遇到了问题:服务器可能在一段显著的时间内无法访问,并且在所有这段时间内,我们将在每次act
调用时重新读取文件。这显著影响了性能。
我们可以尝试通过在类内部存储default_
来修复它:
struct postprocessor {
typedef std::vector<std::string> answer_t;
private:
answer_t default_;
public:
postprocessor()
: default_(read_defaults())
{}
// Concurrent calls on the same variable are safe.
answer_t act(const std::string& in) const {
if (in.empty()) {
// Extremely rare condition.
return default_;
}
// ...
}
};
这也不是一个完美的解决方案:我们不知道用户构造了多少个postprocessor
类的实例,并且我们在可能在运行过程中不需要的默认值上浪费了内存。
因此,我们必须在第一次远程服务器失败时并发安全地读取和存储数据,并且在下一次失败时不再读取。有许多方法可以做到这一点,但让我们看看最正确的方法。
做好准备
对于这个配方,基本的线程知识已经足够了。
如何做...
- 我们必须添加变量来存储默认值已经初始化的信息,以及一个变量来存储默认值:
#include <boost/thread/once.hpp>
struct postprocessor {
typedef std::vector<std::string> answer_t;
private:
mutable boost::once_flag default_flag_;
mutable answer_t default_;
变量是mutable
,因为我们将在const
成员函数内部修改它们。
- 让我们初始化我们的变量:
public:
postprocessor()
: default_flag_(BOOST_ONCE_INIT)
, default_()
{}
- 最后,让我们改变
act
函数:
// Concurrent calls on the same variable are safe.
answer_t act(const std::string& in) const {
answer_t ret;
if (in.empty()) {
// Extremely rare condition.
boost::call_once(default_flag_, [this]() {
this->default_ = read_defaults();
});
return default_;
}
// ...
return ret;
}
};
工作原理...
简而言之,boost::call_once
和boost::once_flag
确保第二个参数作为函数只执行一次。
boost::call_once
函数同步调用作为第二个参数传递的函数F。boost::call_once
和boost::once_flag
确保在同一个once_flag
上有两个或更多并发调用时,只有一个对函数F的调用会进行,确保只有一次对F的成功调用。
如果对函数F的调用没有抛出异常离开F的主体,那么boost::call_once
假定调用成功,并将该信息存储在boost::once_flag
内。对具有相同boost::once_flag
的boost::call_once
的任何后续调用都不起作用。
不要忘记使用BOOST_ONCE_INIT
宏初始化boost::once_flag
。
还有更多...
boost::call_once
可以将参数传递给要调用的函数:
#include <iostream>
void once_printer(int i) {
static boost::once_flag flag = BOOST_ONCE_INIT;
boost::call_once(
flag,
[](int v) { std::cout << "Print once " << v << '\n'; },
i // <=== Passed to lambda from above.
);
// ...
}
现在,如果我们在循环中调用once_printer
函数:
int main() {
for (unsigned i = 0; i < 10; ++i) {
once_printer(i);
}
}
只有一行将被输出:
Print once 0
C++11 在<mutex>
头文件中有std::call_once
和std::once_flag
。与 Boost 版本不同,标准库版本的once_flag
不需要通过宏进行初始化,它有一个 constexpr 构造函数。通常情况下,如果必须支持旧编译器,则可以使用 Boost 版本。
2015 年之前的 Visual Studio 发行的std::call_once
实现效率不佳,比 Boost 版本慢十多倍。如果不使用现代编译器,请使用boost::call_once
。
另请参阅
Boost.Thread
文档提供了许多不同情况下的很好的例子。可以在boost.org/libs/thread
找到。
锁定多个互斥体
在接下来的几段中,你将成为编写游戏的人之一。恭喜,你可以在工作中玩游戏!
您正在开发一个服务器,必须编写代码来在两个用户之间交换战利品:
class user {
boost::mutex loot_mutex_;
std::vector<item_t> loot_;
public:
// ...
void exchange_loot(user& u);
};
每个用户操作都可能由服务器上的不同线程并发处理,因此您必须通过互斥体保护资源。初级开发人员试图解决问题,但他的解决方案不起作用:
void user::exchange_loot(user& u) {
// Terribly wrong!!! ABBA deadlocks.
boost::lock_guard<boost::mutex> l0(loot_mutex_);
boost::lock_guard<boost::mutex> l1(u.loot_mutex_);
loot_.swap(u.loot_);
}
上面代码中的问题是一个众所周知的ABBA 死锁问题。想象一下线程 1锁定互斥锁 A,线程 2锁定互斥锁 B。现在线程 1尝试锁定已经被锁定的互斥锁 B,而线程 2尝试锁定已经被锁定的互斥锁 A。这导致两个线程相互无限期地锁定,因为它们需要另一个线程拥有的资源才能继续,而另一个线程则在等待当前线程拥有的资源。
现在,如果user1
和user2
同时为对方调用exchange_loot
,那么我们可能会出现这样的情况,即user1.exchange_loot(user2)
调用锁定了user1.loot_mutex_
,而user2.exchange_loot(user1)
调用锁定了user2.loot_mutex_
。user1.exchange_loot(user2)
会无限等待尝试锁定user2.loot_mutex_
,而user2.exchange_loot(user1)
会无限等待尝试锁定user1.loot_mutex_
。
准备工作
对线程和互斥锁的基本知识就足够了。
如何做...
对此问题有两个主要的开箱即用的解决方案:
- 需要编译器支持可变模板的短模板:
#include <boost/thread/lock_factories.hpp>
void user::exchange_loot(user& u) {
typedef boost::unique_lock<boost::mutex> lock_t;
std::tuple<lock_t, lock_t> l = boost::make_unique_locks(
loot_mutex_, u.loot_mutex_
);
loot_.swap(u.loot_);
}
使用auto
的相同代码:
#include <boost/thread/lock_factories.hpp>
void user::exchange_loot(user& u) {
auto l = boost::make_unique_locks(
loot_mutex_, u.loot_mutex_
);
loot_.swap(u.loot_);
}
- 可移植解决方案:
#include <boost/thread/locks.hpp>
void user::exchange_loot(user& u) {
typedef boost::unique_lock<boost::mutex> lock_t;
lock_t l0(loot_mutex_, boost::defer_lock);
lock_t l1(u.loot_mutex_, boost::defer_lock);
boost::lock(l0, l1);
loot_.swap(u.loot_);
}
它是如何工作的...
核心思想是以某种方式对互斥锁进行排序,并始终按照特定顺序锁定它们。在这种情况下,不可能出现 ABBA 问题,因为所有线程在锁定互斥锁A之前始终会锁定互斥锁B。通常会使用其他死锁避免算法,但为了简单起见,我们假设了互斥锁的排序。
在第一个例子中,我们使用了boost::make_unique_locks
,它总是以特定顺序锁定线程并返回一个持有锁的元组。
在第二个例子中,我们手动创建了锁,但由于传递了boost::defer_lock
参数,没有锁定它们。实际的锁定发生在boost::lock(l0, l1)
调用中,它以预定义的顺序锁定了互斥锁。
现在,如果user1
和user2
同时为对方调用exchange_loot
,那么user1.exchange_loot(user2)
和user2.exchange_loot(user1)
的调用都会尝试首先锁定user1.loot_mutex_
,或者两者都会尝试首先锁定user2.loot_mutex_
。这取决于运行时。
还有更多...
boost::make_unique_locks
和boost::lock
函数可能接受超过 2 个锁或互斥锁,因此您可以在需要同时锁定两个以上互斥锁的更高级情况下使用它们。
C++11 在头文件<mutex>
中定义了一个std::lock
函数,其行为与boost::lock
函数完全相同。
C++17 有一个更美观的解决方案:
#include <mutex>
void user::exchange_loot(user& u) {
std::scoped_lock l(loot_mutex_, u.loot_mutex_);
loot_.swap(u.loot_);
}
在上面的代码中,std::scoped_lock
是一个接受可变数量锁的类。它具有可变模板参数,这些参数可以从 C++17 的推导指南中自动推导出来。上面例子中std::scoped_lock
的实际类型是:
std::scoped_lock<std::mutex, std::mutex>
std::scoped_lock
在构造期间持有所有传递的互斥锁的锁,并避免了死锁。换句话说,它的工作原理类似于第一个例子,但看起来更好一些。
另请参阅
Boost.Thread
的官方文档可能会给您带来很多其他有用的类,这些类在本章中没有描述;请访问boost.org/libs/thread.
第六章:操作任务
在这一章中,我们将涵盖:
-
为任意数据类型处理注册任务
-
创建定时器并将定时器事件作为任务处理
-
将网络通信作为任务
-
接受传入连接
-
并行执行不同的任务
-
管道任务处理
-
创建非阻塞屏障
-
存储异常并从中创建任务
-
获取和处理系统信号作为任务
介绍
这一章都是关于任务的。我们将称函数对象为任务,因为这样更简洁,更能反映它将要做的事情。这一章的主要思想是,我们可以将所有的处理、计算和交互分解为函数对象(任务),并几乎独立地处理每一个任务。此外,我们可能不会在一些慢操作上阻塞,比如从套接字接收数据或等待超时,而是提供一个回调任务并继续处理其他任务。一旦操作系统完成慢操作,我们的回调就会被执行。
理解示例的最佳方法是通过修改、运行和扩展来玩耍。网站apolukhin.github.io/Boost-Cookbook/
中有本章的所有示例,甚至可以在线玩耍一些示例。
开始之前
这一章至少需要对第一、第二和第五章有基本了解。还需要对 C++11 的右值引用和 lambda 有基本了解。
为任意数据类型处理注册任务
首先,让我们来处理保存所有任务并提供它们执行方法的类。在第五章的多线程,创建 work_queue 类的食谱中,我们已经在做类似的事情,但以下一些问题尚未解决:
-
work_queue
类只存储和返回任务,但我们还需要执行现有任务。 -
任务可能会抛出异常。如果它们离开任务边界,我们需要捕获和处理异常。
-
任务可能不会注意到线程中断。下一个任务可能会收到中断。
-
我们需要一种方法来停止任务的处理。
准备工作
此食谱需要链接boost_system
和boost_thread
库。还需要对Boost.Thread
有基本了解。
如何做...
在这个食谱中,我们使用boost::asio::io_service
代替了上一章的work_queue
。这样做是有原因的,我们将在接下来的食谱中看到。
- 让我们从包装用户任务的结构开始:
#include <boost/thread/thread.hpp>
#include <iostream>
namespace detail {
template <class T>
struct task_wrapped {
private:
T task_unwrapped_;
public:
explicit task_wrapped(const T& f)
: task_unwrapped_(f)
{}
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";
}
}
};
} // namespace detail
- 为了方便使用,我们将创建一个函数,从用户的函数对象产生
task_wrapped
:
namespace detail {
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 {
protected:
static boost::asio::io_service& get_ios() {
static boost::asio::io_service ios;
static boost::asio::io_service::work work(ios);
return ios;
}
- 让我们添加
push_task
方法:
public:
template <class T>
static void push_task(const T& task_unwrapped) {
get_ios().post(detail::make_task_wrapped(task_unwrapped));
}
- 让我们通过添加启动和停止任务执行循环的成员函数来完成这个类:
static void start() {
get_ios().run();
}
static void stop() {
get_ios().stop();
}
}; // tasks_processor
完成!现在是时候测试我们的类了:
int func_test() {
static int counter = 0;
++ counter;
boost::this_thread::interruption_point();
switch (counter) {
case 3:
throw std::logic_error("Just checking");
case 10:
// Emulation of thread interruption.
// Caught inside task_wrapped and does not stop execution.
throw boost::thread_interrupted();
case 90:
// Stopping the tasks_processor.
tasks_processor::stop();
}
return counter;
}
main
函数可能是这样的:
int main () {
for (std::size_t i = 0; i < 100; ++i) {
tasks_processor::push_task(&func_test);
}
// Processing was not started.
assert(func_test() == 1);
// We can also use lambda as a task.
// Counting 2 + 2 asynchronously.
int sum = 0;
tasks_processor::push_task(
[&sum]() { sum = 2 + 2; }
);
// Processing was not started.
assert(sum == 0);
// Does not throw, but blocks till
// one of the tasks it is owning
// calls tasks_processor::stop().
tasks_processor::start();
assert(func_test() == 91);
}
工作原理...
boost::asio::io_service
变量可以存储和执行发布到它的任务。但我们可能不能直接将用户的任务发布到它,因为它们可能接收到针对其他任务的中断或抛出异常。这就是为什么我们将用户的任务包装在detail::task_wrapped
结构中。它通过调用重置所有先前的中断:
try {
boost::this_thread::interruption_point();
} catch(const boost::thread_interrupted&){}
detail::task_wrapped
在try{ } catch()
块中执行任务,确保没有异常离开operator()
边界。
看一下start()
函数。boost::asio::io_service::run()
开始处理发布到io_service
变量的任务。如果没有调用boost::asio::io_service::run()
,则不会执行发布的任务(可以在main()
函数中看到)。可以通过调用boost::asio::io_service::stop()
来停止任务处理。
如果boost::asio::io_service
类从run()
函数返回,表示没有剩余的任务,因此我们使用boost::asio::io_service::work
的实例来强制它继续执行:
static boost::asio::io_service& get_ios() {
static boost::asio::io_service ios;
static boost::asio::io_service::work work(ios);
return ios;
}
在预 C++11 编译器上,iostream
类和变量(如std::cerr
和std::cout
)不是线程安全的,并且可能在 C++11 兼容的编译器上产生交错的字符。在实际项目中,必须使用额外的同步来获得可读的输出。为了简单起见,我们没有这样做。
还有更多...
C++17 标准库没有io_service
。但是,Boost.Asio
库的大部分内容被提议作为 Networking Technical Specification (TS)作为 C++的补充。
另请参阅
-
本章中的以下示例将向您展示为什么我们选择
boost::asio::io_service
而不是使用我们在第五章,多线程中手写的代码 -
您可以考虑阅读
Boost.Asio
的文档,获取一些示例、教程和类引用,网址为boost.org/libs/asio
-
您还可以阅读Boost.Asio C++ Network Programming一书,该书对
Boost.Asio
进行了更顺畅的介绍,并涵盖了本书中未涵盖的一些细节
创建定时器并将定时器事件处理为任务
以指定的间隔检查某些内容是一个常见的任务。例如,我们需要每 5 秒检查一次某些会话的活动情况。对于这样的问题,有一些常见的解决方案:
-
糟糕的解决方案创建一个线程进行检查,然后休眠 5 秒。这是一个糟糕的解决方案,会消耗大量系统资源,并且扩展性差。
-
正确的解决方案使用特定于系统的 API 来异步操作定时器。这是一个更好的解决方案,需要一些工作,并且不具有可移植性,除非您使用
Boost.Asio
。
准备工作
您必须知道如何使用 C++11 的右值引用和unique_ptr
。
这个示例是基于上一个示例的代码。请参阅本章的第一个示例,了解boost::asio::io_service
和task_queue
类的信息。
将此示例与boost_system
和boost_thread
库链接。定义BOOST_ASIO_DISABLE_HANDLER_TYPE_REQUIREMENTS
以绕过限制性库检查。
如何做...
我们只需通过添加新的方法来修改tasks_processor
类,以在指定的时间运行任务。
- 让我们为我们的
tasks_processor
类添加一个延迟运行任务的方法:
class tasks_processor {
// ...
public:
template <class Time, class Func>
static void run_delayed(Time duration_or_time, const Func& f) {
std::unique_ptr<boost::asio::deadline_timer> timer(
new boost::asio::deadline_timer(
get_ios(), duration_or_time
)
);
timer_ref.async_wait(
detail::timer_task<Func>(
std::move(timer),
f
)
);
}
};
- 最后一步,我们创建一个
timer_task
结构:
#include <boost/asio/io_service.hpp>
#include <boost/asio/deadline_timer.hpp>
#include <boost/system/error_code.hpp>
#include <memory> // std::unique_ptr
#include <iostream>
namespace detail {
template <class Functor>
struct timer_task {
private:
std::unique_ptr<boost::asio::deadline_timer> timer_;
task_wrapped<Functor> task_;
public:
explicit timer_task(
std::unique_ptr<boost::asio::deadline_timer> timer,
const Functor& task_unwrapped)
: timer_(std::move(timer))
, task_(task_unwrapped)
{}
void operator()(const boost::system::error_code& error) const {
if (!error) {
task_();
} else {
std::cerr << error << '\n';
}
}
};
} // namespace detail
这就是我们如何使用新功能的方式:
int main () {
const int seconds_to_wait = 3;
int i = 0;
tasks_processor::run_delayed(
boost::posix_time::seconds(seconds_to_wait),
test_functor(i)
);
tasks_processor::run_delayed(
boost::posix_time::from_time_t(time(NULL) + 1),
&test_func1
);
assert(i == 0);
// Blocks till one of the tasks
// calls tasks_processor::stop().
tasks_processor::start();
}
其中test_functor
是具有定义的operator()
的结构,test_func1
是一个函数:
struct test_functor {
int& i_;
explicit test_functor(int& i);
void operator()() const {
i_ = 1;
tasks_processor::stop();
}
};
void test_func1();
它是如何工作的...
简而言之,当经过指定的时间后,boost::asio::deadline_timer
将任务推送到boost::asio::io_service
类的实例中进行执行。
所有糟糕的东西都在run_delayed
函数中:
template <class Time, class Functor>
static void run_delayed(Time duration_or_time, const Functor& f) {
std::unique_ptr<boost::asio::deadline_timer>
timer( /* ... */ );
boost::asio::deadline_timer& timer_ref = *timer;
timer_ref.async_wait(
detail::timer_task<Functor>(
std::move(timer),
f
)
);
}
tasks_processor::run_delayed
函数接受一个超时和一个在超时后调用的函数对象。在其中,创建了一个boost::asio::deadline_timer
的唯一指针。boost::asio::deadline_timer
保存了用于异步执行任务的特定于平台的内容。
Boost.Asio
不会自动管理内存。库用户必须负责管理资源,通常是通过将资源保存在任务中。因此,如果我们需要一个定时器,并且希望在指定的超时后执行某个函数,我们必须将定时器的唯一指针移动到任务中,获取定时器的引用,并将任务传递给定时器。
我们在这一行中获取了deadline_timer
的引用:
boost::asio::deadline_timer& timer_ref = *timer;
现在,我们创建一个detail::timer_task
对象,该对象存储一个函数对象,并获取unique_ptr<boost::asio::deadline_timer>
的所有权:
detail::timer_task<Functor>(
std::move(timer),
f
)
boost::asio::deadline_timer
在触发之前不能被销毁,并且将其移动到timer_task
函数对象中可以保证这一点。
最后,我们指示 boost::asio::deadline_timer
在请求的时间到达时将 timer_task
函数对象发布到 io_service
中。
timer_ref.async_wait( /* timer_task */ )
io_service
变量的引用被保留在 boost::asio::deadline_timer
变量中。这就是为什么它的构造函数需要一个 io_service
的引用来存储它,并在超时结束后将任务发布到它。
detail::timer_task::operator()
方法接受 boost::system::error_code
,其中包含了等待时发生的错误描述。如果没有发生错误,我们调用用户的函数对象,该函数对象被包装以捕获异常(我们重用了第一个示例中的 detail::task_wrapped
结构)。
boost::asio::deadline_timer::async_wait
在等待超时时不会消耗 CPU 资源或执行线程。您可以简单地将一些任务推送到 io_service
中,它们将在超时被操作系统维护的同时开始执行:
作为一个经验法则:在 async_*
调用期间使用的所有资源必须存储在任务中。
还有更多...
一些古怪/古老的平台没有好的方式来实现定时器的 API,因此 Boost.Asio
库使用每个 io_service
的额外执行线程来模拟异步定时器的行为。没有其他方法可以做到这一点。
C++17 中没有类似于 Boost.Asio
的类;然而,Networking TS 中有 async_wait
和 timer
类。
另请参阅
-
阅读本章第一个示例将教会您如何使用
boost::asio::io_service
的基础知识。接下来的示例将为您提供更多关于io_service
使用的示例,并向您展示如何使用Boost.Asio
处理网络通信、信号和其他功能。 -
您可以考虑查看
Boost.Asio
的文档,获取一些示例、教程和类引用,网址为boost.org/libs/asio
。
网络通信作为一个任务
通过网络接收或发送数据是一个缓慢的操作。当机器接收数据包时,操作系统验证它们并将数据复制到用户指定的缓冲区中,可能会花费多秒钟。
我们可能会做很多工作,而不是等待!让我们修改我们的 tasks_processor
类,使其能够以异步方式发送和接收数据。非技术术语中,我们要求它至少从远程主机接收 N 字节,完成后调用我们的函数对象。顺便说一下,不要在此调用上阻塞。了解 libev、libevent 或 Node.js 的读者可能会在这个示例中找到很多熟悉的东西。
准备工作
这个示例基于前两个示例。请参阅本章的第一个示例,了解有关 boost::asio::io_service
和 task_queue
类的信息。请参阅第二个示例,复习异步处理的基础知识。
将此示例与 boost_system
和 boost_thread
库链接起来。定义 BOOST_ASIO_DISABLE_HANDLER_TYPE_REQUIREMENTS
以绕过过于严格的库检查。
如何做...
让我们通过添加方法来创建连接来扩展前一个示例中的代码。
- 连接将由
connection_with_data
类表示。这个类保持了与远程主机的套接字和一个用于接收和发送数据的std::string
:
#include <boost/asio/ip/tcp.hpp>
#include <boost/core/noncopyable.hpp>
struct connection_with_data: boost::noncopyable {
boost::asio::ip::tcp::socket socket;
std::string data;
explicit connection_with_data(boost::asio::io_service& ios)
: socket(ios)
{}
void shutdown() {
if (!socket.is_open()) {
return;
}
boost::system::error_code ignore;
socket.shutdown(
boost::asio::ip::tcp::socket::shutdown_both,
ignore
);
socket.close(ignore);
}
~connection_with_data() {
shutdown();
}
};
- 与上一个示例一样,类将主要由唯一指针使用。让我们为简单起见添加一个
typedef
:
#include <memory> // std::unique_ptr
typedef std::unique_ptr<connection_with_data> connection_ptr;
- 前一个示例中的
tasks_processor
类拥有boost::asio::io_service
对象。将其作为构建连接的工厂似乎是合理的:
class tasks_processor {
// ...
public:
static connection_ptr create_connection(
const char* addr,
unsigned short port_num)
{
connection_ptr c( new connection_with_data(get_ios()) );
c->socket.connect(boost::asio::ip::tcp::endpoint(
boost::asio::ip::address_v4::from_string(addr),
port_num
));
return c;
}
};
- 以下是将数据异步写入远程主机的方法:
#include <boost/asio/write.hpp>
template <class T>
struct task_wrapped_with_connection;
template <class Functor>
void async_write_data(connection_ptr&& c, const Functor& f) {
boost::asio::ip::tcp::socket& s = c->socket;
std::string& d = c->data;
boost::asio::async_write(
s,
boost::asio::buffer(d),
task_wrapped_with_connection<Functor>(std::move(c), f)
);
}
- 以下是从远程主机异步读取数据的方法:
#include <boost/asio/read.hpp>
template <class Functor>
void async_read_data(
connection_ptr&& c,
const Functor& f,
std::size_t at_least_bytes)
{
c->data.resize(at_least_bytes);
c->data.resize(at_least_bytes);
boost::asio::ip::tcp::socket& s = c->socket;
std::string& d = c->data;
char* p = (d.empty() ? 0 : &d[0]);
boost::asio::async_read(
s,
boost::asio::buffer(p, d.size()),
task_wrapped_with_connection<Functor>(std::move(c), f)
);
}
template <class Functor>
void async_read_data_at_least(
connection_ptr&& c,
const Functor& f,
std::size_t at_least_bytes,
std::size_t at_most)
{
std::string& d = c->data;
d.resize(at_most);
char* p = (at_most == 0 ? 0 : &d[0]);
boost::asio::ip::tcp::socket& s = c->socket;
boost::asio::async_read(
s,
boost::asio::buffer(p, at_most),
boost::asio::transfer_at_least(at_least_bytes),
task_wrapped_with_connection<Functor>(std::move(c), f)
);
}
- 最后一部分是
task_wrapped_with_connection
类的定义:
template <class T>
struct task_wrapped_with_connection {
private:
connection_ptr c_;
T task_unwrapped_;
public:
explicit task_wrapped_with_connection
(connection_ptr&& c, const T& f)
: c_(std::move(c))
, task_unwrapped_(f)
{}
void operator()(
const boost::system::error_code& error,
std::size_t bytes_count)
{
c_->data.resize(bytes_count);
task_unwrapped_(std::move(c_), error);
}
};
完成!现在,库用户可以像这样使用前面的类来发送数据:
void send_auth() {
connection_ptr soc = tasks_processor::create_connection(
"127.0.0.1", g_port_num
);
soc->data = "auth_name";
async_write_data(
std::move(soc),
&on_send
);
}
用户也可以像这样使用它来接收数据:
void receive_auth_response(
connection_ptr&& soc,
const boost::system::error_code& err)
{
if (err) {
std::cerr << "Error on sending data: "
<< err.message() << '\n';
assert(false);
}
async_read_data(
std::move(soc),
&process_server_response,
2
);
}
这是库用户处理接收到的数据的方法:
void process_server_response(
connection_ptr&& soc,
const boost::system::error_code& err)
{
if (err && err != boost::asio::error::eof) {
std::cerr << "Client error on receive: "
<< err.message() << '\n';
assert(false);
}
if (soc->data.size() != 2) {
std::cerr << "Wrong bytes count\n";
assert(false);
}
if (soc->data != "OK") {
std::cerr << "Wrong response: " << soc->data << '\n';
assert(false);
}
soc->shutdown();
tasks_processor::stop();
}
工作原理...
Boost.Asio
库不会直接管理资源和缓冲区。因此,如果我们想要一些简单的接口来读取和写入数据,最简单的解决方案就是将套接字和缓冲区绑定在一起以发送/接收数据。这就是connection_with_data
类所做的事情。它包含一个boost::asio::ip::tcp::socket
,这是Boost.Asio
对本机套接字的包装,以及一个我们用作缓冲区的std::string
变量。
boost::asio::ip::tcp::socket
类的构造函数接受boost::asio::io_service
,几乎所有Boost.Asio
类都是如此。创建套接字后,它必须连接到某个远程端点:
c->socket.connect(boost::asio::ip::tcp::endpoint(
boost::asio::ip::address_v4::from_string(addr),
port_num
));
看一下写函数。它接受一个指向connection_with_data
类的唯一指针和函数对象f
:
#include <boost/asio/write.hpp>
template <class Functor>
void async_write_data(connection_ptr&& c, const Functor& f) {
在其中,我们获取了套接字和缓冲区的引用:
boost::asio::ip::tcp::socket& s = c->socket;
std::string& d = c->data;
然后,我们请求进行异步写入:
boost::asio::async_write(
s,
boost::asio::buffer(d),
task_wrapped_with_connection<Functor>(std::move(c), f)
);
}
所有有趣的事情都发生在boost::asio::async_write
函数中。就像定时器一样,异步调用立即返回而不执行函数。它只是告诉在某些操作完成后将回调任务发布到boost::asio::io_service
中。boost::asio::io_service
在调用io_service::run()
方法的线程中执行我们的函数。以下图表说明了这一点:
现在,看一下task_wrapped_with_connection::operator()
。它接受const boost::system::error_code& error
和std::size_t bytes_count
,因为boost::asio::async_write
和boost::asio::async_read
函数在异步操作完成时传递这些参数。调用c_->data.resize(bytes_count);
将缓冲区的大小调整为仅包含接收/写入的数据。最后,我们调用最初传递给async
函数并存储为task_unwrapped_
的回调。
这一切是关于什么?这一切都是为了简单地发送数据!现在,我们有一个async_write_data
函数,它会将缓冲区中的数据异步写入套接字,并在操作完成时执行回调:
void on_send(connection_ptr&& soc, const boost::system::
error_code& err);
void connect_and_send() {
connection_ptr s = tasks_processor::create_connection
("127.0.0.1", 80);
s->data = "data_to_send";
async_write_data(
std::move(s),
&on_send
);
}
async_read_data
与async_write_data
非常接近。它调整缓冲区的大小,创建一个task_wrapped_with_connection
函数,并在异步操作完成时将其推送到is_service
中。
注意async_read_data_at_least
函数。在其中,对boost::asio::async_read
有一个略有不同的调用:
boost::asio::async_read(
s,
boost::asio::buffer(p, at_most),
boost::asio::transfer_at_least(at_least_bytes),
task_wrapped_with_connection<Functor>(std::move(c), f)
);
它包含boost::asio::transfer_at_least(al_least_bytes)
。Boost.Asio
有很多用于自定义读取和写入的函数对象。这个函数对象表示,在调用回调之前至少传输at_least_bytes
字节。更多的字节是可以的,直到它们适合缓冲区。
最后,让我们看一下其中一个回调函数:
void process_server_response(
connection_ptr&& soc,
const boost::system::error_code& err);
在这个例子中,回调函数必须接受connection_ptr
和boost::system::error_code
变量。boost::system::error_code
变量包含有关错误的信息。它有一个显式转换为bool
运算符,所以检查错误的简单方法就是写if (err) { ... }
。如果远程端结束传输并关闭套接字,err
可能包含boost::asio::error::eof
错误代码。这并不总是坏事。在我们的例子中,我们将其视为非错误行为:
if (err && err != boost::asio::error::eof) {
std::cerr << "Client error on receive: "
<< err.message() << '\n';
assert(false);
}
因为我们已经将套接字和缓冲区绑定在一起,所以可以从soc->data
中获取接收到的数据:
if (soc->data.size() != 2) {
std::cerr << "Wrong bytes count\n";
assert(false);
}
if (soc->data != "OK") {
std::cerr << "Wrong response: " << soc->data << '\n';
assert(false);
}
soc->shutdown()
的调用是可选的,因为当soc
超出范围时,会调用其析构函数。unique_ptr<connection_with_data>
的析构函数调用~connection_with_data
,其主体中包含shutdown()
。
还有更多...
我们的task_wrapped_with_connection::operator()
还不够好!用户提供的task_unwrapped_
回调可能会抛出异常,并且可能会被不属于特定任务的Boost.Thread
中断。修复方法是将回调包装到第一个示例中的类中:
void operator()(
const boost::system::error_code& error,
std::size_t bytes_count)
{
const auto lambda = [this, &error, bytes_count]() {
this->c_->data.resize(bytes_count);
this->task_unwrapped_(std::move(this->c_), error);
};
const auto task = detail::make_task_wrapped(lambda);
task();
}
在task_wrapped_with_connection::operator()
中,我们创建了一个名为lambda
的 lambda 函数。在执行时,lambda
将connection_with_data
类内的数据调整为bytes_count
,并调用最初传递的回调。最后,我们将lambda
包装到我们的安全执行任务中,并执行它。
您可能会在互联网上看到很多Boost.Asio
的示例。其中许多使用shared_ptr
而不是unique_ptr
来保留数据。使用shared_ptr
的方法更容易实现;但是,它有两个很大的缺点:
-
效率:
shared_ptr
内部有一个原子计数器,从不同线程修改它可能会显著降低性能。在接下来的一个示例中,您将看到如何在多个线程中处理任务,这是在高负载情况下可能会有所不同的地方。 -
明确性:使用
unique_ptr
,您总是可以看到连接的所有权已转移到某个地方(在代码中看到std::move
)。使用shared_ptr
,您无法从接口中了解函数是否获取了所有权,还是仅使用了对象的引用。
然而,如果根据应用程序的逻辑,所有权必须在多个任务之间共享,您可能会被迫使用shared_ptr
。
Boost.Asio
不是 C++17 的一部分,但它将很快作为 Networking TS 发布,并包含在即将到来的 C++标准中。
另请参阅
-
请参阅
Boost.Asio
的官方文档,了解更多示例、教程、完整参考资料,以及如何使用 UDP 或 ICMP 协议的示例,网址为boost.org/libs/asio
。 -
您还可以阅读《Boost.Asio C++网络编程》一书,该书更详细地描述了
Boost.Asio
接受传入连接
与网络一起工作的服务器端通常看起来像一个序列,我们首先获取新连接,然后读取数据,然后处理数据,最后发送结果。想象一下,我们正在创建某种必须每秒处理大量请求的授权服务器。在这种情况下,我们需要异步接受、接收、发送,并在多个线程中处理任务。
在这个示例中,我们将看到如何扩展我们的tasks_processor
类以接受和处理传入连接,在下一个示例中,我们将看到如何使其多线程化。
准备就绪
这个示例需要对boost::asio::io_service
的基础知识有很好的了解,就像本章的第一个示例中描述的那样。对网络通信的一些了解将对您有所帮助。还需要对boost::function
的了解,以及至少两个先前示例中的信息。将此示例与boost_system
和boost_thread
库链接。定义BOOST_ASIO_DISABLE_HANDLER_TYPE_REQUIREMENTS
以绕过过于严格的库检查。
如何做...
就像在以前的示例中一样,我们向我们的tasks_processor
类添加新的方法。
- 我们首先添加一些
typedefs
到tasks_processor
:
class tasks_processor {
typedef boost::asio::ip::tcp::acceptor acceptor_t;
typedef boost::function<
void(connection_ptr, const boost::system::error_code&)
> on_accpet_func_t;
- 让我们添加一个类,将新传入连接的套接字、用于监听的套接字以及用户提供的用于处理新连接的回调绑定在一起:
private:
struct tcp_listener {
acceptor_t acceptor_;
const on_accpet_func_t func_;
connection_ptr new_c_;
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)
{}
};
typedef std::unique_ptr<tcp_listener> listener_ptr;
- 我们需要添加一个函数,以在指定端口上开始监听:
public:
template <class Functor>
static void add_listener(unsigned short port_num, const Functor& f) {
std::unique_ptr<tcp_listener> listener(
new tcp_listener(get_ios(), port_num, f)
);
start_accepting_connection(std::move(listener));
}
- 开始接受传入连接的函数:
private:
static void start_accepting_connection(listener_ptr&& listener) {
if (!listener->acceptor_.is_open()) {
return;
}
listener->new_c_.reset(new connection_with_data(
listener->acceptor_.get_io_service()
));
boost::asio::ip::tcp::socket& s = listener->new_c_->socket;
acceptor_t& a = listener->acceptor_;
a.async_accept(
s,
tasks_processor::handle_accept(std::move(listener))
);
}
- 我们还需要一个处理新连接的函数对象:
private:
struct handle_accept {
listener_ptr listener;
explicit handle_accept(listener_ptr&& l)
: listener(std::move(l))
{}
void operator()(const boost::system::error_code& error) {
task_wrapped_with_connection<on_accpet_func_t> task(
std::move(listener->new_c_), listener->func_
);
start_accepting_connection(std::move(listener));
task(error, 0);
}
};
完成!现在,我们可以以以下方式接受连接:
class authorizer {
public:
static void on_connection_accpet(
connection_ptr&& connection,
const boost::system::error_code& error)
{
assert(!error);
// ...
}
};
int main() {
tasks_processor::add_listener(80, &authorizer::on_connection_accpet);
tasks_processor::start();
}
它是如何工作的...
函数add_listener
构造了新的tcp_listener
,它保留了接受连接所需的所有内容。就像任何异步操作一样,我们需要在操作执行时保持资源活动。tcp_listener
的唯一指针可以完成这项工作。
当我们构造boost::asio::ip::tcp::acceptor
并指定端点(参见步骤 3)时,它会在指定地址打开一个套接字,并准备好接受连接。
在步骤 4中,我们创建了一个新套接字,并为该新套接字调用async_accept
。当新连接到来时,listener->acceptor_
将此连接绑定到套接字,并将tasks_processor::handle_accept
回调推送到boost::asio::io_service
中。正如我们从上一个示例中了解到的,所有的async_*
调用都会立即返回,async_accept
也不是特例。
让我们更仔细地看看我们的handle_accept::operator()
。在其中,我们从上一个示例中创建了一个task_wrapped_with_connection
函数对象,并将一个新连接移动到其中。现在,我们的listener_ptr
在new_c_
中没有套接字,因为它是由函数对象拥有的。我们调用函数start_accepting_connection(std::move(listener))
,它在listener->new_c_
中创建一个新套接字并开始异步接受。异步接受操作不会阻塞,因此程序继续执行,从start_accepting_connection(std::move(listener))
函数返回,并执行带有连接task(error, 0)
的函数对象。
您已经按照示例中所示的一切做了,但服务器的性能还不够好。这是因为示例是简化的,许多优化被留在了现场。最重要的一个是在connection_with_data
中保留一个单独的小缓冲区,并将其用于所有内部Boost.Asio
回调相关的分配。有关此优化主题的更多信息,请参阅Boost.Asio
库的官方文档中的Custom memory allocation example。
当调用boost::asio::io_service
的析构函数时,所有回调的析构函数都会被调用。这使得tcp_connection_ptr
的析构函数被调用并释放资源。
还有更多...
我们没有使用boost::asio::ip::tcp::acceptor
类的所有功能。如果我们提供一个特定的boost::asio::ip::tcp::endpoint
,它可以绑定到特定的 IPv6 或 IPv4 地址。您还可以通过native_handle()
方法获取本机套接字,并使用一些特定于操作系统的调用来调整行为。您可以通过调用set_option
为acceptor_
设置一些选项。例如,这是您可以强制acceptor_
重用地址的方法:
boost::asio::socket_base::reuse_address option(true);
acceptor_.set_option(option);
重用地址可以在服务器在没有正确关闭的情况下快速重新启动。服务器终止后,套接字可能会打开一段时间,如果没有reuse_address
选项,您将无法在相同的地址上启动服务器。
C++17 没有来自Boost.Asio
的类,但具有大部分功能的 Networking TS 即将推出。
另请参阅
-
从头开始阅读本章是获取关于
Boost.Asio
更多信息的好方法 -
请参阅
Boost.Asio
的官方文档,了解更多示例、教程和完整参考资料,网址为boost.org/libs/asio
并行执行不同的任务
现在,是时候让我们的tasks_processor
在多个线程中处理任务了。这有多难呢?
入门
您需要阅读本章的第一个示例。还需要一些关于多线程的知识,特别是阅读Manipulating a group of threads示例。
将此示例与boost_system
和boost_thread
库链接。定义BOOST_ASIO_DISABLE_HANDLER_TYPE_REQUIREMENTS
以绕过限制性库检查。
如何做...
我们只需要将start_multiple
方法添加到我们的tasks_processor
类中:
#include <boost/thread/thread.hpp>
class tasks_processor {
public:
// Default value will attempt to guess optimal count of threads.
static void start_multiple(std::size_t threads_count = 0) {
if (!threads_count) {
threads_count = (std::max)(static_cast<int>(
boost::thread::hardware_concurrency()), 1
);
}
// First thread is the current thread.
-- threads_count;
boost::asio::io_service& ios = get_ios();
boost::thread_group tg;
for (std::size_t i = 0; i < threads_count; ++i) {
tg.create_thread([&ios]() { ios.run(); });
}
ios.run();
tg.join_all();
}
};
现在,我们可以做更多的工作,如下图所示:
它是如何工作的...
boost::asio::io_service::run
方法是线程安全的。我们只需要从不同的线程运行boost::asio::io_service::run
方法。
如果您正在执行修改共享资源的任务,则需要在该资源周围添加互斥锁,或者以一种方式组织您的应用程序,使得共享资源不会同时被不同的任务使用。可以安全地从不同的任务中使用资源而不会并发访问资源,因为boost::asio::io_service
负责在任务之间进行额外的同步,并强制使一个任务的修改结果被另一个任务看到。
请参阅对boost::thread::hardware_concurrency()
的调用。它返回可以在当前硬件上并发运行的线程数。但是,这只是一个提示,有时可能会返回0
值,这就是为什么我们对其调用std::max
函数。std::max
确保threads_count
至少存储值1
。
我们将std::max
包装在括号中,因为一些流行的编译器定义了min()
和max()
宏,所以我们需要额外的技巧来解决这个问题。
还有更多...
boost::thread::hardware_concurrency()
函数是 C++11 的一部分;您可以在std::
命名空间的<thread>
头文件中找到它。
所有boost::asio
类都不是 C++17 的一部分,但它们将很快作为网络 TS 提供。
另请参阅
-
请参阅
Boost.Asio
文档,了解更多示例和有关不同类的信息,网址为boost.org/libs/asio
-
来自第五章 多线程的配方(特别是最后一个名为操作一组线程的配方)将为您提供有关
Boost.Thread
用法的信息 -
请参阅
Boost.Thread
文档,了解boost::thread_group
和boost::threads
的信息,网址为boost.org/libs/thread
管道任务处理
有时,有一个要求在指定的时间间隔内处理任务。与以前的配方相比,在那里我们试图按照它们在队列中出现的顺序处理任务,这是一个很大的不同。
考虑一个例子,我们正在编写一个连接两个子系统的程序,其中一个子系统产生数据包,另一个子系统将修改后的数据写入磁盘(类似于视频摄像机、声音记录仪和其他设备中可以看到的情况)。我们需要按指定顺序逐个处理数据包,平稳地处理,有小的抖动,并且在多个线程中进行。
天真的方法在这里不起作用:
#include <boost/thread/thread.hpp>
subsystem1 subs1;
subsystem2 subs2;
void process_data() {
while (!subs1.is_stopped()) {
data_packet data = subs1.get_data();
decoded_data d_decoded = decode_data(data);
compressed_data c_data = compress_data(d_decoded);
subs2.send_data(c_data);
}
}
void run_in_multiple_threads() {
boost::thread t(&process_data);
process_data();
t.join();
}
在多线程环境中,我们可以在第一个线程中获取数据包#1,然后在第二个执行线程中获取数据包#2。由于不同的处理时间、操作系统上下文切换和调度,数据包#2可能会在数据包#1之前被处理。对于数据包,没有处理顺序的保证。让我们来解决这个问题!
准备工作
从第五章 多线程的制作工作队列配方中,需要理解这个例子。代码必须链接到boost_thread
和boost_system
库。
需要基本的 C++11 知识,特别是关于 lambda 函数。
如何做...
这个配方是基于第五章 多线程的制作工作队列配方中的work_queue
类的代码。我们将进行一些修改,并将使用该类的几个实例。
- 让我们首先为数据解码、数据压缩和数据发送创建单独的队列:
work_queue decoding_queue, compressing_queue, sending_queue;
- 现在,是时候重构
process_data
并将其拆分为多个函数了:
void start_data_accepting();
void do_decode(const data_packet& packet);
void do_compress(const decoded_data& packet);
void start_data_accepting() {
while (!subs1.is_stopped()) {
data_packet packet = subs1.get_data();
decoding_queue.push_task(
[packet]() {
do_decode(packet);
}
);
}
}
void do_decode(const data_packet& packet) {
decoded_data d_decoded = decode_data(packet);
compressing_queue.push_task(
[d_decoded]() {
do_compress(d_decoded);
}
);
}
void do_compress(const decoded_data& packet) {
compressed_data c_data = compress_data(packet);
sending_queue.push_task(
[c_data]() {
subs2.send_data(c_data);
}
);
}
- 我们的
work_queue
类从第五章 多线程中得到了一些接口更改,用于停止和运行任务:
#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 run();
void stop();
// Same as in Chapter 5, but with
// rvalue references support.
void push_task(task_type&& task);
};
work_queue
的stop()
和run()
函数的实现必须如下所示:
void work_queue::stop() {
boost::lock_guard<boost::mutex> lock(mutex_);
is_stopped_ = true;
cond_.notify_all();
}
void work_queue::run() {
while (1) {
boost::unique_lock<boost::mutex> lock(mutex_);
while (tasks_.empty()) {
if (is_stopped_) {
return;
}
cond_.wait(lock);
}
task_type t = std::move(tasks_.front());
tasks_.pop_front();
lock.unlock();
t();
}
}
- 就是这样!现在,我们只需要启动管道:
#include <boost/thread/thread.hpp>
int main() {
boost::thread t_data_decoding(
[]() { decoding_queue.run(); }
);
boost::thread t_data_compressing(
[]() { compressing_queue.run(); }
);
boost::thread t_data_sending(
[]() { sending_queue.run(); }
);
start_data_accepting();
- 可以这样停止管道:
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 |
然而,我们的世界并不理想,因此有些任务可能比其他任务更快完成。例如,接收可能比解码更快,在这种情况下,解码队列将保存一组要完成的任务。为了避免队列溢出,请努力使每个后续任务略微快于前一个任务。
在我们的例子中,我们没有使用boost::asio::io_service
,因为它不能保证发布的任务按照它们的发布顺序执行。
还有更多...
在这个例子中用来创建管线的所有工具都可以在 C++11 中使用,因此没有什么能阻止您在兼容 C++11 的编译器上创建相同的东西而不使用 Boost。然而,Boost 使您的代码更具可移植性,并且可以在 C++11 之前的编译器上使用。
另请参阅
-
这种技术是处理器开发人员熟知并使用的。请参阅
en.wikipedia.org/wiki/Instruction_pipeline
。在这里,您可以找到管线的所有特征的简要描述。 -
来自第五章的制作工作队列,多线程配方将为您提供有关本配方中使用的方法的更多信息。
制作非阻塞屏障
在多线程编程中,有一种称为屏障的抽象。它会阻止执行线程到达它,直到请求的线程数没有被阻塞在它上面。之后,所有线程都被释放,它们继续执行。考虑下面的例子,它可以使用在哪里。
我们希望在不同的线程中处理数据的不同部分,然后发送数据:
#include <boost/array.hpp>
#include <boost/thread/barrier.hpp>
#include <boost/thread/thread.hpp>
typedef boost::array<std::size_t, 10000> vector_type;
typedef boost::array<vector_type, 4> data_t;
void fill_data(vector_type& data);
void compute_send_data(data_t& data);
void runner(std::size_t thread_index, boost::barrier& barrier, data_t& data) {
for (std::size_t i = 0; i < 1000; ++ i) {
fill_data(data.at(thread_index));
barrier.wait();
if (!thread_index) {
compute_send_data(data);
}
barrier.wait();
}
}
int main() {
// Initing barrier.
boost::barrier 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([i, &barrier, &data] () {
runner(i, barrier, data);
});
}
tg.join_all();
}
data_barrier.wait()
方法会阻塞,直到所有线程填充数据。之后,所有线程都被释放。索引为0
的线程使用compute_send_data(data)
计算要发送的数据,而其他线程再次在屏障处等待,如下图所示:
看起来很糟糕,不是吗?
做好准备
这个配方需要对本章的第一个配方有所了解。还需要了解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) {
return;
}
compute_send_data(data);
if (++iteration == 1000) {
// Exiting, because 1000 iterations are done.
tasks_processor::stop();
return;
}
counter = 0;
for (std::size_t i = 0; i < data_t::static_size; ++ i) {
tasks_processor::push_task([i, iteration, &counter, &data]() {
clever_runner(
i,
iteration,
counter,
data
);
});
}
}
main
函数需要进行微小的更改:
// Initing counter.
atomic_count_t counter(0);
// Initing data.
data_t data;
// Run 4 tasks.
for (std::size_t i = 0; i < data_t::static_size; ++i) {
tasks_processor::push_task([i, &counter, &data]() {
clever_runner(
i,
0, // first iteration
counter,
data
);
});
}
tasks_processor::start();
它是如何工作的...
我们根本不会阻塞。我们不是阻塞,而是计算完成填充数据的任务。这是通过counter
原子变量完成的。最后剩下的任务将具有等于data_t::static_size
的counter
变量。只有该任务必须计算并发送数据。
之后,我们检查退出条件(完成 1000 次迭代),并通过将任务推送到队列来发布新数据。
还有更多...
这是更好的解决方案吗?首先,它的扩展性更好:
这种方法也可以更有效地处理程序执行大量不同工作的情况。因为没有线程在等待屏障,自由线程可以在一个线程计算和发送数据时执行其他任务。
这个配方可以在没有 Boost 库的情况下在 C++11 中实现。您只需要在tasks_processor
中用第五章的work_queue
替换io_service
。但是像往常一样,Boost 提供了更好的可移植性,并且可以使这个示例在 C++11 之前的编译器上运行,使用 Boost 库只需要用boost::bind
和boost::ref
替换 lambda 函数。
另请参阅
-
Boost.Asio
的官方文档可能会给您更多关于io_service
使用的信息,网址为boost.org/libs/asio
。 -
查看第二章中与
Boost.Function
相关的所有配方,资源管理,以及官方文档boost.org/libs/function
以了解任务的概述。 -
查看第一章的相关内容,了解有关
boost::bind
的更多信息,或者查看官方文档boost.org/libs/bind
。
存储异常并从中创建任务
处理异常并不总是简单的,可能会消耗大量时间。考虑异常必须被序列化并通过网络发送的情况。这可能需要几毫秒和几千行代码。在捕获异常后,处理它的时间和地点并不总是最佳的。
我们能存储异常并延迟它们的处理吗?
准备工作
这个配方需要熟悉boost::asio::io_service
,这在本章的第一个配方中有描述。
此配方需要链接boost_system
和boost_thread
库。
如何做...
我们所需要的就是能够存储异常并在线程之间传递它们,就像普通变量一样。
- 让我们从存储和处理异常的函数开始:
#include <boost/exception_ptr.hpp>
struct process_exception {
boost::exception_ptr exc_;
explicit process_exception(const boost::exception_ptr& exc)
: exc_(exc)
{}
void operator()() const;
};
- 该函数对象的
operator()
只是将异常输出到控制台:
#include <boost/lexical_cast.hpp>
void func_test2(); // Forward declaration.
void process_exception::operator()() const {
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::push_task(&func_test2);
} catch (...) {
std::cout << "Can not handle such exceptions:\n"
<< boost::current_exception_diagnostic_information()
<< std::endl;
// Stopping.
tasks_processor::stop();
}
}
- 让我们编写一些函数来演示异常的工作方式:
#include <stdexcept>
void func_test1() {
try {
boost::lexical_cast<int>("oops!");
} catch (...) {
tasks_processor::push_task(
process_exception(boost::current_exception())
);
}
}
void func_test2() {
try {
// ...
BOOST_THROW_EXCEPTION(std::logic_error("Some fatal logic error"));
// ...
} catch (...) {
tasks_processor::push_task(
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:
main.cpp(48): 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
库提供了存储和重新抛出异常的功能。boost::current_exception()
方法只能在catch()
块内部调用,并返回一个boost::exception_ptr
类型的对象。
在func_test1()
的前面的示例中,抛出了boost::bad_lexical_cast
异常。它由boost::current_exception()
返回;从该异常创建了一个process_exception
任务。
从boost::exception_ptr
中恢复异常类型的唯一方法是使用boost::rethrow_exception(exc)
函数重新抛出它。这就是process_exception
函数的作用。
抛出和捕获异常是一个繁重的操作。抛出可能会动态分配内存,触及冷内存,锁定互斥锁,计算一堆地址,以及做其他事情。在性能关键路径中不要没有非常好的理由就抛出异常!
在func_test2
中,我们使用BOOST_THROW_EXCEPTION
宏抛出了一个std::logic_error
异常。这个宏做了很多有用的工作;它检查我们的异常是否派生自std::exception
,为我们的异常添加关于源文件名、函数名和抛出异常的代码行号的信息。当我们的std::logic_error
异常在process_exception::operator()
内部重新抛出时,它被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(
&run_throw,
boost::ref(ptr)
);
// Some code goes here.
// ...
t.join();
// Checking for exception.
if (ptr) {
// Exception occurred 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++17 中。
另请参阅
-
Boost.Exception
的官方文档在boost.org/libs/exception
中包含了关于实现和限制的大量有用信息。您还可以找到一些本配方中未涵盖的信息(例如,如何向已抛出的异常添加附加信息)。 -
本章的第一个配方为您提供了关于
tasks_processor
类的信息。第三章的将字符串转换为数字配方描述了Boost.LexicalCast
库,该库在本配方中使用。
获取和处理系统信号作为任务
在编写一些服务器应用程序(特别是针对 Linux 操作系统)时,捕获和处理信号是必需的。通常,在服务器启动时设置所有信号处理程序,并且在应用程序执行期间不会更改。
这个配方的目标是使我们的tasks_processor
类能够处理信号。
准备工作
我们将需要本章的第一个配方中的代码。还需要对Boost.Function
有扎实的了解。
这个配方需要链接boost_system
和boost_thread
库。
如何做...
这个配方类似于本章的第2到4个配方:我们有async
信号等待函数,一些async
信号处理程序和一些支持代码。
- 让我们从以下头文件开始包括:
#include <boost/asio/signal_set.hpp>
#include <boost/function.hpp>
- 现在,我们为
tasks_processor
类添加一个信号处理成员:
protected:
static boost::asio::signal_set& signals() {
static boost::asio::signal_set signals_(get_ios());
return signals_;
}
static boost::function<void(int)>& signal_handler() {
static boost::function<void(int)> users_signal_handler_;
return users_signal_handler_;
}
- 在信号捕获时将被调用的函数如下:
static void handle_signals(
const boost::system::error_code& error,
int signal_number)
{
signals().async_wait(&tasks_processor::handle_signals);
if (error) {
std::cerr << "Error in signal handling: " << error << '\n';
} else {
boost::function<void(int)> h = signal_handler();
h(signal_number);
}
}
- 现在我们需要一个函数来注册信号处理程序:
public:
// This function is not thread safe!
// Must be called before all the `start()` calls.
// Function can be called only once.
template <class Func>
static void register_signals_handler(
const Func& f,
std::initializer_list<int> signals_to_wait)
{
// Making sure that this is the first call.
assert(!signal_handler());
signal_handler() = f;
boost::asio::signal_set& sigs = signals();
std::for_each(
signals_to_wait.begin(),
signals_to_wait.end(),
&sigs { sigs.add(signal); }
);
sigs.async_wait(&tasks_processor::handle_signals);
}
就这些了。现在,我们准备处理信号。以下是一个测试程序:
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::stop();
}
}
int main () {
tasks_processor::register_signals_handler(
&accept_3_signals_and_stop,
{ SIGINT, SIGSEGV }
);
tasks_processor::start();
}
这将产生以下输出:
Captured 1 SIGINT
Captured 2 SIGINT
Captured 3 SIGINT
Press any key to continue . . .
它是如何工作的...
这里没有什么困难(与本章之前的一些配方相比)。register_signals_handler
函数添加将被处理的信号编号。通过对signals_to_wait
的每个元素调用boost::asio::signal_set::add
函数来完成。
接下来,sigs.async_wait
开始async
等待信号,并在信号捕获时调用tasks_processor::handle_signals
函数。tasks_processor::handle_signals
函数立即开始异步等待下一个信号,检查错误,如果没有错误,则调用回调函数提供信号编号。
还有更多...
我们可以做得更好!我们可以将用户提供的回调包装到第一个配方中的类中,以正确处理异常并执行第一个配方中的其他好东西:
boost::function<void(int)> h = signal_handler();
detail::make_task_wrapped([h, signal_number]() {
h(signal_number);
})(); // make and run task_wrapped
当需要线程安全的动态添加和删除信号时,我们可以修改此示例,使其类似于本章的制作定时器和处理定时器事件作为任务食谱中的detail::timer_task
。当多个boost::asio::signal_set
对象注册等待相同的信号时,每个signal_set
的处理程序都会在单个信号上被调用。
C++长期以来一直能够使用<csignal>
头文件中的signal
函数处理信号。网络 TS 可能不会具有signal_set
功能。
另请参阅
-
来自第二章管理资源的将任何功能对象存储在变量中食谱提供了有关
boost::function
的信息 -
有关
boost::asio::signal_set
和此优秀库的其他功能的更多信息和示例,请参阅Boost.Asio
的官方文档boost.org/libs/asio
第七章:操作字符串
在本章中,我们将涵盖:
-
更改大小写和不区分大小写比较
-
使用正则表达式匹配字符串
-
使用正则表达式搜索和替换字符串
-
使用安全的 printf 样式函数格式化字符串
-
替换和删除字符串
-
用两个迭代器表示一个字符串
-
使用对字符串类型的引用
介绍
整个章节都致力于不同方面的更改、搜索和表示字符串。我们将看到如何使用 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>
const bool solution_1 = (
boost::iequals(str1, str2)
);
- 使用 Boost 谓词和标准库方法:
#include <boost/algorithm/string/compare.hpp>
#include <algorithm>
const bool solution_2 = (
str1.size() == str2.size() && std::equal(
str1.begin(),
str1.end(),
str2.begin(),
boost::is_iequal()
)
);
- 制作两个字符串的小写副本:
#include <boost/algorithm/string/case_conv.hpp>
void solution_3() {
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>
void solution_4() {
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>
void solution_5() {
boost::to_lower(str1);
boost::to_lower(str2);
assert(str1 == str2);
}
它是如何工作的...
第二种方法并不明显。在第二种方法中,我们比较字符串的长度。如果它们长度相同,我们使用boost::is_iequal
谓词的实例逐个字符比较字符串,该谓词以不区分大小写的方式比较两个字符。
Boost.StringAlgorithm
库在方法或类的名称中使用i
,如果该方法是不区分大小写的。例如,boost::is_iequal
,boost::iequals
,boost::is_iless
等。
还有更多...
Boost.StringAlgorithm
库的每个函数和函数对象都接受std::locale
。默认情况下(在我们的示例中),方法和类使用默认构造的std::locale
。如果我们大量使用字符串,一次构造std::locale
变量并将其传递给所有方法可能是一个很好的优化。另一个很好的优化是通过std::locale::classic()
使用C语言环境(如果您的应用逻辑允许):
// On some platforms std::locale::classic() works
// faster than std::locale().
boost::iequals(str1, str2, std::locale::classic());
没有人禁止您同时使用这两种优化。
不幸的是,C++17 没有来自Boost.StringAlgorithm
的字符串函数。所有的算法都快速可靠,所以不要害怕在代码中使用它们。
另请参阅
-
Boost String Algorithms 库的官方文档可以在
boost.org/libs/algorithm/string
找到 -
请参阅 Andrei Alexandrescu 和 Herb Sutter 的C++编程标准一书,了解如何使用几行代码制作不区分大小写的字符串的示例
使用正则表达式匹配字符串
让我们做一些有用的事情!当用户的输入必须使用一些正则表达式进行检查时,这是一个常见情况。问题在于有很多正则表达式语法,使用一种语法编写的表达式在其他语法中处理得不好。另一个问题是,长的正则表达式不那么容易编写。
因此,在这个示例中,我们将编写一个支持不同正则表达式语法并检查输入字符串是否匹配指定正则表达式的程序。
入门
这个示例需要基本的标准库知识。了解正则表达式语法可能会有所帮助。
需要将示例链接到boost_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\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 << "Incorrect 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()) {
- 对其应用正则表达式并输出结果:
const 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
类完成的。它构造了一个能够进行正则表达式解析和编译的对象。通过flag
输入变量将额外的配置选项传递给类。
如果正则表达式不正确,boost::regex
会抛出异常。如果传递了boost::regex::no_except
标志,它会在status()
调用中返回非零以报告错误(就像我们的示例中一样):
if (e.status()) {
std::cout << "Incorrect regex pattern!\n";
continue;
}
这将导致:
Input regex: (incorrect regex(
Incorrect regex pattern!
通过调用boost::regex_match
函数来进行正则表达式匹配。如果匹配成功,它将返回true
。可以向regex_match
传递其他标志,但为了简洁起见,我们避免了它们的使用。
还有更多...
C++11 几乎包含了所有Boost.Regex
类和标志。它们可以在std::
命名空间的<regex>
头文件中找到(而不是boost::
)。官方文档提供了关于 C++11 和Boost.Regex
的差异的信息。它还包含一些性能测量,表明Boost.Regex
很快。一些标准库存在性能问题,因此在 Boost 和标准库版本之间明智地进行选择。
另请参阅
-
使用正则表达式搜索和替换字符串示例将为您提供有关
Boost.Regex
用法的更多信息 -
您还可以考虑官方文档,以获取有关标志、性能测量、正则表达式语法和 C++11 兼容性的更多信息,网址为
boost.org/libs/regex
使用正则表达式搜索和替换字符串
我的妻子非常喜欢通过正则表达式匹配字符串示例。但是,她想要更多,并告诉我,除非我提升这个配方以便能够根据正则表达式匹配替换输入字符串的部分,否则我将得不到食物。
好的,它来了。每个匹配的子表达式(括号中的正则表达式部分)必须从 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###
准备工作
我们将重用通过正则表达式匹配字符串示例中的代码。建议在阅读本示例之前先阅读它。
需要链接一个示例到boost_regex
库。
如何做到...
这个配方是基于前一个配方的代码。让我们看看必须改变什么:
- 不需要包含额外的头文件。但是,我们需要一个额外的字符串来存储替换模式:
std::string regex, str, replace_string;
- 我们用
boost::regex_match
替换为boost::regex_find
并输出匹配的结果:
std::cout << "String to match: ";
while (std::getline(std::cin, str) && !str.empty()) {
boost::smatch results;
const 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
函数不仅返回true
或false
值(不像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
之外的其他双向迭代器,您应该将您的双向迭代器的类型作为boost::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 标准兼容性和正则表达式语法的示例和信息,网址为boost.org/libs/regex
。通过正则表达式匹配字符串示例将告诉您Boost.Regex
的基础知识。
使用安全的 printf 样式函数格式化字符串
printf
系列函数对安全性构成威胁。允许用户将自己的字符串作为类型并格式化说明符是非常糟糕的设计。那么当需要用户定义的格式时,我们该怎么办?我们应该如何实现以下类的成员函数std::string to_string(const std::string& format_specifier) const;
?
class i_hold_some_internals
{
int i;
std::string s;
char c;
// ...
};
准备工作
对标准库的基本知识就足够了。
如何做到...
我们希望允许用户为字符串指定自己的输出格式:
- 为了以安全的方式进行操作,我们需要以下头文件:
#include <boost/format.hpp>
- 现在,我们为用户添加一些注释:
// `fmt` parameter may 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& fmt) const {
- 是时候让所有部分都运行起来了:
boost::format f(fmt);
unsigned char flags = boost::io::all_error_bits;
flags ^= boost::io::too_many_args_bit;
f.exceptions(flags);
return (f % i % s % c).str();
}
就是这样。看一下这段代码:
int main() {
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 is catched.
std::cout << e.what() << '\n';
}
前一个代码片段通过控制台输出了以下行:
boost::too_few_args: format-string referred to more arguments than
were passed
C++17 没有std::format
。Boost.Format
库不是一个非常快的库。尽量不要在性能关键的部分大量使用它。
另请参阅
官方文档包含了有关Boost.Format
库性能的更多信息。在boost.org/libs/format
上还有更多关于扩展 printf 格式的示例和文档。
替换和擦除字符串
我们需要在字符串中擦除某些内容,替换字符串的一部分,或者擦除某些子字符串的第一个或最后一个出现的情况非常常见。标准库允许我们做更多的部分,但通常需要编写太多的代码。
我们在更改大小写和不区分大小写比较示例中看到了Boost.StringAlgorithm
库的实际应用。让我们看看当我们需要修改一些字符串时,它如何简化我们的生活:
#include <string>
const std::string str = "Hello, hello, dear Reader.";
准备工作
这个示例需要对 C++有基本的了解。
如何做到...
这个示例展示了Boost.StringAlgorithm
库中不同的字符串擦除和替换方法的工作原理:
- 擦除需要
#include <boost/algorithm/string/erase.hpp>
头文件:
#include <boost/algorithm/string/erase.hpp>
void erasing_examples() {
namespace ba = boost::algorithm;
using std::cout;
cout << "\n erase_all_copy :" << ba::erase_all_copy(str, ",");
cout << "\n erase_first_copy:" << ba::erase_first_copy(str, ",");
cout << "\n erase_last_copy :" << ba::erase_last_copy(str, ",");
cout << "\n ierase_all_copy :" << ba::ierase_all_copy(str, "hello");
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>
头文件:
#include <boost/algorithm/string/replace.hpp>
void replacing_examples() {
namespace ba = boost::algorithm;
using std::cout;
cout << "\n replace_all_copy :"
<< ba::replace_all_copy(str, ",", "!");
cout << "\n replace_first_copy :"
<< ba::replace_first_copy(str, ",", "!");
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 作为默认参数。
您经常使用不区分大小写的方法并且需要更好的性能吗?只需创建一个持有std::locale::classic()
的std::locale
变量,并将其传递给所有算法。在小字符串上,大部分时间都被std::locale
构造所消耗,而不是算法:
#include <boost/algorithm/string/erase.hpp>
void erasing_examples_locale() {
namespace ba = boost::algorithm;
const std::locale loc = std::locale::classic();
const std::string r1
= ba::ierase_all_copy(str, "hello", loc);
const std::string r2
= ba::ierase_nth_copy(str, ",", 1, loc);
// ...
}
C++17 没有Boost.StringAlgorithm
方法和类。然而,它有一个std::string_view
类,可以在没有内存分配的情况下使用子字符串。您可以在本章的下两个配方中找到更多关于类似std::string_view
的类的信息。
另请参阅
-
官方文档包含大量示例和所有方法的完整参考
boost.org/libs/algorithm/string
-
有关
Boost.StringAlgorithm
库的更多信息,请参见本章的更改大小写和不区分大小写比较配方
用两个迭代器表示一个字符串
有时我们需要将一些字符串拆分成子字符串并对这些子字符串进行操作。在这个配方中,我们想将字符串拆分成句子,计算字符和空格,当然,我们想使用 Boost 并尽可能高效。
准备工作
对于这个配方,您需要一些标准库算法的基本知识。
如何做...
使用 Boost 非常容易:
- 首先,包括正确的头文件:
#include <iostream>
#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 << 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
35 characters.
Sentence has 6 whitespaces.
Sentence #2 : Please split this character array to sentences
46 characters.
Sentence has 6 whitespaces.
Sentence #3 : Do you know, that sentences are separated using dot,
exclamation mark and question mark
90 characters.
Sentence has 13 whitespaces.
Sentence #4 : :-)
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
。
默认构造的分割迭代器表示eof()
。因此,我们可以将循环条件从!sentences.eof()
重写为sentences != split_iter_t()
。您还可以使用分割迭代器与算法,例如:std::for_each(sentences, split_iter_t(), [](auto range){ /**/ });
。
还有更多...
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++17 中既没有iterator_range
也没有split_iterator
。然而,正在讨论接受类似iterator_range
的类,可能会有名为std::span
的名称。
boost::iterator_range
类没有虚函数和动态内存分配,它非常快速和高效。然而,它的输出流操作符<<
对字符数组没有特定的优化,因此流操作可能会很慢。
boost::split_iterator
类中有一个boost::function
类,因此为大型函数构造它可能会很慢。迭代只会增加微小的开销,即使在性能关键的部分,你也不会感觉到。
另请参阅
-
下一个示例将告诉您
boost::iterator_range<const char*>
的一个很好的替代品 -
Boost.StringAlgorithm
的官方文档可能会为您提供有关类的更详细信息以及大量示例的信息,网址为boost.org/libs/algorithm/string
-
关于
boost::iterator_range
的更多信息可以在这里找到:boost.org/libs/range
;它是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();
}
++ pos_beg;
std::string::const_iterator pos_end
= std::find(pos_beg, input.end(), ends);
return std::string(pos_beg, pos_end);
}
你喜欢这个实现吗?在我看来,这个实现很糟糕。考虑对它的以下调用:
between_str("Getting expression (between brackets)", '(', ')');
在这个示例中,从"Getting expression (between brackets)"
构造了一个临时的std::string
变量。字符数组足够长,因此在std::string
构造函数内可能会调用动态内存分配,并将字符数组复制到其中。然后,在between_str
函数的某个地方,将构造新的std::string
,这可能还会导致另一个动态内存分配和复制。
因此,这个简单的函数可能会,并且在大多数情况下会:
-
调用动态内存分配(两次)
-
复制字符串(两次)
-
释放内存(两次)
我们能做得更好吗?
准备工作
这个示例需要对标准库和 C++有基本的了解。
如何做...
在这里我们实际上并不需要std::string
类,我们只需要一些轻量级的类,它不管理资源,只有一个指向字符数组和数组大小的指针。Boost 有boost::string_view
类可以满足这个需求。
- 要使用
boost::string_view
类,请包含以下头文件:
#include <boost/utility/string_view.hpp>
- 更改方法的签名:
boost::string_view between(
boost::string_view input,
char starts,
char ends)
- 在函数体内的任何地方将
std::string
更改为boost::string_view
:
{
boost::string_view::const_iterator pos_beg
= std::find(input.cbegin(), input.cend(), starts);
if (pos_beg == input.cend()) {
return boost::string_view();
}
++ pos_beg;
boost::string_view::const_iterator pos_end
= std::find(pos_beg, input.cend(), ends);
// ...
boost::string_view
构造函数接受大小作为第二个参数,因此我们需要稍微更改代码:
if (pos_end == input.cend()) {
return boost::string_view(pos_beg, input.end() - pos_beg);
}
return boost::string_view(pos_beg, pos_end - pos_beg);
}
就是这样!现在我们可以调用between("Getting expression (between brackets)", '(', ')')
,而且它将在没有任何动态内存分配和字符复制的情况下工作。而且我们仍然可以将其用于std::string
:
between(std::string("(expression)"), '(', ')')
工作原理...
如前所述,boost::string_view
只包含一个指向字符数组的指针和数据大小。它有很多构造函数,可以以不同的方式初始化:
boost::string_view r0("^_^");
std::string O_O("O__O");
boost::string_view r1 = O_O;
std::vector<char> chars_vec(10, '#');
boost::string_view r2(&chars_vec.front(), chars_vec.size());
boost::string_view
类具有container
类所需的所有方法,因此可以与标准库算法和 Boost 算法一起使用:
#include <boost/algorithm/string/case_conv.hpp>
#include <boost/algorithm/string/replace.hpp>
#include <boost/lexical_cast.hpp>
#include <iterator>
#include <iostream>
void string_view_algorithms_examples() {
boost::string_view r("O_O");
// Finding single 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", "^"
);
std::cout << '\n';
r = "100";
assert(boost::lexical_cast<int>(r) == 100);
}
boost::string_view
类实际上并不拥有字符串,因此它的所有方法都返回常量迭代器。因此,我们不能在修改数据的方法中使用它,比如boost::to_lower(r)
。
在使用boost::string_view
时,我们必须额外注意它所引用的数据;它必须存在并且在整个boost::string_view
变量的生命周期内都有效。
在 Boost 1.61 之前,没有boost::string_view
类,而是使用boost::string_ref
类。这些类非常接近。boost::string_view
更接近 C++17 的设计,并且具有更好的 constexpr 支持。自 Boost 1.61 以来,boost::string_ref
已被弃用。
string_view
类是快速和高效的,因为它们从不分配内存,也没有虚函数!在任何可能的地方使用它们。它们被设计为const std::string&
和const char*
参数的即插即用替代品。这意味着你可以替换以下三个函数:
void foo(const std::string& s);
void foo(const char* s);
void foo(const char* s, std::size_t s_size);
用一个单一的:
void foo(boost::string_view s);
还有更多...
boost::string_view
类是一个 C++17 类。如果您的编译器兼容 C++17,可以在std::
命名空间的<string_view>
头文件中找到它。
Boost 和标准库的版本支持对string_view
的 constexpr 使用;然而,std::string_view
目前具有更多的标记为 constexpr 的函数。
请注意,我们已经通过值接受了string_view
变量,而不是常量引用。这是传递boost::string_view
和std::string_view
的推荐方式,因为:
-
string_view
是一个具有平凡类型的小类。通过值传递它通常会导致更好的性能,因为减少了间接引用,并且允许编译器进行更多的优化。 -
在其他情况下,当没有性能差异时,编写
string_view val
比编写const string_view& val
更短。
就像 C++17 的std::string_view
一样,boost::string_view
类实际上是一个typedef
:
typedef basic_string_view<char, std::char_traits<char> > string_view;
您还可以在boost::
和std::
命名空间中找到宽字符的以下 typedef:
typedef basic_string_view<wchar_t, std::char_traits<wchar_t> > wstring_view;
typedef basic_string_view<char16_t, std::char_traits<char16_t> > u16string_view;
typedef basic_string_view<char32_t, std::char_traits<char32_t> > u32string_view;
另请参阅
string_ref
和string_view
的 Boost 文档可以在boost.org/libs/utility
找到。
第八章:元编程
在本章中,我们将涵盖:
-
使用类型向量
-
操作类型向量
-
在编译时获取函数的结果类型
-
制作一个高阶元函数
-
延迟评估元函数
-
将所有元组元素转换为字符串
-
拆分元组
-
在 C++14 中操作异构容器
介绍
本章专门介绍一些酷而难以理解的元编程方法。这些方法不是为日常使用而设计的,但它们可能对开发通用库有所帮助。
第四章,编译时技巧,已经涵盖了元编程的基础知识。建议阅读以便更好地理解。在本章中,我们将深入探讨如何将多个类型打包在单个类似元组的类型中。我们将创建用于操作类型集合的函数,看看如何改变编译时集合的类型,以及如何将编译时技巧与运行时混合。所有这些都是元编程。
系好安全带,准备好,让我们开始...!
使用类型向量
有时候,希望能够像在容器中一样处理所有模板参数。想象一下,我们正在编写一些东西,比如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_declared;
typedef variant<
volatile int,
const int,
const long,
declared,
non_declared,
std::string
>::types types;
- 我们可以在编译时检查所有内容。让我们断言类型不为空:
#include <boost/static_assert.hpp>
#include <boost/mpl/empty.hpp>
BOOST_STATIC_ASSERT((!boost::mpl::empty<types>::value));
- 我们还可以检查,例如,
non_declared
类型仍然在索引4
位置:
#include <boost/mpl/at.hpp>
#include <boost/type_traits/is_same.hpp>
BOOST_STATIC_ASSERT((boost::is_same<
non_declared,
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_declared'"
struct non_declared{};
#include <boost/mpl/sizeof.hpp>
typedef boost::mpl::transform<
unique_types,
boost::mpl::sizeof_<boost::mpl::_1>
>::type sizes_types;
- 这是如何从
sizes_type
类型中获取最大大小的:
#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
中使用它。
与标准库容器不同,Boost.MPL
容器没有成员方法。相反,方法在单独的头文件中声明。因此,要使用一些方法,我们需要:
-
包含正确的头文件。
-
通常通过指定容器作为第一个参数来调用该方法。
我们已经在第四章中看到了元函数,编译时技巧。我们使用了一些元函数(如boost::is_same
)来自熟悉的Boost.TypeTraits
库。
因此,在步骤 3、步骤 4和步骤 5中,我们只是为我们的容器类型调用元函数。
最困难的部分即将到来!
占位符被Boost.MPL
库广泛用于组合元函数:
typedef boost::mpl::transform<
types,
boost::remove_cv<boost::mpl::_1>
>::type noncv_types;
在这里,boost::mpl::_1
是一个占位符,整个表达式的意思是,对于 types
中的每种类型,执行 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> t; // 't' stands for 'types'.
std::unique(t.begin(), t.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++17 没有 Boost.MPL
类,而 Boost.MPL
也不使用现代 C++ 的特性,比如可变模板。这使得在 C++11 编译器上,Boost.MPL
的编译时间不会尽可能短,但使得该库可以在 C++03 编译器上使用。
另请参阅
-
参见 第四章,编译时技巧,了解元编程的基础知识
-
操作类型向量 配方将为您提供有关元编程和
Boost.MPL
库的更多信息 -
查看
boost.org/libs/mpl
上的Boost.MPL
官方文档,了解更多示例和完整参考资料
操作类型向量
这个配方的任务是根据第二个 boost::mpl::vector
函数的内容修改一个 boost::mpl::vector
函数的内容。我们将调用第二个向量为修改器向量,每个修改器可能具有以下类型:
// Make unsigned.
struct unsigne; // Not a typo: `unsigned` is a keyword, we can not 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>
#include <boost/mpl/at.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;
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中,我们将Types
、Modifiers
和binary_operator_t
参数从步骤 4传递给boost::mpl::transform
元函数。这个元函数非常简单--对于每个传递的向量,它获取一个元素并将其传递给第三个参数--一个二进制元函数。如果我们用伪代码重写它,它将看起来像下面这样:
void boost_mpl_transform_pseoudo_code() {
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
向量中的每对类型编写一个元函数(请参阅前面的伪代码):
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;
正如我们已经知道的,boost::mpl::_2
和boost::mpl::_1
是占位符。在这个配方中,_1
是Types
向量中类型的占位符,_2
是Modifiers
向量中类型的占位符。
因此,整个元函数的工作方式如下:
-
将传递给它的第二个参数(通过
_2
)与一个unsigned
类型进行比较。 -
如果类型相等,使传递给它的第一个参数(通过
_1
)变为无符号,并返回该类型。 -
否则,它将传递给它的第二个参数(通过
_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 only two parameters overloads.
foo();
还有更多...
使用元函数需要一些实践。即使是您谦卑的仆人也不能在第一次尝试时正确地编写一些函数(尽管第二次和第三次尝试也不好)。不要害怕或困惑去尝试!
Boost.MPL
库不是 C++17 的一部分,也不使用现代 C++特性,但可以与 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
的官方文档在boost.org/libs/mpl
上有更多示例和完整的目录
在编译时获取函数的结果类型
C++11 添加了许多良好的功能,以简化元编程。其中一个功能是替代函数语法。它允许推断模板函数的结果类型。这里是一个例子:
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;
s3 res0 = 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>
typename result_of::my_function_cpp03<T1, T2>::type
my_function_cpp03(const T1& v1, const T2& v2)
{
return v1 + v2;
}
就是这样!现在,我们可以像使用 C++11 一样使用这个函数:
int main() {
s1 v1;
s2 v2;
s3 res1 = my_function_cpp03(v1, v2);
assert(my_function_cpp03('\0', 1) == 1);
}
工作原理...
这个食谱的主要思想是,我们可以制作一个特殊的元函数来推断结果类型。这样的技术可以在 Boost 库的各个地方看到,例如,在Boost.Variant
的boost::get<>
实现中,或者在Boost.Fusion
的几乎任何函数中。
现在,让我们一步一步地进行。result_of
命名空间只是一种传统,但您可以使用自己的,这并不重要。boost::common_type<>
元函数推断出几种类型的公共类型,因此我们将其用于一般情况。我们还为s1
和s2
类型添加了result_of::my_function_cpp03
结构的两个模板特化。
在 C++03 中编写元函数的缺点是,有时我们需要写很多代码。比较my_function_cpp11
和my_function_cpp03
的代码量,包括result_of
命名空间,以感受其中的差异。
当元函数准备好后,我们可以在没有 C++11 的情况下推断出结果类型:
template <class T1, class T2>
typename result_of::my_function_cpp03<T1, T2>::type
my_function_cpp03(const T1& v1, const T2& v2);
还有更多...
这种技术不会增加运行时开销,但可能会稍微减慢编译速度。您也可以在现代 C++编译器上使用它。
另请参阅
-
第四章的食谱启用整数类型的模板函数使用、禁用实数类型的模板函数使用和为模板参数选择最佳运算符将为您提供有关
Boost.TypeTraits
和元编程的更多信息 -
考虑官方文档
Boost.TypeTraits
,了解有关准备好的元函数的更多信息boost.org/libs/type_traits
制作高阶元函数
接受其他函数作为输入参数或返回其他函数的函数称为高阶函数。例如,以下函数是高阶函数:
typedef void(*function_t)(int);
function_t higher_order_function1();
void higher_order_function2(function_t f);
function_t higher_order_function3(function_t f); f);
我们已经在本章的使用类型类型向量和操作类型向量食谱中看到了高阶元函数,我们在那里使用了boost::mpl::transform
。
在这个食谱中,我们将尝试制作自己的高阶元函数,名为coalesce
,它接受两种类型和两个元函数。coalesce
元函数将第一个类型参数应用于第一个元函数,并将结果类型与boost::mpl::false_
类型进行比较。如果结果类型是boost::mpl::false_
类型,则返回将第二个类型参数应用于第二个元函数的结果,否则返回第一个结果类型:
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>
#include <boost/mpl/next.hpp>
using boost::mpl::_1;
using boost::mpl::_2;
typedef coalesce<
boost::mpl::true_,
boost::mpl::int_<5>,
boost::mpl::not_<_1>,
boost::mpl::next<_1>
>::type res1_t;
BOOST_STATIC_ASSERT((res1_t::value == 6));
typedef coalesce<
boost::mpl::false_,
boost::mpl::int_<5>,
boost::mpl::not_<_1>,
boost::mpl::next<_1>
>::type res2_t;
BOOST_STATIC_ASSERT((res2_t::value));
工作原理...
编写高阶元函数的主要问题是要注意占位符。这就是为什么我们不应该直接调用Func1<Param1>::type
。而是必须使用boost::mpl::apply
元函数,它接受一个函数和最多五个参数,这些参数将传递给这个函数。
您可以配置boost::mpl::apply
以接受更多参数,将BOOST_MPL_LIMIT_METAFUNCTION_ARITY
宏定义为所需的参数数量,例如为 6。
还有更多...
C++11 没有任何接近Boost.MPL
库应用元函数的东西。
现代 C++有很多功能,可以帮助你实现Boost.MPL
的功能。例如,C++11 有一个<type_traits>
头文件和基本 constexpr支持。C++14 有扩展 constexpr支持,C++17 有一个可以与元组一起使用并且可以在常量表达式中使用的std::apply
函数。此外,在 C++17 中,lambda 默认是 constexpr,并且有一个if constexpr(expr)。
编写自己的解决方案会浪费很多时间,而且可能在旧编译器上无法工作。因此,Boost.MPL
仍然是最适合元编程的解决方案之一。
另请参阅
查看官方文档,特别是Tutorial部分,了解有关Boost.MPL
的更多信息,请访问boost.org/libs/mpl
。
惰性评估元函数
惰性评估意味着在真正需要其结果之前不会调用函数。了解这个方法对于编写良好的元函数非常重要。惰性评估的重要性将在以下示例中展示。
想象一下,我们正在编写一些元函数,它接受一个函数Func
,一个参数Param
和一个条件Cond
。如果将Cond
应用于Param
返回false
,那么该函数的结果类型必须是一个fallback
类型,否则结果必须是将Func
应用于Param
的结果:
struct fallback;
template <
class Func,
class Param,
class Cond,
class Fallback = fallback>
struct apply_if;
这个元函数是我们无法离开惰性评估的地方,因为如果Cond
不满足,可能无法将Func
应用于Param
。这样的尝试总是会导致编译失败,并且永远不会返回Fallback
。
准备工作
阅读第四章,Compile-time Tricks,是非常推荐的。然而,对元编程的良好了解应该足够了。
如何做...
注意一些小细节,比如在示例中不调用::type
:
- 我们需要以下头文件:
#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 assertion somewhere deeply in the implementation
// of boost::make_unsigned<_1> if we do not evaluate the function lazily.
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
));
那么,我们如何才能惰性评估元函数呢?
如果没有访问元函数的内部类型或值,编译器将不会查看元函数的内部。换句话说,当我们通过::
尝试获取其成员之一时,编译器会尝试编译元函数。这可以是对::type
或::value
的调用。这就是apply_if
的不正确版本的样子:
template <class Func, class Param, class Cond, class Fallback>
struct apply_if {
typedef typename boost::mpl::apply<
Cond, Param
>::type 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; // call `::type` only for one parameter
};
因为boost::mpl::eval_if_c
对于成功的条件调用了::type
,而fallback
没有::type
,所以我们需要将fallback
包装到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
类型可用于禁用模板函数的Argument Dependent Lookup(ADL)。请参阅<boost/implicit_cast.hpp>
头文件中boost::implicit_cast
的源代码。 -
从头开始阅读本章和
Boost.MPL
的官方文档,网址为boost.org/libs/mpl
,可能会有所帮助。
将所有元组元素转换为字符串
这个配方和下一个配方都致力于混合编译时和运行时特性。我们将使用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;
}
到此为止!现在,我们可以将任何想要的东西转换为字符串:
#include <iostream>
#include <boost/fusion/include/vector.hpp>
#include <boost/fusion/adapted/boost_tuple.hpp>
#include <boost/fusion/adapted/std_pair.hpp>
#include <boost/fusion/adapted/boost_array.hpp>
struct cat{};
std::ostream& operator << (std::ostream& os, const cat& ) {
return os << "Meow! ";
}
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 <string>
#include <cassert>
#include <boost/tuple/tuple.hpp>
#include <boost/fusion/include/vector.hpp>
#include <boost/fusion/include/at_c.hpp>
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");
}
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()
在编译时知道迭代次数,并完全展开循环。对于boost::tuple<cat, int, std::string> tup2
,boost::fusion::for_each(tup2, functor)
调用等同于以下代码:
functor(boost::fusion::at_c<0>(tup2));
functor(boost::fusion::at_c<1>(tup2));
functor(boost::fusion::at_c<2>(tup2));
C++11 不包含Boost.Fusion
类。Boost.Fusion
的所有方法都非常有效。它们尽可能多地在编译时执行,并具有一些非常高级的优化。
C++14 添加了std::integer_sequence
和std::make_integer_sequence
来简化使用可变模板的for
。使用这些实体,可以手动编写boost::fusion::for_each
功能,并在没有Boost.Fusion
的情况下实现stringize
函数:
#include <utility>
#include <tuple>
template <class Tuple, class Func, std::size_t... I>
void stringize_cpp11_impl(const Tuple& t, const Func& f, std::index_sequence<I...>) {
// Oops. Requires C++17 fold expressions feature.
// (f(std::get<I>(t)), ...);
int tmp[] = { 0, (f(std::get<I>(t)), 0)... };
(void)tmp; // Suppressing unused variable warnings.
}
template <class Tuple>
std::string stringize_cpp11(const Tuple& t) {
std::string result;
stringize_cpp11_impl(
t,
stringize_functor(result),
std::make_index_sequence< std::tuple_size<Tuple>::value >()
);
return result;
}
正如你所看到的,有很多代码被编写来做到这一点,这样的代码并不容易阅读和理解。
关于在 C++20 标准中添加类似于constexpr for
的功能的想法在 C++标准化工作组中进行了讨论。有了这个功能,有一天我们可以编写以下代码(语法可能会改变!):
template <class Tuple>
std::string stringize_cpp20(const Tuple& t) {
std::string result;
for constexpr(const auto& v: t) {
result += boost::lexical_cast<std::string>(v);
}
return result;
}
在那之前,Boost.Fusion
似乎是最通用和简单的解决方案。
另请参阅
-
拆分元组配方将提供有关
Boost.Fusion
真正能力的更多信息。 -
Boost.Fusion
的官方文档包含一些有趣的例子和完整的参考资料,可以在boost.org/libs/fusion
找到
拆分元组
这个配方将展示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>
#include <boost/blank.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_nonarithmetics
。首先,它的结果类型是使用以下结构推导出来的:
typename boost::fusion::result_of::remove_if<
const Sequence,
boost::is_arithmetic<boost::mpl::_1>
>::type
这对我们来说应该很熟悉。我们在本章的在编译时获取函数结果类型配方中看到了类似的东西。Boost.MPL
的占位符boost::mpl::_1
与boost::fusion::result_of::remove_if
元函数很搭配,它返回一个新的序列类型。
现在,让我们进入函数内部,看看以下代码:
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.Fusion
容器使用Boost.MPL
函数。您只需要包含#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_STATIC_ASSERT((boost::is_same<
boost::fusion::result_of::value_at_c<nc_type, 1>::type,
boost::blank
>::value));
BOOST_STATIC_ASSERT((boost::is_same<
boost::fusion::result_of::value_at_c<nc_type, 2>::type,
boost::blank
>::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++17 的一部分。Boost.Fusion
非常快。它有许多优化。
值得一提的是,我们只看到了Boost.Fusion
能力的一小部分。可以写一本单独的书来介绍它。
另请参阅
-
Boost.Fusion
的良好教程和完整文档可在boost.org/libs/fusion
上找到。 -
您可能还希望查看
boost.org/libs/mpl
上的Boost.MPL
的官方文档
在 C++14 中操作异构容器
本章中我们看到的大多数元编程技巧都是在 C++11 之前发明的。可能你已经听说过其中的一些东西。
怎么样来点全新的?怎么样用 C++14 实现上一个配方,使用一个将元编程颠倒过来并让你眉毛竖起来的库?系好安全带,我们要进入Boost.Hana
的世界了。
准备工作
这个配方需要了解 C++11 和 C++14,特别是 lambda 表达式。您需要一个真正兼容 C++14 的编译器来编译示例。
如何做...
现在,让我们用Boost.Hana
的方式来做一切:
- 从包含头文件开始:
#include <boost/hana/traits.hpp>
- 我们创建了一个
is_arithmetic_
函数对象:
constexpr auto is_arithmetic_ = [](const auto& v) {
auto type = boost::hana::typeid_(v);
return boost::hana::traits::is_arithmetic(type);
};
- 现在,我们实现
get_nonarithmetics
函数:
#include <boost/hana/remove_if.hpp>
template <class Sequence>
auto get_nonarithmetics(const Sequence& seq) {
return boost::hana::remove_if(seq, [](const auto& v) {
return is_arithmetic_(v);
});
}
- 让我们用另一种方式定义
get_arithmetics
。就是为了好玩!
#include <boost/hana/filter.hpp>
constexpr auto get_arithmetics = [](const auto& seq) {
return boost::hana::filter(seq, is_arithmetic_);
};
就是这样。现在,我们可以使用这些函数:
#include <boost/hana/tuple.hpp>
#include <boost/hana/integral_constant.hpp>
#include <boost/hana/equal.hpp>
#include <cassert>
struct foo {
bool operator==(const foo&) const { return true; }
bool operator!=(const foo&) const { return false; }
};
int main() {
const auto tup1
= boost::hana::make_tuple(8, foo{}, foo{}, 0.0);
const auto res_na = get_nonarithmetics(tup1);
const auto res_a = get_arithmetics(tup1);
using boost::hana::literals::operator ""_c;
assert(res_a[0_c] == 8);
const auto res_na_expected = boost::hana::make_tuple(foo(), foo());
assert(res_na == res_na_expected);
}
工作原理...
乍一看,代码可能看起来很简单,但事实并非如此。Boost.Hana
将元编程颠倒过来了!在以前的配方中,我们直接使用类型,但Boost.Hana
创建了一个保存类型并大部分时间使用变量的变量。
看一下步骤 2中的typeid_
调用:
auto type = boost::hana::typeid_(v);
它实际上返回一个变量。有关类型的信息现在隐藏在type
变量内部,并且可以通过调用decltype(type)::type
来提取。
但让我们一行一行地来。在步骤 2中,我们将通用 lambda 存储在is_arithmetic_
变量中。从这一点开始,我们可以将该变量用作函数对象。在 lambda 内部,我们创建了一个type
变量,它现在保存了有关v
类型的信息。下一行是对std::is_arithmetic
的特殊包装,它从type
变量中提取有关v
类型的信息,并将其传递给std::is_arithmetic
特性。该调用的结果是一个布尔整数常量。
现在,神奇的部分来了!存储在is_arithmetic_
变量内的 lambda 实际上从未被boost::hana::remove_if
和boost::hana::filter
函数调用。所有使用它的Boost.Hana
函数只需要 lambda 函数的结果类型,而不需要它的主体。我们可以安全地更改定义,整个示例将继续正常工作:
constexpr auto is_arithmetic_ = [] (const auto& v) {
assert(false);
auto type = boost::hana::typeid_(v);
return boost::hana::traits::is_arithmetic(type);
};
在步骤 3和4中,我们分别调用boost::hana::remove_if
和boost::hana::filter
函数。在步骤 3中,我们在 lambda 内部使用了is_arithmetic_
。在步骤 4中,我们直接使用了它。你可以使用任何你喜欢的语法,这只是一个习惯问题。
最后在main()
中,我们检查一切是否按预期工作,并且元组中索引为 0 的元素是否等于8
:
using boost::hana::literals::operator ""_c;
assert(res_a[0_c] == 8);
理解Boost.Hana
库的最佳方法是进行实验。你可以在apolukhin.github.io/Boost-Cookbook/
上在线进行。
还有更多...
还有一个小细节没有描述。operator[]
如何访问元组?不可能有一个单一的函数返回不同的类型!
如果你第一次遇到这个技巧,这是非常有趣的。Boost.Hana
的operator ""_c
可以与文字一起工作,并根据文字构造不同的类型:
-
如果你写
0_c
,那么将返回integral_constant<long long, 0>
-
如果你写
1_c
,那么将返回integral_constant<long long, 1>
-
如果你写
2_c
,那么将返回integral_constant<long long, 2>
boost::hana::tuple
类实际上有许多operator[]
重载,接受不同类型的integral_constant
。根据整数常量的值,返回正确的元组元素。例如,如果你写some_tuple[1_c]
,那么将调用tuple::operator[](integral_constant<long long, 1>)
,并返回索引为1
的元素。
Boost.Hana
不是 C++17 的一部分。然而,该库的作者参与了 C++标准化会议,并提出了不同的有趣事物,以纳入 C++标准。
如果你期望从Boost.Hana
获得比从Boost.MPL
更好的编译时间,那就不要指望了。目前编译器对Boost.Hana
的方法处理得并不是非常好。也许有一天会改变。
值得一看Boost.Hana
库的源代码,以发现使用 C++14 特性的新有趣方法。所有 Boost 库都可以在 GitHub 上找到github.com/boostorg
。
另请参阅
官方文档中有更多示例,完整的参考部分,一些更多的教程,以及一个编译时性能部分。在boost.org/libs/hana
上享受Boost.Hana
库。
第九章:容器
在本章中,我们将涵盖:
-
在序列容器中存储少量元素
-
在序列容器中存储大多数 N 个元素
-
以超快速度比较字符串
-
使用无序集和映射
-
制作一个地图,其中值也是一个键
-
使用多索引容器
-
获得单链表和内存池的好处
-
使用扁平的关联容器
介绍
本章专门介绍了 Boost 容器及与其直接相关的内容。它提供了关于 Boost 类的信息,这些类可以在日常编程中使用,并且可以使您的代码更快,新应用程序的开发更容易。
容器不仅在功能上有所不同,而且在某些成员的效率(复杂性)上也有所不同。了解复杂性对于编写快速应用程序至关重要。本章不仅向您介绍了一些新的容器,还为您提供了关于何时以及何时不使用特定类型的容器或其方法的建议。
所以,让我们开始吧!
在序列容器中存储少量元素
在过去的 20 年里,C++程序员一直将std::vector
作为默认的序列容器。它是一个快速的容器,不会进行大量的分配,以 CPU 缓存友好的方式存储元素,并且因为容器连续存储元素,std::vector::data()
等函数允许与纯 C 函数进行交互。
但是,我们想要更多!有些情况下,我们知道要在向量中存储的典型元素数量,并且我们需要通过完全消除该情况下的内存分配来提高向量的性能。
想象一下,我们正在编写一个高性能的用于处理银行交易的系统。交易是一系列操作,如果其中至少有一个操作失败,那么所有操作都必须成功或失败。我们知道 99%的交易由 8 个或更少的操作组成,并希望加快处理速度:
#include <vector>
class operation;
template <class T>
void execute_operations(const T&);
bool has_operation();
operation get_operation();
void process_transaction_1() {
std::vector<operation> ops;
ops.reserve(8); // TODO: Memory allocation. Not good!
while (has_operation()) {
ops.push_back(get_operation());
}
execute_operations(ops);
// ...
}
准备就绪
这个示例只需要基本的标准库和 C++知识。
如何做...
这将是本书中最简单的任务,这要归功于Boost.Container
库:
- 包括适当的头文件:
#include <boost/container/small_vector.hpp>
- 用
boost::container::small_vector
替换std::vector
并删除reserve()
调用:
void process_transaction_2() {
boost::container::small_vector<operation, 8> ops;
while (has_operation()) {
ops.push_back(get_operation());
}
execute_operations(ops);
// ...
}
它是如何工作的...
boost::container::small_vector
的第二个模板参数是要在堆栈上预分配的元素数量。因此,如果大多数情况下我们需要在向量中存储 8 个或更少的元素,我们只需将8
作为第二个模板参数。
如果我们需要在容器中存储超过 8 个元素,那么small_vector
的行为就与std::vector
完全相同,并动态分配一块内存来存储超过 8 个元素。就像std::vector
一样,small_vector
是一个具有随机访问迭代器的序列容器,它一致地存储元素。
总之,boost::container::small_vector
是一个行为与std::vector
完全相同的容器,但允许在编译时指定的元素数量避免内存分配。
还有更多...
使用small_vector
的一个缺点是,我们的元素数量假设泄漏到接受small_vector
作为参数的函数签名中。因此,如果我们有三个专门用于分别处理4
、8
和16
个元素的函数,并且所有这些函数都使用前面示例中的execute_operations
进行事务处理,我们将得到execute_operations
函数的多个实例化:
void execute_operations(
const boost::container::small_vector<operation, 4>&);
void execute_operations(
const boost::container::small_vector<operation, 8>&);
void execute_operations(
const boost::container::small_vector<operation, 16>&);
这不好!现在,我们的可执行文件中有多个函数执行完全相同的操作,并且几乎完全由相同的机器代码组成。这会导致更大的二进制文件,可执行文件启动时间更长,编译和链接时间更长。一些编译器可能会消除冗余,但机会很低。
然而,解决方案非常简单。boost::container::small_vector
是从boost::container::small_vector_base
类型派生的,该类型独立于预分配的元素数量:
void execute_operations(
const boost::container::small_vector_base<operation>& ops
);
就是这样!现在,我们可以在任何boost::container::small_vector
中使用新的execute_operations
函数,而不会使二进制大小膨胀。
C++17 没有像small_vector
这样的类。有提案将small_vector
包含在下一个 C++标准中,该标准将在 2020 年左右发布。
另请参阅
-
Boost.Container
库在boost.org/libs/container
上为许多有趣的类提供了完整的参考文档 -
small_vector
来自LLVM项目的 Boost;您可以在原始网站llvm.org/docs/ProgrammersManual.html#llvm-adt-smallvector-h
上阅读有关该容器的信息
在序列容器中存储最多 N 个元素
这里有一个问题:如果我们知道序列永远不会超过N个元素,而N不大,那么我们应该使用什么容器来从函数中返回序列。例如,我们必须如何编写get_events()
函数,以返回最多五个事件:
#include <vector>
std::vector<event> get_events();
std::vector<event>
分配内存,因此先前的代码不是一个好的解决方案。
#include <boost/array.hpp>
boost::array<event, 5> get_events();
boost::array<event, 5>
不分配内存,但它构造了所有五个元素。没有办法返回少于五个元素。
#include <boost/container/small_vector.hpp>
boost::container::small_vector<event, 5> get_events();
boost::container::small_vector<event, 5>
不会为五个或更少的元素分配内存,并允许我们返回少于五个元素。但是,这个解决方案并不完美,因为从函数接口中并不明显它永远不会返回超过五个元素。
准备就绪
这个教程只需要对标准库和 C++有基本的了解。
如何做到...
Boost.Container
有一个完全满足我们需求的容器:
#include <boost/container/static_vector.hpp>
boost::container::static_vector<event, 5> get_events();
它是如何工作的...
boost::container::static_vector<T, N>
是一个不分配内存并且最多可以容纳编译时指定数量的元素的容器。可以将其视为boost::container::small_vector<T, N>
,只是它不能动态分配内存,任何尝试存储超过N个元素的操作都会导致std::bad_alloc
异常:
#include <cassert>
int main () {
boost::container::static_vector<event, 5> ev = get_events();
assert(ev.size() == 5);
boost::container::static_vector<int, 2> ints;
ints.push_back(1);
ints.push_back(2);
try {
// The following line always throws:
ints.push_back(3);
} catch (const std::bad_alloc& ) {
// ...
}
}
就像Boost.Container
库的所有容器一样,static_vector
支持移动语义,并且在编译器不支持 rvalues 的情况下使用 Boost.Move 库模拟 rvalue 引用。
还有更多...
std::vector
如果用户插入一个元素并且无法将新值放入已分配的内存中,则会分配更大的内存块。在这种情况下,如果元素是无异常移动可构造的,则std::vector
会将元素从旧位置移动到新位置。否则,std::vector
会将元素复制到新位置,然后调用旧位置中每个元素的析构函数。
因此,std::vector
的行为对于许多成员函数具有摊销常数复杂度。static_vector
从不分配内存,因此不必将元素从旧位置移动或复制到新位置。因此,对于std::vector
具有摊销 O(1)复杂度的操作,对于boost::container::static_vector
具有真正的 O(1)复杂度。这对于一些实时应用可能很方便;但是要注意异常!
有些人仍然更喜欢通过引用传递输出参数,而不是返回它们:void get_events(static_vector<event, 5>& result_out)
。他们认为这样可以保证不会发生结果的复制。不要这样做,这会使情况变得更糟!C++编译器有一整套优化,例如返回值优化(RVO)和命名返回值优化(NRVO);不同的平台在 ABI 中已经约定,带有retun something;
的代码不会导致不必要的复制等等。已经不会发生复制。但是,当您传递一个值时,引用编译器只是看不到值来自何处,并且可能会假定它与作用域中的其他值有别名。这可能会严重降低性能。
C++17 没有static_vector
类,目前也没有计划将其添加到 C++20 中。
另请参阅
Boost.Container
的官方文档有一个详细的参考部分,描述了boost::container::static_vector
类的所有成员函数。参考boost.org/libs/container.
以超快速的方式比较字符串
操作字符串是一个常见的任务。在这里,我们将看到如何使用一些简单的技巧快速进行字符串比较操作。这个教程是下一个教程的跳板,这里描述的技术将用于实现常数时间复杂度搜索。
因此,我们需要创建一个能够快速比较字符串是否相等的类。我们将创建一个模板函数来测量比较的速度:
#include <string>
template <class T>
std::size_t test_default() {
// Constants
const std::size_t ii_max = 200000;
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 data1[] = {
T(s),
T(s + s),
T(s + ". Whooohooo"),
T(std::string(""))
};
const T data2[] = {
T(s),
T(s + s),
T(s + ". Whooohooo"),
T(std::string(""))
};
const std::size_t data_dimensions = sizeof(data1) / sizeof(data1[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 (data1[i] == data2[j]) {
++ matches;
}
}
}
}
return matches;
}
准备工作
这个教程只需要基本的标准库和 C++知识。
如何做...
我们将在我们自己的类中将std::string
作为公共字段,并将所有比较代码添加到我们的类中,而不是编写用于处理存储的std::string
的辅助方法,如下面的步骤所示:
- 为了这样做,我们需要以下标题:
#include <boost/functional/hash.hpp>
- 现在,我们可以创建我们的
fast comparison_
类:
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)
{}
};
- 不要忘记定义
equality comparisons
运算符:
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>
#include <iostream>
#include <cassert>
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:
return 2;
}
}
它是如何工作的...
字符串的比较很慢,因为我们需要逐个比较字符串的所有字符,如果字符串长度相等的话。我们不是这样做,而是用整数的比较代替字符串的比较。这是通过hash
函数完成的-这个函数生成字符串的一些短固定长度的表示。
让我们谈谈苹果上的hash
值。想象一下,你有两个带标签的苹果,如下图所示,你希望检查这些苹果是否属于同一品种。比较这些苹果最简单的方法是通过比较它们的标签。否则,基于颜色、大小、形状和其他参数比较苹果会浪费很多时间。Hash 就像是一个反映对象值的标签。
现在,让我们一步一步地进行。
在步骤 1中,我们包含包含hash
函数定义的头文件。在步骤 2中,我们声明了我们的新string
类,其中包含str_
,这是字符串的原始值,以及comparison_
,这是计算出的hash
值。注意构造:
boost::hash<std::string>()(s)
在这里,boost::hash<std::string>
是一个结构,一个功能对象,就像std::negate<>
一样。这就是为什么我们需要第一个括号--我们构造了那个功能对象。带有s
的第二个括号是对std::size_t operator()(const std::string& s)
的调用,它计算了hash
值。
现在,看一下步骤 3,我们定义operator==
:
return s1.comparison_ == s2.comparison_ && s1.str_ == s2.str_;
对表达式的第二部分要特别小心。哈希操作会丢失信息,这意味着可能有多个字符串产生完全相同的hash
值。这意味着如果哈希不匹配,那么可以百分之百保证字符串不匹配;否则,我们需要使用传统方法比较字符串。
好了,现在是比较数字的时候了。如果我们使用默认的比较方法来测量执行时间,它将给出 819 毫秒;然而,我们的哈希比较工作几乎快两倍,并在 475 毫秒内完成。
还有更多...
C++11 有hash
函数对象;你可以在std::
命名空间的<functional>
头文件中找到它。Boost 和标准库中的哈希是快速可靠的。它不会分配额外的内存,也不会有虚拟函数。
你可以为自己的类型专门设计哈希。在 Boost 中,通过在自定义类型的命名空间中专门设计hash_value
函数来实现:
// Must be in the namespace of string_hash_fast class.
inline std::size_t hash_value(const string_hash_fast& v) {
return v.comparison_;
}
这与std::hash
的标准库专门化不同,其中你需要在std::
命名空间中对hash<>
结构进行模板专门化。
在 Boost 中,对于所有基本类型(如int
、float
、double
和char
)、数组以及所有标准库容器,包括std::array
、std::tuple
和std::type_index
,都定义了哈希。一些库还提供了哈希专门化,例如Boost.Variant
库可以哈希任何boost::variant
类。
另请参阅
-
在本章中阅读使用无序集和映射配方,了解有关哈希函数使用的更多信息。
-
Boost.Functional/Hash
的官方文档将告诉你如何组合多个哈希并提供更多示例;在boost.org/libs/functional/hash
上阅读有关它的信息。
使用无序集和映射
在前面的配方中,我们看到了如何使用哈希来优化字符串比较。阅读之后,可能会产生以下问题:我们是否可以创建一个容器,以便缓存哈希值以便更快地进行比较?
答案是肯定的,我们还可以做更多。我们可以几乎实现常量搜索、插入和删除元素的时间。
准备工作
需要基本的 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>
#include <string>
#include <cassert>
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
那么,性能差异有多大呢?通常取决于实现的质量。我得到了以下数字:
For 100 elements:
Boost: map is 1.69954 slower than unordered map
Std: map is 1.54316 slower than unordered map
For 1000 elements:
Boost: map is 4.13714 slower than unordered map
Std: map is 2.12495 slower than unordered map
For 10000 elements:
Boost: map is 2.04475 slower than unordered map
Std: map is 2.23285 slower than unordered map
For 100000 elements:
Boost: map is 1.67128 slower than unordered map
Std: map is 1.68169 slower than unordered map
性能是使用以下代码块进行测量的:
T map;
for (std::size_t ii = 0; ii < ii_max; ++ii) {
map[s + boost::lexical_cast<std::string>(ii)] = ii;
}
// Asserting.
for (std::size_t ii = 0; ii < ii_max; ++ii) {
assert(map[s + boost::lexical_cast<std::string>(ii)] == ii);
}
代码包含了很多字符串构造,因此使用这个测试来衡量加速并不是 100%正确。这里只是为了表明无序容器通常比有序容器更快。
有时,可能会出现需要在无序容器中使用用户定义类型的任务:
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>
头文件中。就像标准库的情况一样,容器的多个版本能够存储多个相等的键值。
所有无序容器都允许您指定自己的哈希函数,而不是默认的boost::hash
。它们还允许您专门化自己的相等比较函数,而不是默认的std::equal_to
。
C++11 具有 Boost 库的所有无序容器。您可以在头文件<unordered_set>
和<unordered_map>
中找到它们,它们位于std::
命名空间中,而不是boost::
。Boost 和标准库版本在性能上可能有所不同,但必须以相同的方式工作。但是,Boost 的无序容器甚至可以在 C++03/C++98 编译器上使用,并利用了Boost.Move
的右值引用模拟,因此即使在 C++11 之前的编译器上,您也可以使用这些容器来处理仅移动的类。
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 1.64 以来,Boost 中的无序容器具有 C++17 的功能,用于提取和插入节点。
另请参阅
-
有关
Boost.Move
的右值引用模拟的更多详细信息,请参阅第一章中的教程使用 C++11 移动模拟。 -
有关无序容器的更多信息,请访问官方网站
boost.org/libs/unordered
-
有关组合哈希和计算范围哈希的更多信息,请访问
boost.org/libs/functional/hash
制作一个映射,其中值也是一个键
一年中有几次,我们需要一些可以存储和索引一对值的东西。此外,我们需要使用第二个部分来获取第一部分,并使用第一部分来获取第二部分。感到困惑了吗?让我给你举个例子。我们创建一个词汇表类。当用户将值放入其中时,该类必须返回标识符,当用户将标识符放入其中时,该类必须返回值。
为了更实际,用户们将登录名放入我们的词汇表中,并希望从中获取唯一标识符。他们还希望获取标识符的所有登录名。
让我们看看如何使用 Boost 来实现它。
准备工作
此教程需要对标准库和模板有基本的了解。
如何做...
这个教程是关于Boost.Bimap
库的能力。让我们看看它如何用于实现这个任务:
- 我们需要以下包含:
#include <iostream>
#include <boost/bimap.hpp>
#include <boost/bimap/multiset_of.hpp>
- 现在,我们准备制作我们的词汇结构:
int main() {
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;
const left_const_iterator lend = name_id.left.end();
for (left_const_iterator it = name_id.left.begin();
it!= lend;
++it)
{
std::cout << it->first << " <=> " << it->second << '\n';
}
- 右边部分几乎与左边相同:
std::cout << "\nRight:\n";
typedef name_id_type::right_const_iterator right_const_iterator;
const right_const_iterator rend = name_id.right.end();
for (right_const_iterator it = name_id.right.begin();
it!= rend;
++it)
{
std::cout << it->first << " <=> " << it->second << '\n';
}
- 我们还需要确保词汇表中有这样的人:
assert(
name_id.find(name_id_type::value_type(
"Anton Polukhin", 3
)) != name_id.end()
);
} /* end of main() */
就是这样,现在如果我们将所有代码(除了包含)放在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
作为默认行为。就像在我们的示例中一样,我们可以尝试使用标准库来表达以下代码:
#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;
使用标准库,它看起来像以下两个变量的组合:
std::map<std::string, std::size_t> key1; // == name_id.left
std::multimap<std::size_t, std::string> key2; // == name_id.right
从前面的评论中可以看出,调用name_id.left
(在步骤 4中)返回一个接口接近于std::map<std::string, std::size_t>
的引用。从步骤 5中调用name_id.right
返回一个接口接近于std::multimap<std::size_t, std::string>
的东西。
在步骤 6中,我们使用整个bimap
,搜索一对键,并确保它们在容器中。
还有更多...
不幸的是,C++17 没有类似于Boost.Bimap
的东西。以下是一些其他坏消息:
Boost.Bimap
不支持右值引用,在一些编译器上会显示大量警告。请参考您的编译器文档,了解如何抑制特定警告。
好消息是,Boost.Bimap
通常比两个标准库容器使用更少的内存,并且使搜索速度与标准库容器一样快。它内部没有虚函数调用,而是使用动态分配。
另请参阅
-
下一个配方,使用多索引容器,将为您提供有关多索引和可以用来替代
Boost.Bimap
的 Boost 库的更多信息 -
阅读官方文档,了解有关
bimap
的更多示例和信息,网址为boost.org/libs/bimap
使用多索引容器
在前面的示例中,我们制作了一种词汇,当我们需要处理成对时,这是很好的。但是,如果我们需要更高级的索引呢?让我们制作一个索引人员的程序:
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、身高和体重。
准备工作
需要基本了解标准库容器和无序映射。
如何做...
所有的索引都可以由单个Boost.Multiindex
容器构造和管理。
- 为此,我们需要很多包含:
#include <iostream>
#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>
- 最困难的部分是构造
multi-index
类型:
void example_main() {
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 them 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;
- 现在,我们可以将值插入到我们的
multi-index
中:
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't 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 容器之间的关系:
Boost.MultiIndex 类型 |
STL 容器 |
---|---|
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_mutiset |
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 结构的单个成员字段person::id_
应用哈希函数。
剩下的索引现在不会有麻烦了;所以让我们看看在print
函数中使用索引的用法。使用以下代码可以获取特定索引的迭代器类型:
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++17 没有多索引库。Boost.MultiIndex
是一个快速库,不使用虚拟函数。Boost.MultiIndex
的官方文档包含性能和内存使用情况的测量,显示该库在大多数情况下使用的内存比基于标准库的手写代码少。不幸的是,boost::multi_index::multi_index_container
不支持 C++11 特性,也没有使用Boost.Move
进行右值引用模拟。
另请参阅
Boost.MultiIndex
的官方文档包含教程、性能测量、示例和其他Boost.Multiindex
库的有用功能描述。请在boost.org/libs/multi_index
上阅读相关内容。
获得单链表和内存池的好处
如今,当我们需要非关联和非有序的容器时,我们通常使用std::vector
。这是由Andrei Alexandrescu和Herb Sutter在书籍C++ Coding Standards中推荐的。即使没有读过这本书的用户通常也使用std::vector
。为什么呢?嗯,std::list
更慢,使用的资源比std::vector
多得多。std::deque
容器非常接近std::vector
,但不连续存储值。
如果我们需要一个容器,其中删除和插入元素不会使迭代器失效,那么我们被迫选择一个慢的std::list
。
但是,等等,我们可以使用 Boost 组装一个更好的解决方案!
准备工作
需要对标准库容器有良好的了解才能理解介绍部分。之后,只需要基本的 C++和标准库容器的知识。
如何做...
在这个示例中,我们将同时使用两个 Boost 库:Boost.Pool
和Boost.Container
中的单链表。
- 我们需要以下头文件:
#include <boost/pool/pool_alloc.hpp>
#include <boost/container/slist.hpp>
#include <cassert>
- 现在,我们需要描述我们列表的类型。可以按照以下代码进行操作:
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 is still valid and points to the 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 is still valid and points to the same value
assert(it != list.end());
assert(*it == 777);
}
void test_slist() {
test_lists<slist_t>();
}
void test_list() {
test_lists<std::list<int> >();
}
- 一些特定于列表的函数:
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: slist rebinds allocator_t and allocates
// nodes of the slist, not just ints.
boost::singleton_pool<
boost::fast_pool_allocator_tag,
sizeof(slist_t::stored_allocator_type::value_type)
>::release_memory();
} // end of list_specific function
工作原理...
当我们使用std::list
时,可能会注意到减速,因为列表的每个节点都需要单独分配。这意味着通常当我们向std::list
插入 10 个元素时,容器会调用 10 次new
。此外,分配的节点通常位于内存中随机位置,这对 CPU 缓存不友好。
这就是为什么我们使用了 Boost.Pool 中的 Boost ::fast_pool_allocator<int>
。这个分配器尝试分配更大的内存块,这样在后期,可以构造多个节点而不需要多次调用new
。
Boost.Pool
库有一个缺点——它使用内存来满足内部需求。通常,每个元素会额外使用sizeof(void*)
的内存。为了解决这个问题,我们使用了Boost.Containers
中的单链表。
boost::container::slist
类更加紧凑,但其迭代器只能向前迭代。对于了解标准库容器的读者来说,步骤 3很简单,所以我们转到步骤 4来看一些boost::container::slist
的特定功能。由于单链表迭代器只能向前迭代,插入和删除的传统算法需要线性时间 O(N)。这是因为在擦除或插入时,列表的前一个元素必须被修改。为了解决这个问题,单链表有erase_after
和insert_after
方法,可以在常数时间 O(1)内工作。这些方法在当前迭代器位置之后插入或擦除元素。
然而,在单链表的开头删除和插入值并没有太大的区别。
仔细看一下以下代码:
boost::singleton_pool<
boost::fast_pool_allocator_tag,
sizeof(slist_t::stored_allocator_type::value_type)
>::release_memory();
这是必需的,因为 boost::fast_pool_allocator
不会释放内存,所以我们必须手动释放。第二章 中的 在作用域退出时执行某些操作 示例,管理资源,可能有助于释放 Boost.Pool
。
让我们来看一下执行时间,感受一下其中的差异:
$ TIME="Runtime=%E RAM=%MKB" time ./07_slist_and_pool l
std::list: Runtime=0:00.08 RAM=34224KB
$ TIME="Runtime=%E RAM=%MKB" time ./07_slist_and_pool s
slist_t: Runtime=0:00.04 RAM=19640KB
正如我们所看到的,slist_t
使用了一半的内存,并且比 std::list
类快两倍。
还有更多...
Boost.Container
库实际上有一个开箱即用的解决方案,称为 boost::container::stable_vector
。后者允许对元素进行随机访问,具有随机访问迭代器,但具有 std::list
大部分性能和内存使用缺点。
C++11 有 std::forward_list
,它与 boost::containers::slist
非常接近。它也有 *_after
方法,但没有 size()
方法。C++11 和 Boost 版本的单链表具有相同的性能,它们都没有虚函数。然而,Boost 版本也可以在 C++03 编译器上使用,并且甚至支持通过 Boost.Move
对右值引用进行模拟。
boost::fast_pool_allocator
不在 C++17 中。然而,C++17 有一个更好的解决方案!头文件 <memory_resource>
包含了一些有用的内容,可以用于处理多态分配器,在那里你可以找到 std::pmr::synchronized_pool_resource
、std::pmr::unsynchronized_pool_resource
和 std::pmr::monotonic_buffer_resource
。尝试使用这些内容来实现更好的性能。
猜想为什么 boost::fast_pool_allocator
不会自动释放内存?那是因为 C++03 没有有状态的分配器,所以容器不会复制和存储分配器。这使得不可能实现一个可以自动释放内存的 boost::fast_pool_allocator
函数。
另请参阅
-
Boost.Pool
的官方文档包含了更多的示例和类,用于处理内存池。点击链接boost.org/libs/pool
了解更多信息。 -
使用平面关联容器 示例将向您介绍
Boost.Container
中的一些其他类。您也可以在boost.org/libs/container
阅读Boost.Container
的官方文档,自行学习该库或获取其类的完整参考文档。 -
Vector vs List,以及 C++ 编程语言的发明者 Bjarne Stroustrup 的其他有趣主题,可以在
channel9.msdn.com/Events/GoingNative/GoingNative-2012/Keynote-Bjarne-Stroustrup-Cpp11-Style
网站找到。
使用平面关联容器
在阅读了前面的示例之后,一些读者可能会开始在各处使用快速池分配器;特别是对于 std::set
和 std::map
。好吧,我不会阻止你这样做,但至少让我们看看另一种选择:平面关联容器。这些容器是在传统的向量容器之上实现的,并按顺序存储值。
准备工作
需要基本了解标准库关联容器。
如何做...
平面容器是 Boost.Container
库的一部分。我们已经看到了如何在之前的示例中使用它的一些容器。在这个示例中,我们将使用 flat_set
关联容器:
- 我们只需要包含一个头文件:
#include <boost/container/flat_set.hpp>
- 之后,我们可以自由地构造平面容器并进行实验:
#include <algorithm>
#include <cassert>
int main() {
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)
);
} // end of main() function
工作原理...
步骤 1 和 步骤 2 非常简单,但 步骤 3 需要注意。这是在使用平面关联容器和 std::vector
时最重要的步骤之一。
boost::container::flat_set
类将其值按顺序存储在向量中,这意味着对容器中非末尾元素的任何插入或删除都需要线性时间 O(N),就像std::vector
一样。这是一个必要的牺牲。但是为此,我们几乎可以减少每个元素的内存使用量三倍,更加友好地存储处理器缓存,并且具有随机访问迭代器。看看步骤 5,5.1
,在那里我们得到了通过调用lower_bound
成员函数返回的两个迭代器之间的距离。使用 flat set 获取距离只需要常数时间 O(1),而在std::set
的迭代器上进行相同操作需要线性时间 O(N)。在5.1的情况下,使用std::set
获取距离的速度比使用 flat set 容器慢 400 倍。
回到步骤 3。如果不预留内存,插入元素可能会变得更慢,内存效率也会降低。std::vector
类会分配所需的内存块,然后在该块上就地构造元素。当我们在没有预留内存的情况下插入一些元素时,有可能预分配的内存块上没有剩余的空间,因此std::vector
会分配一个更大的内存块。之后,std::vector
会将元素从第一个块复制或移动到第二个块,删除第一个块的元素,并释放第一个块。只有在此之后才会进行插入。在插入过程中可能会多次发生这种复制和释放,从而大大降低速度。
如果你知道std::vector
或任何扁平容器必须存储的元素数量,请在插入之前为这些元素预留空间。这在大多数情况下可以加快程序!
步骤 4很简单,我们在这里插入元素。请注意,我们在这里插入有序元素。这不是必需的,但建议以加快插入速度。在std::vector
的末尾插入元素比在中间或开头更便宜得多。
在步骤 5中,5.2
和5.3
并没有太大区别,除了它们的执行速度。删除元素的规则与插入它们的规则几乎相同。有关解释,请参见前一段。
也许我正在向你讲述关于容器的简单事情,但我看到一些非常流行的产品使用了 C++11 的特性,进行了大量的优化,并且对标准库容器的使用非常糟糕,特别是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++17 没有平面容器。来自 Boost 的平面容器速度快,有很多优化,并且不使用虚拟函数。Boost.Containers
中的类支持通过Boost.Move
模拟右值引用,因此即使在 C++03 编译器上也可以自由使用它们。
另请参阅
-
有关
Boost.Container
的获取单链表和内存池的好处的更多信息,请参考配方。 -
在第一章 开始编写您的应用程序 中的使用 C++11 移动模拟配方将为您提供关于在兼容 C++03 的编译器上模拟右值引用的基础知识。
-
Boost.Container
的官方文档包含大量关于Boost.Container
和每个类的完整参考的有用信息。请访问boost.org/libs/container.
了解更多信息。
第十章:收集平台和编译器信息
在本章中,我们将涵盖:
-
检测操作系统和编译器
-
检测 int128 的支持
-
检测和绕过禁用的 RTTI
-
使用更简单的方法编写元函数
-
减少代码大小并提高 C++11 中用户定义类型(UDTs)的性能
-
导出和导入函数和类的可移植方式
-
检测 Boost 版本并获取最新功能
介绍
不同的项目和公司有不同的编码要求。有些禁止异常或 RTTI,而有些禁止 C++11。如果您愿意编写可供广泛项目使用的可移植代码,那么这一章就是为您准备的。
想要尽可能快地编写代码并使用最新的 C++功能吗?您肯定需要一个工具来检测编译器功能。
一些编译器具有独特的功能,可以极大地简化您的生活。如果您只针对一个编译器,可以节省很多时间并使用这些功能。无需从头开始实现它们的类似物!
本章专门介绍了用于检测编译器、平台和 Boost 功能的不同辅助宏。这些宏广泛用于 Boost 库,并且对于编写能够使用任何编译器标志的可移植代码至关重要。
检测操作系统和编译器
我猜你可能见过很多丑陋的宏来检测代码编译的编译器。像这样的东西在 C 语言中是一种典型的做法:
#include <something_that_defines_macros>
#if !defined(__clang__) \
&& !defined(__ICC) \
&& !defined(__INTEL_COMPILER) \
&& (defined(__GNUC__) || defined(__GNUG__))
// GCC specific
#endif
现在,试着想出一个好的宏来检测 GCC 编译器。尽量使宏的使用尽可能简短。
看一下以下的步骤来验证你的猜测。
准备工作
只需要基本的 C++知识。
如何做...
这个步骤很简单,只包括一个头文件和一个宏。
- 头文件:
#include <boost/predef/compiler.h>
- 宏:
#if BOOST_COMP_GNUC
// GCC specific
#endif
它是如何工作的...
头文件<boost/predef/compiler.h>
知道所有可能的编译器,并为每个编译器都有一个宏。因此,如果当前编译器是 GCC,那么宏BOOST_COMP_GNUC
被定义为1
,而其他编译器的所有其他宏都被定义为0
。如果我们不在 GCC 编译器上,那么BOOST_COMP_GNUC
宏被定义为0
。
通过这种方法,您无需检查宏本身是否已定义:
#if defined(BOOST_COMP_GNUC) // Wrong!
// GCC specific
#endif
Boost.Predef
库的宏总是被定义的,这样就不需要在#ifdef
中输入defined()
或def
。
还有更多...
Boost.Predef
库还有用于检测操作系统、架构、标准库实现和一些硬件能力的宏。使用总是被定义的宏的方法;这使您能够更简洁地编写复杂的表达式:
#include <boost/predef/os.h>
#include <boost/predef/compiler.h>
#if BOOST_COMP_GNUC && BOOST_OS_LINUX && !BOOST_OS_ANDROID
// Do something for non Android Linux.
#endif
现在,最好的部分。Boost.Predef
库可用于 C、C++和 Objective-C 编译器。如果您喜欢它,可以在非 C++项目中使用它。
C++17 没有Boost.Predef
库的功能。
另请参阅
-
阅读
Boost.Predef
的官方文档,了解更多关于其在boost.org/libs/predef
的能力。 -
下一个步骤将向您介绍
Boost.Config
库,它的顺序更多,稍微不那么美观,但功能更加强大
检测 int128 的支持
一些编译器支持扩展算术类型,如 128 位浮点数或整数。让我们快速了解如何使用 Boost 来使用它们。
我们将创建一个接受三个参数并返回这些方法的乘积值的方法。如果编译器支持 128 位整数,那么我们就使用它们。如果编译器支持long long
,那么我们就使用它;否则,我们需要发出编译时错误。
准备工作
只需要基本的 C++知识。
如何做...
我们需要什么来处理 128 位整数?显示它们可用的宏和一些typedefs
以在各个平台上拥有可移植的类型名称。
- 包括一个头文件:
#include <boost/config.hpp>
- 现在,我们需要检测 int128 的支持:
#ifdef BOOST_HAS_INT128
- 添加一些
typedefs
并按以下方式实现该方法:
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 类型且没有
long long
的编译器,我们可能会产生编译时错误:
#else // #ifdef BOOST_HAS_INT128
#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 // #ifdef BOOST_HAS_INT128
工作原理...
头文件<boost/config.hpp>
包含许多宏来描述编译器和平台特性。在此示例中,我们使用BOOST_HAS_INT128
来检测对 128 位整数的支持,使用BOOST_NO_LONG_LONG
来检测对 64 位整数的支持。
正如我们从示例中看到的,Boost 具有 64 位有符号和无符号整数的typedefs
:
boost::long_long_type
boost::ulong_long_type
它还有 128 位有符号和无符号整数的typedefs
:
boost::int128_type
boost::uint128_type
还有更多...
C++11 通过long long int
和unsigned long long int
内置类型支持 64 位类型。不幸的是,并非所有编译器都支持 C++11,因此BOOST_NO_LONG_LONG
可能对您有用。
128 位整数不是 C++17 的一部分,因此 Boost 中的typedefs
和宏是编写可移植代码的一种方式。
C++标准化委员会正在进行工作,以添加编译时指定宽度的整数。当这项工作完成时,您将能够创建 128 位、512 位甚至 8388608 位(1 MB 大)的整数。
另请参阅
-
阅读有关“检测和绕过禁用的 RTTI”更多关于
Boost.Config
的信息。 -
阅读官方文档
boost.org/libs/config
以获取有关其功能的更多信息。 -
Boost 中有一个允许构造无限精度类型的库。点击链接
boost.org/libs/multiprecision
并查看Boost.Multiprecision
库。
检测和绕过禁用的 RTTI
一些公司和库对其 C++代码有特定要求,例如成功编译而无需 RTTI。
在这个小配方中,我们不仅会检测禁用的 RTTI,还会从头开始编写一个类似 Boost 的库,用于存储类型信息,并在运行时比较类型,即使没有typeid
。
准备工作
对于这个配方,需要基本的 C++ RTTI 使用知识。
如何做到...
检测禁用的 RTTI,存储类型信息,并在运行时比较类型是 Boost 库中广泛使用的技巧。
- 为此,我们首先需要包含以下头文件:
#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>
#include <iosfwd> // std::basic_ostream
#include <boost/current_function.hpp>
struct type_index {
const char * name_;
explicit type_index(const char* name)
: name_(name)
{}
const char* name() const { return name_; }
};
inline bool operator == (type_index v1, type_index v2) {
return !std::strcmp(v1.name_, v2.name_);
}
inline bool operator != (type_index v1, type_index v2) {
return !(v1 == v2);
}
- 最后一步是定义
type_id
函数:
template <class T>
inline type_index type_id() {
return type_index(BOOST_CURRENT_FUNCTION);
}
#endif
- 现在,我们可以比较类型:
#include <cassert>
int main() {
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
变量不等于它的原因。
恭喜!我们刚刚重新发明了大部分Boost.TypeIndex
库的功能。删除步骤 1 到 4中的所有代码,并稍微更改步骤 5中的代码以使用Boost.TypeIndex
库:
#include <boost/type_index.hpp>
void test() {
using boost::typeindex::type_id;
assert(type_id<unsigned int>() == type_id<unsigned>());
assert(type_id<double>() != type_id<long double>());
}
还有更多...
当然,Boost.TypeIndex
略微超出了这个范围;它允许您以与平台无关的方式获取可读的类型名称,解决与平台相关的问题,允许发明自己的 RTTI 实现,拥有 constexpr RTTI 等等。
不同的编译器有不同的宏用于获取完整的函数名。使用 Boost 的宏是最通用的解决方案。BOOST_CURRENT_FUNCTION
宏在编译时返回名称,因此它意味着最小的运行时惩罚。
C++11 有一个__func__
魔术标识符,它被评估为当前函数的名称。然而,__func__
的结果只是函数名,而BOOST_CURRENT_FUNCTION
则努力显示函数参数,包括模板参数。
另请参阅
-
阅读即将发布的食谱,了解更多关于
Boost.Config
的信息 -
浏览
github.com/boostorg/type_index
以查看Boost.TypeIndex
库的源代码 -
阅读
boost.org/libs/config
上的Boost.Config
的官方文档 -
阅读
boost.org/libs/type_index
上的Boost.TypeIndex
库的官方文档 -
第一章的食谱获取可读的类型名称,开始编写您的应用程序将向您介绍
Boost.TypeIndex
的其他功能
使用更简单的方法编写元函数
第四章,编译时技巧,和第八章,元编程,都致力于元编程。如果您尝试使用这些章节中的技术,您可能已经注意到编写元函数可能需要很长时间。因此,在编写可移植实现之前,使用更用户友好的方法,如 C++11 的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
- 就是这样。现在,我们可以自由地编写以下代码:
#include <array>
int main() {
std::array<short, 5> arr;
static_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 的常量表达式可以提高编译速度和错误诊断信息。这是一个很好的功能。如果您的函数需要来自 C++14 的relaxed constexpr,那么您可以使用BOOST_CXX14_CONSTEXPR
宏。如果放松的 constexpr 可用,则它扩展为constexpr
,否则不扩展。
另请参阅
-
有关
constexpr
用法的更多信息,请阅读en.cppreference.com/w/cpp/language/constexpr
-
阅读官方文档
Boost.Config
,了解有关宏的更多信息boost.org/libs/config
减小 C++11 中用户定义类型(UDTs)的代码大小并提高性能
当标准库容器中使用用户定义类型(UDTs)时,C++11 具有非常具体的逻辑。一些容器仅在移动构造函数不抛出异常或不存在复制构造函数时才使用移动赋值和移动构造。
让我们看看如何确保编译器知道move_nothrow
类具有不抛出异常的move
赋值运算符和不抛出异常的move
构造函数。
准备工作
本教程需要基本的 C++11 右值引用知识。对标准库容器的了解也会对你有所帮助。
如何做...
让我们看看如何使用 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 goes here.
// ...
{}
move_nothrow& operator=(move_nothrow&&) BOOST_NOEXCEPT {
// Implementation goes here.
// ...
return *this;
}
move_nothrow(const move_nothrow&);
move_nothrow& operator=(const move_nothrow&);
};
- 现在,我们可以在 C++11 中使用
std::vector
类而无需进行任何修改:
#include <vector>
int main() {
std::vector<move_nothrow> v(10);
v.push_back(move_nothrow());
}
- 如果我们从
move
构造函数中移除BOOST_NOEXCEPT
,我们将收到以下错误,因为我们没有为复制构造函数提供定义:
undefined reference to `move_nothrow::move_nothrow(move_nothrow
const&)
工作原理...
BOOST_NOEXCEPT
宏在支持它的编译器上扩展为noexcept
。标准库容器使用类型特征来检测构造函数是否抛出异常。类型特征主要基于noexcept
说明符做出决定。
为什么没有BOOST_NOEXCEPT
会出错?编译器的类型特征返回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
的函数确实抛出异常,您的程序将在不调用已构造对象的析构函数的情况下终止。
另请参阅
-
有关
move
构造函数允许抛出异常以及容器必须移动对象的文档可在www.open-std.org/jtc1/sc22/wg21/docs/papers/2010/n3050.html
上找到。 -
阅读
Boost.Config
的官方文档,了解更多BOOST_NOEXCEPT
的示例,例如 Boost 中存在的宏boost.org/libs/config
导出和导入函数和类的可移植方式
几乎所有现代语言都有制作库的能力,即一组具有明确定义接口的类和方法。C++也不例外。我们有两种类型的库:运行时(也称为共享或动态)和静态。但是,在 C++中编写库并不是一项简单的任务。不同的平台有不同的方法来描述必须从共享库中导出哪些符号。
让我们看看如何使用 Boost 以一种可移植的方式管理符号可见性。
准备工作
在本教程中,创建动态和静态库的经验可能会有所帮助。
如何做...
本教程的代码由两部分组成。第一部分是库本身。第二部分是使用该库的代码。这两部分都使用相同的头文件,在其中声明了库方法。使用 Boost 以一种可移植的方式管理符号可见性很简单,可以通过以下步骤完成:
- 在头文件中,我们需要以下头文件的定义:
#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 goes here.
// ...
return 0;
}
int bar::meow() const {
throw bar_exception();
}
- 现在,我们可以像下面的代码一样使用库:
#include "../06_A_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
进行转换的类。
还有更多...
一些编译器默认导出所有符号,但提供了禁用此行为的标志。例如,Linux 上的 GCC 和 Clang 提供了-fvisibility=hidden
。强烈建议使用这些标志,因为它可以导致更小的二进制文件大小,更快的动态库加载,以及更好的二进制逻辑结构。一些程序间优化在导出较少符号时可以表现更好。C++17 没有描述可见性的标准方式。希望有一天,C++中会出现一种可移植的可见性处理方式,但在那之前,我们必须使用 Boost 中的宏。
另请参阅
-
从头开始阅读本章,以获取更多关于
Boost.Config
使用的示例 -
请阅读
Boost.Config
的官方文档,以获取完整的Boost.Config
宏列表及其描述,网址为boost.org/libs/config
。
检测 Boost 版本并获取最新功能
Boost 正在积极开发,因此每个版本都包含新功能和库。一些人希望有针对不同版本的 Boost 编译的库,并且还想使用新版本的一些功能。
让我们看一下boost::lexical_cast
的变更日志。根据它,Boost 1.53 有一个lexical_cast(const CharType* chars, std::size_t count)
函数重载。我们这个示例的任务是为新版本的 Boost 使用该函数重载,并为旧版本解决缺少的函数重载。
准备工作
只需要基本的 C++和Boost.LexicalCast
库的知识。
如何做...
好吧,我们需要做的就是获取有关 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
- 现在,我们可以像这里展示的代码一样使用:
#include <cassert>
int main() {
assert(to_int("10000000", 3) == 100);
}
它是如何工作的...
BOOST_VERSION
宏包含 Boost 版本,格式如下:主版本号为一个数字,次版本号为三个数字,修订级别为两个数字。例如,Boost 1.73.1 将在BOOST_VERSION
宏中包含107301
数字。
因此,在步骤 2中,我们检查 Boost 版本,并根据Boost.LexicalCast
的能力选择to_int
函数的正确实现。
还有更多...
拥有版本宏是大型库的常见做法。一些 Boost 库允许您指定要使用的库的版本;请参阅Boost.Thread
及其BOOST_THREAD_VERSION
宏以获取示例。
顺便说一句,C++也有一个版本宏。__cplusplus
宏的值允许您区分 C++11 之前的版本和 C++11,C++11 和 C++14,或 C++17。目前,它可以定义为以下值之一:199711L
,201103L
,201402L
或201703L
。宏值代表委员会批准标准的年份和月份。
另请参阅
-
阅读第五章中的创建执行线程配方,了解有关
BOOST_THREAD_VERSION
及其对Boost.Thread
库的影响的更多信息,或阅读boost.org/libs/thread
的文档。 -
从头开始阅读本章,或考虑阅读Boost.Config的官方文档
第十一章:与系统一起工作
在本章中,我们将涵盖:
-
在目录中列出文件
-
删除和创建文件和目录
-
编写和使用插件
-
获取回溯-当前调用序列
-
快速从一个进程传递数据到另一个进程
-
同步进程间通信
-
在共享内存中使用指针
-
读取文件的最快方式
-
协程-保存状态和推迟执行
介绍
每个操作系统都有许多系统调用。这些调用在一个操作系统和另一个操作系统之间有所不同,但执行的功能非常接近。Boost 提供了对这些调用的可移植和安全的包装器。了解包装器对于编写良好的程序至关重要。
本章专门讨论与操作系统的工作。我们已经看到如何处理网络通信和信号第六章中的操作任务。在本章中,我们将更仔细地研究文件系统,创建和删除文件。我们将看到如何在不同系统进程之间传递数据,如何以最大速度读取文件,以及如何执行其他技巧。
在目录中列出文件
有标准库函数和类来读取和写入文件数据。但在 C++17 之前,没有函数来列出目录中的文件,获取文件类型或获取文件的访问权限。
让我们看看如何使用 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++17 的一部分。C++17 中的所有内容都位于单个头文件<filesystem>
中,位于std::filesystem
命名空间中。标准库版本的文件系统与 Boost 版本略有不同,主要是通过使用作用域枚举(enum class
)来区分,而Boost.Filesystem
使用的是非作用域enum
。
有一个类;directory_entry
。该类提供了文件系统信息的缓存,因此如果您经常使用文件系统并查询不同的信息,请尝试使用directory_entry
以获得更好的性能。
就像其他 Boost 库一样,Boost.Filesystem
可以在 C++17 编译器之前甚至在 C++11 编译器之前工作。
另请参阅
-
擦除和创建文件和目录教程将展示
Boost.Filesystem
的另一个用法示例 -
阅读 Boost 关于
Boost.Filesystem
的官方文档,以获取有关其功能的更多信息;可以在以下链接找到:boost.org/libs/filesystem
-
您可以在
www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4659.pdf
找到 C++17 草案
擦除和创建文件和目录
让我们考虑以下代码行:
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
库。
如何做...
我们继续处理文件系统的可移植包装器,在本教程中,我们将看到如何修改目录内容:
- 与其他 Boost 库一样,我们需要包含一些头文件:
#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_symlink(
"dir/subdir/file.txt", "symlink", error);
- 然后,我们需要检查通过
symlink
是否可以访问文件:
if (!error) {
std::cerr << "Symlink created\n";
assert(boost::filesystem::exists("symlink"));
- 如果
symlink
创建失败,我们将删除创建的文件:
} else {
std::cerr << "Failed to create a symlink\n";
boost::filesystem::remove_all("dir", error);
assert(!error);
boost::filesystem::remove("symlink", 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::filesystem::remove_all
和boost::filesystem::remove
也是同样的情况。前者可以删除包含文件和子目录的非空目录。后者删除单个文件。
其余步骤很容易理解,不应该引起任何麻烦。
还有更多...
boost::system::error_code
类是 C++11 的一部分,可以在std::
命名空间的<system_error>
头文件中找到。Boost.Filesystem
的类是 C++17 的一部分。
最后,这里是一个对于那些打算使用Boost.Filesystem
的小建议。当文件系统操作中发生错误时,如果是例行操作或应用程序需要高度的责任/性能,使用boost::system::error_codes
。否则,捕获异常更可取且更可靠。
另请参阅
在目录中列出文件的配方还包含有关Boost.Filesystem
的信息。阅读 Boost 的官方文档boost.org/libs/filesystem
以获取更多信息和示例。
编写和使用插件
这里有一个棘手的问题:我们希望允许用户编写扩展我们程序功能的功能,但我们不想给他们源代码。换句话说,我们想说,“编写一个函数 X 并将其打包到共享库中。我们可以使用您的函数以及其他一些用户的函数!”
您在日常生活中会遇到这种技术:您的浏览器使用它来允许第三方插件,您的文本编辑器可能使用它进行语法高亮显示,游戏使用动态库加载进行可下载内容(DLC)和添加游戏玩家内容,服务器返回的网页使用模块/插件进行加密/身份验证等。
用户功能的要求是什么,我们如何在某个时刻使用该功能,而不将其链接到共享库?
准备就绪
这个配方需要基本的 C++知识。阅读第十章中的导出和导入函数和类的便携式方法是必需的。
如何做...
首先,您必须与用户达成协议:
-
记录插件接口的要求。例如,您可以说所有插件必须导出一个名为
greet
的函数,并且该函数必须接受const std::string&
并返回std::string
。 -
之后,用户可以按以下方式编写插件/共享库:
#include <string>
#include <boost/config.hpp>
#define API extern "C" BOOST_SYMBOL_EXPORT
API std::string greeter(const std::string& name) {
return "Good to meet you, " + name + ".";
}
- 加载共享库的程序代码必须包括来自
Boost.DLL
的头文件:
#include <boost/dll/shared_library.hpp>
- 加载库的代码必须如下所示:
int main() {
boost::filesystem::path plugin_path = /* path-to-pligin */;
boost::dll::shared_library plugin(
plugin_path,
boost::dll::load_mode::append_decorations
);
- 获取用户功能必须如下所示:
auto greeter = plugin.get<std::string(const std::string&)>("greeter");
- 完成。现在,您可以使用该功能:
std::cout << greeter("Sally Sparrow");
}
根据加载的插件,您将获得不同的结果:
plugin_hello
:
Good to meet you, Sally Sparrow.
plugin_do_not
:
They are fast. Faster than you can believe. Don't turn
your back, don't look away, and don't blink. Good luck, Sally Sparrow.
它是如何工作的...
步骤 2中有一个小技巧。当您将函数声明为extern "C"
时,这意味着编译器不得操纵(更改)函数名称。换句话说,在步骤 2中,我们只是创建一个名为greet
的函数,并且以该确切名称从共享库中导出。
在步骤 4中,我们创建一个名为plugin
的boost::dll::shared_library
变量。该变量的构造函数将共享库加载到当前可执行文件的地址空间中。在步骤 5中,我们在plugin
中搜索名为greet
的函数。我们还指定该函数具有std::string(const std::string&)
的签名,并将该函数的指针存储在变量greet
中。
就是这样!从现在开始,我们可以将greet
变量用作函数,只要plugin
变量及其所有副本未被销毁。
您可以从共享库中导出多个函数;甚至可以导出变量。
小心!始终将 C 和 C++库动态链接到插件和主可执行文件中,否则您的应用程序将崩溃。始终在插件和应用程序中使用相同或 ABI 兼容的 C 和 C++库版本。否则您的应用程序将崩溃。阅读典型误用的文档!
还有更多...
Boost.DLL
是一个新库;它出现在 Boost 1.61 中。我最喜欢的部分是该库具有向共享库名称添加特定于平台的装饰的能力。例如,根据平台,以下代码将尝试加载"./some/path/libplugin_name.so"
、"./some/path/plugin_name.dll"
或"./some/path/libplugin_name.dll"
:
boost::dll::shared_library lib(
"./some/path/plugin_name",
boost::dll::load_mode::append_decorations
);
C++17 没有类似boost::dll::shared_library
的类。但是,工作正在进行中,总有一天我们可能会在 C++标准中看到它。
另请参阅
官方文档包含多个示例,更重要的是,库的典型问题/误用boost.org/libs/dll
网站。
获取回溯 - 当前调用序列
在报告错误或失败时,更重要的是报告导致错误的步骤,而不是错误本身。考虑一个简单的交易模拟器:
int main() {
int money = 1000;
start_trading(money);
}
它只报告一行:
Sorry, you're bankrupt!
这是行不通的。我们想知道是怎么发生的,导致破产的步骤是什么!
好的。让我们修复以下函数,并让它报告导致破产的步骤:
void report_bankruptcy() {
std::cout << "Sorry, you're bankrupt!\n";
std::exit(0);
}
入门
您需要 Boost 1.65 或更新版本。还需要基本的 C++知识。
如何做到...
对于这个示例,我们只需要构造一个单独的类并输出它:
#include <iostream>
#include <boost/stacktrace.hpp>
void report_bankruptcy() {
std::cout << "Sorry, you're bankrupt!\n";
std::cout << "Here's how it happened:\n"
<< boost::stacktrace::stacktrace();
std::exit(0);
}
完成。现在report_bankruptcy()
输出的内容与以下内容接近(从下往上读):
Sorry, you're bankrupt!
Here's how it happened:
0# report_bankruptcy()
1# loose(int)
2# go_to_casino(int)
3# go_to_bar(int)
4# win(int)
5# go_to_casino(int)
6# go_to_bar(int)
7# win(int)
8# make_a_bet(int)
9# loose(int)
10# make_a_bet(int)
11# loose(int)
12# make_a_bet(int)
13# start_trading(int)
14# main
15# 0x00007F79D4C48F45 in /lib/x86_64-linux-
gnu/libc.so.6
16# 0x0000000000401F39 in ./04_stacktrace
它是如何工作的...
所有的魔法都在boost::stacktrace::stacktrace
类中。在构造时,它会快速将当前调用堆栈存储在自身中。boost::stacktrace::stacktrace
是可复制和可移动的,因此存储的调用序列可以传递给其他函数,复制到异常类中,甚至存储在某个文件中。随心所欲地使用它吧!
在输出的boost::stacktrace::stacktrace
实例上,解码存储的调用序列并尝试获取人类可读的函数名称。这就是您在之前的示例中看到的:导致report_bankruptcy()
函数调用的调用序列。
boost::stacktrace::stacktrace
允许您迭代存储的地址,将单个地址解码为人类可读的名称。如果您不喜欢跟踪的默认输出格式,可以编写自己喜欢的输出方式的函数。
请注意,回溯的有用性取决于多个因素。程序的发布版本可能包含内联函数,导致跟踪不太可读:
0# report_bankruptcy()
1# go_to_casino(int)
2# win(int)
3# make_a_bet(int)
4# make_a_bet(int)
5# make_a_bet(int)
6# main
在没有调试符号的情况下构建可执行文件可能会产生没有许多函数名称的跟踪。
阅读官方文档的配置和构建部分,了解有关可能影响跟踪可读性的不同编译标志和宏的更多信息。
还有更多...
Boost.Stacktrace
库对于大型项目有一个非常好的功能。您可以在链接程序时禁用所有跟踪。这意味着您不需要重新构建所有源文件。只需为整个项目定义BOOST_STACKTRACE_LINK
宏。现在,如果您链接boost_stacktrace_noop
库,将收集空跟踪。链接boost_stacktrace_windbg
/boost_stacktrace_windbg_cached
/boost_stacktrace_backtrace
/...库
以获得不同可读性的跟踪。
Boost.Stacktrace
是一个新库;它出现在 Boost 1.65 中。
boost::stacktrace::stacktrace
相当快地收集当前的调用序列;它只是动态分配一块内存并将一堆地址复制到其中。解码地址要慢得多;它使用多个特定于平台的调用,可能会分叉进程,并且可能会初始化和使用COM。
C++17 没有Boost.Stacktrace
功能。正在进行工作,将其添加到下一个 C++标准中。
另请参阅
官方文档boost.org/libs/stacktrace/
中有一些关于异步信号安全的堆栈跟踪的示例,以及有关所有Boost.Stacktrace
功能的详细描述。
快速将数据从一个进程传递到另一个进程
有时,我们编写大量相互通信的程序。当程序在不同的机器上运行时,使用套接字是最常见的通信技术。但是,如果多个进程在单台机器上运行,我们可以做得更好!
让我们看看如何使用Boost.Interprocess
库使单个内存片段可在不同进程中使用。
准备就绪
这个配方需要对 C++有基本的了解。还需要了解原子变量(查看另请参阅部分,了解有关原子的更多信息)。一些平台需要链接到运行时库rt
。
如何做...
在这个例子中,我们将在进程之间共享一个原子变量,使其在新进程启动时递增,在进程终止时递减:
- 我们需要包含以下头文件进行跨进程通信:
#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
- 创建或获取共享内存段:
int main() {
boost::interprocess::managed_shared_memory
segment(boost::interprocess::open_or_create, "shm1-cache", 1024);
- 获取或构造
atomic
变量:
atomic_t& atomic
= *segment.find_or_construct<atomic_t> // 1
("shm1-counter") // 2
(0) // 3
;
- 以通常的方式处理
atomic
变量:
std::cout << "I have index " << ++ atomic
<< ". Press any key...\n";
std::cin.get();
- 销毁
atomic
变量:
const int snapshot = --atomic;
if (!snapshot) {
segment.destroy<atomic_t>("shm1-counter");
boost::interprocess::shared_memory_object
::remove("shm1-cache");
}
} /*main*/
就是这样!现在,如果我们同时运行这个程序的多个实例,我们会看到每个新实例都会递增其索引值:
I have index 1\. Press any key...
I have index 2\.
Press any key...
I have index 3\. Press any key...
I have index 4\. Press any key...
I have index 5\.
Press any key...
它是如何工作的...
这个配方的主要思想是获得一个对所有进程可见的内存段,并在其中放置一些数据。让我们看看步骤 3,在那里我们检索这样一个内存段。在这里,shm1-cache
是段的名称(不同的段有不同的名称)。您可以为段指定任何名称。第一个参数是boost::interprocess::open_or_create
,它告诉boost::interprocess::managed_shared_memory
必须打开一个具有名称shm1-cache
的现有段或构造它。最后一个参数是段的大小。
段的大小必顺应足够大,以适应Boost.Interprocess
库特定的数据。这就是为什么我们使用1024
而不是sizeof(atomic_t)
。但实际上,操作系统会将这个值舍入到最接近的更大的支持值,通常等于或大于 4 千字节。
步骤 4是一个棘手的步骤,因为我们在这里同时执行多个任务。在这一步的第 2 部分,我们在段中找到或构造一个名为shm1-counter
的变量。在步骤 4的第 3 部分,我们提供一个参数,用于初始化变量,如果在步骤 2中没有找到。只有在找不到变量并且必须构造变量时,才会使用此参数,否则将被忽略。仔细看第二行(第 1 部分)。看到解引用运算符*
的调用。我们这样做是因为segment.find_or_construct<atomic_t>
返回一个指向atomic_t
的指针,在 C++中使用裸指针是一种不好的风格。
我们在共享内存中使用原子变量!这是必需的,因为两个或更多进程可能同时使用相同的shm1-counter
原子变量。
在处理共享内存中的对象时,您必须非常小心;不要忘记销毁它们!在步骤 6中,我们使用它们的名称销毁对象和段。
还有更多...
仔细看看步骤 2,我们在那里检查BOOST_ATOMIC_INT_LOCK_FREE != 2
。我们正在检查atomic_t
是否不使用互斥锁。这非常重要,因为通常的互斥锁在共享内存中不起作用。因此,如果BOOST_ATOMIC_INT_LOCK_FREE
不等于2
,我们会得到未定义的行为。
不幸的是,C++11 没有跨进程类,据我所知,Boost.Interprocess
也没有被提议纳入 C++20。
一旦创建了托管段,它就不能自动增加大小!确保您创建的段足够大以满足您的需求,或者查看另请参阅部分,了解有关增加托管段的信息。
共享内存是进程进行通信的最快方式,但适用于可能共享内存的进程。这通常意味着进程必须在同一主机上运行,或者在对称多处理(SMP)集群上运行。
另请参阅
-
同步跨进程通信配方将告诉您更多关于共享内存、跨进程通信和同步访问共享内存资源的信息。
-
有关原子操作的更多信息,请参阅使用原子快速访问共享资源示例
-
Boost 的官方文档
Boost.Interprocess
也可能会有所帮助;可以在boost.org/libs/interprocess
找到。 -
如何增加托管段的方法在
boost.org/libs/interprocess
的增长托管段中有描述
同步进程间通信
在上一个示例中,我们看到了如何创建共享内存以及如何在其中放置一些对象。现在,是时候做一些有用的事情了。让我们从第五章的多线程中的制作工作队列示例中获取一个例子,并使其适用于多个进程。在这个示例结束时,我们将得到一个可以存储不同任务并在进程之间传递它们的类。
准备工作
这个示例使用了前一个示例中的技术。你还需要阅读第五章的多线程中的制作工作队列示例,并理解它的主要思想。该示例需要在某些平台上链接运行时库rt
。
如何做...
认为将独立的子进程代替线程使程序更可靠,因为子进程的终止不会终止主进程。我们不会在这里对这个假设进行争论,只是看看如何实现进程之间的数据共享。
- 这个示例需要很多头文件:
#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 boost::interprocess::managed_shared_memory
managed_shared_memory_t;
typedef task_structure task_type;
typedef boost::interprocess::allocator<
task_type,
boost::interprocess::managed_shared_memory::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;
deque_t& tasks_;
typedef boost::interprocess::interprocess_mutex mutex_t;
mutex_t& mutex_;
typedef boost::interprocess::interprocess_condition condition_t;
condition_t& cond_;
typedef boost::interprocess::scoped_lock<mutex_t> scoped_lock_t;
- 成员的初始化必须如下所示:
public:
explicit work_queue()
: segment_(
boost::interprocess::open_or_create,
"work-queue",
1024 * 1024 * 32
)
, 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
,而不是原始的 unique locks:
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;
}
- 不要忘记清理资源:
void cleanup() {
segment_.destroy<condition_t>("work-queue:condition");
segment_.destroy<mutex_t>("work-queue:mutex");
segment_.destroy<deque_t>("work-queue:deque");
boost::interprocess::shared_memory_object
::remove("work-queue");
}
工作原理...
在这个示例中,我们几乎做了和第五章的多线程中的制作工作队列 类示例中完全相同的事情,但我们是在共享内存中分配数据。
在存储具有指针或引用作为成员字段的共享内存对象时需要额外小心。我们将在下一个示例中看到如何处理指针。
看一下步骤 2。我们没有使用boost::function
作为任务类型,因为它里面有指针,所以它在共享内存中无法工作。
步骤 3很有趣,因为涉及allocator_t
。如果内存不是从共享内存段分配的,它就可以被其他进程使用;这就是为什么需要为容器使用特定的分配器。allocator_t
是一个有状态的分配器,这意味着它会随着容器一起被复制。此外,它不能被默认构造。
步骤 4非常简单,只是tasks_
、mutex_
和cond_
只有引用。这是因为对象本身是在共享内存中构造的。所以,work_queue
只能在其中存储引用。
在步骤 5中,我们正在初始化成员。这段代码对你来说一定很熟悉。在上一个示例中,我们做了完全相同的事情。
在构造tasks_
时,我们提供了一个分配器的实例。这是因为allocator_t
不能由容器本身构造。共享内存在进程退出事件时不会被销毁,所以我们可以运行程序一次,将任务发布到工作队列,停止程序,启动其他程序,并获取由第一个程序实例存储的任务。共享内存只有在重新启动时才会被销毁,或者如果你显式调用segment.deallocate("work-queue");
。
还有更多...
正如前面的内容中已经提到的,C++17 没有 Boost.Interprocess
中的类。此外,不得在共享内存段中使用 C++17 或 C++03 容器。其中一些容器可能有效,但这种行为不具有可移植性。
如果你查看一些 <boost/interprocess/containers/*.hpp>
头文件,你会发现它们只是使用了 Boost.Containers
库中的容器:
namespace boost { namespace interprocess {
using boost::container::vector;
}}
Boost.Interprocess
的容器具有 Boost.Containers
库的所有优点,包括右值引用及其在旧编译器上的模拟。
Boost.Interprocess
是在同一台机器上运行的进程之间进行通信的最快解决方案。
另请参阅
-
在共享内存中使用指针 的方法
-
阅读 第五章, 多线程,了解更多关于同步原语和多线程的信息
-
有关
Boost.Interprocess
库的更多示例和信息,请参考 Boost 官方文档;可在以下链接找到:boost.org/libs/interprocess
在共享内存中使用指针
很难想象在没有指针的情况下编写一些低级别的 C++ 核心类。指针和引用在 C++ 中随处可见,但它们在共享内存中无法使用!因此,如果我们在共享内存中有这样的结构,并将共享内存中某个整数变量的地址分配给 pointer_
,那么 pointer_
在其他进程中将无效:
struct with_pointer {
int* pointer_;
// ...
int value_holder_;
};
我们如何修复这个问题?
准备工作
理解前面的内容是理解这个的前提。在某些平台上,示例需要链接运行时系统库 rt
。
如何做...
修复很简单;我们只需要用 offset_ptr<>
替换指针:
#include <boost/interprocess/offset_ptr.hpp>
struct correct_struct {
boost::interprocess::offset_ptr<int> pointer_;
// ...
int value_holder_;
};
现在,我们可以像使用普通指针一样自由使用它:
int main() {
boost::interprocess::managed_shared_memory
segment(boost::interprocess::open_or_create, "segment", 4096);
correct_struct* ptr =
segment.find<correct_struct>("structure").first;
if (ptr) {
std::cout << "Structure found\n";
assert(*ptr->pointer_ == ethalon_value);
segment.destroy<correct_struct>("structure");
}
}
工作原理...
我们无法在共享内存中使用指针,因为当共享内存的一部分映射到进程的地址空间时,其地址仅对该进程有效。当我们获取变量的地址时,它只是该进程的本地地址。其他进程将共享内存映射到不同的基地址,因此变量地址会有所不同。
那么,我们如何处理始终在变化的地址?有一个技巧!由于指针和结构位于同一共享内存段中,它们之间的距离不会改变。boost::interprocess::offset_ptr
的想法是记住 offset_ptr
和指向值之间的距离。在解引用时,offset_ptr
将距离值添加到 offset_ptr
变量的进程相关地址上。
偏移指针模拟了指针的行为,因此可以快速应用替换。
不要将可能包含指针或引用的类放入共享内存中!
还有更多...
偏移指针的工作速度略慢于通常的指针,因为每次解引用都需要计算地址。但是,这种差异通常不应该让你担心。
C++17 没有偏移指针。
另请参阅
-
Boost 官方文档包含许多示例和更高级的
Boost.Interprocess
功能;可在boost.org/libs/interprocess
找到 -
最快的文件读取方法 的方法包含了
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 = static_cast<const char*>(
region.get_address()
);
就是这样!现在,我们可以像处理常规内存一样处理文件:
const char* pos = std::find(
begin, begin + region.get_size(), '\1'
);
它是如何工作的...
所有流行的操作系统都具有将文件映射到进程地址空间的能力。在这样的映射完成后,进程可以像处理常规内存一样处理这些地址。操作系统会处理所有文件操作,如缓存和预读。
为什么它比传统的读/写更快?这是因为在大多数情况下,读/写是作为内存映射和将数据复制到用户指定的缓冲区来实现的。因此,读取通常比内存映射多做一点。
就像标准库的std::fstream
一样,在打开文件时必须提供打开模式。请参阅步骤 2,我们在那里提供了boost::interprocess::read_only
模式。
请参阅步骤 3,我们在那里一次映射了整个文件。这个操作实际上非常快,因为操作系统不会从磁盘读取数据,而是等待对映射区域的请求。在请求了映射区域的一部分后,操作系统将该文件的那部分加载到内存中。正如我们所看到的,内存映射操作是懒惰的,并且映射区域的大小不会影响性能。
但是,32 位操作系统无法内存映射大文件,因此您必须按部就班地映射它们。POSIX(Linux)操作系统要求在 32 位平台上处理大文件时定义_FILE_OFFSET_BITS=64
宏。否则,操作系统将无法映射超过 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++17 的一部分,看起来它们在 C++20 中也不会是。
写入内存映射区域也是一个非常快的操作。操作系统会缓存写入操作,并不会立即将修改刷新到磁盘。操作系统和std::ofstream
数据缓存之间存在差异。如果std::ofstream
数据由应用程序缓存,并且应用程序终止,则缓存的数据可能会丢失。当数据由操作系统缓存时,应用程序的终止不会导致数据丢失。断电和操作系统崩溃都会导致数据丢失。
如果多个进程映射单个文件,并且其中一个进程修改了映射区域,则其他进程立即看到更改(甚至无需实际将数据写入磁盘!现代操作系统非常聪明!)。
另请参阅
Boost.Interprocess
库包含许多有用的功能,用于与系统一起工作;并非所有功能都在本书中涵盖。您可以在官方网站上阅读有关这个伟大库的更多信息:boost.org/libs/interprocess
。
协程-保存状态和推迟执行
如今,许多嵌入式设备仍然只有一个核心。开发人员为这些设备编写代码,试图从中挤取最大的性能。
对于这些设备使用Boost.Threads
或其他线程库并不有效。操作系统将被迫调度线程进行执行,管理资源等,因为硬件无法并行运行它们。
那么,我们如何强制程序在等待主程序的某些资源时切换到子程序的执行?此外,我们如何控制子程序的执行时间?
准备工作
这个食谱需要基本的 C++和模板知识。阅读一些关于Boost.Function
的食谱也可能有所帮助。
如何做到...
这个教程是关于协程或子程序,允许多个入口点。多个入口点使我们能够在特定位置暂停和恢复程序的执行,切换到/从其他子程序。
Boost.Coroutine2
库几乎负责一切。我们只需要包含它的头文件:
#include <boost/coroutine2/coroutine.hpp>
- 创建具有所需输入参数类型的协程类型:
typedef boost::coroutines2::asymmetric_coroutine<std::size_t> corout_t;
- 创建一个表示子程序的类:
struct coroutine_task {
std::string& result;
coroutine_task(std::string& r)
: result(r)
{}
void operator()(corout_t::pull_type& yield);
private:
std::size_t ticks_to_work;
void tick(corout_t::pull_type& yield);
};
- 让我们创建协程本身:
int main() {
std::string result;
coroutine_task task(result);
corout_t::push_type coroutine(task);
- 现在,我们可以在主程序中等待某个事件的同时执行子程序:
// Somewhere in main():
while (!spinlock.try_lock()) {
// We may do some useful work, before
// attempting to lock a spinlock once more.
coroutine(10); // 10 is the ticks count to run.
}
// Spinlock is locked.
// ...
while (!port.block_ready()) {
// We may do some useful work, before
// attempting to get block of data once more.
coroutine(300); // 300 is the ticks count to run.
// Do something with `result` variable.
}
- 协程方法可能如下所示:
void coroutine_task::operator()(corout_t::pull_type& yield) {
ticks_to_work = yield.get();
// Prepare buffers.
std::string buffer0;
while (1) {
const bool requiers_1_more_copy = copy_to_buffer(buffer0);
tick(yield);
if (requiers_1_more_copy) {
std::string buffer1;
copy_to_buffer(buffer1);
tick(yield);
process(buffer1);
tick(yield);
}
process(buffer0);
tick(yield);
}
}
tick()
函数可以这样实现:
void coroutine_task::tick(corout_t::pull_type& yield) {
if (ticks_to_work != 0) {
--ticks_to_work;
}
if (ticks_to_work == 0) {
// Switching back to main.
yield();
ticks_to_work = yield.get();
}
}
它是如何工作的...
在步骤 2中,我们使用std::size_t
作为模板参数描述了子程序的输入参数。
步骤 3相当简单,除了corout_t::pull_type& yield
参数。我们马上就会看到它的作用。
当我们在步骤 5中调用coroutine(10)
时,我们正在执行一个协程程序。执行跳转到coroutine_task::operator()
,在那里调用yield.get()
返回输入参数10
。执行继续进行,coroutine_task::tick
函数测量经过的滴答声。
最有趣的部分来了!
在步骤 7中,如果在函数coroutine_task::tick
中ticks_to_work
变量变为0
,协程的执行将在yield()
处暂停,而main()
继续执行。在下一次调用coroutine(some_value)
时,协程的执行将从tick
函数的中间位置继续,就在yield()
旁边的行。在那一行,ticks_to_work = yield.get();
被执行,ticks_to_work
变量开始保存一个新的输入值some_value
。
这意味着我们可以在函数的多个位置暂停/继续协程。所有函数状态和变量都会被恢复:
让我描述一下协程和线程之间的主要区别。当执行协程时,主任务什么也不做。当执行主任务时,协程任务什么也不做。使用线程时,您没有这样的保证。使用协程,您明确指定何时启动子任务以及何时暂停它。在单核环境中,线程可能随时切换;您无法控制这种行为。
还有更多...
在切换线程时,操作系统会做很多工作,因此这不是一个非常快的操作。然而,使用协程,您可以完全控制切换任务;此外,您不需要执行一些特定于操作系统的内部内核工作。切换协程比切换线程快得多,尽管不像调用boost::function
那样快。
Boost.Coroutine2
库负责调用协程任务中变量的析构函数,因此无需担心泄漏。
协程使用boost::coroutines2::detail::forced_unwind
异常来释放不是从std::exception
派生的资源。您必须小心不要在协程任务中捕获该异常。
您不能复制Boost.Coroutine2
协程,但可以std::move
它们。
有一个Boost.Coroutine
库(末尾没有2
!),它不需要 C++11 兼容的编译器。但是该库已经被弃用,并且有一些区别(例如它不会从协程中传播异常)。注意区别!Boost.Coroutine
在 Boost 1.56 中也显著改变了其接口。
C++17 没有协程。但协程 TS几乎准备就绪,所以很有可能下一个 C++标准将直接包含它们。
协程 TS 与 Boost.Coroutine2
不同!Boost 提供了 有栈 协程,这意味着您不需要特别使用宏/关键字来使用它们。但这也意味着 Boost 协程更难被编译器优化,并且可能分配更多内存。协程 TS 提供了 无栈 协程,这意味着编译器可以精确计算协程所需的内存,甚至可以优化整个协程。然而,这种方法需要代码更改,可能稍微难以采用。
另请参阅
-
Boost 的官方文档包含了更多关于
Boost.Coroutines2
库的示例、性能说明、限制和用例;请访问以下链接boost.org/libs/coroutine2
-
查看第二章的示例,资源管理,以及第五章,多线程,了解
Boost.Coroutine
、Boost.Thread
和Boost.Function
库之间的区别 -
对 Coroutines TS 感兴趣吗?这里有一场有趣的关于作者 Gor Nishanov 的实现讨论 CppCon 2016: Gor Nishanov. C++ Coroutines: Under the covers,链接在
www.youtube.com/watch?v=8C8NnE1Dg4A
第十二章:只是冰山一角
在本章中,我们将涵盖:
-
处理图
-
可视化图
-
使用真随机数生成器
-
使用可移植数学函数
-
编写测试用例
-
将多个测试用例组合在一个测试模块中
-
操作图像
介绍
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;
- 现在,我们构建它:
int main() {
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);
} // end of main()
- 我们可以创建一个搜索某个顶点的函数:
inline void find_and_print(
const graph_type& graph, 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(graph);
- 是时候运行搜索所需的顶点了:
typedef typename boost::graph_traits<
graph_type
>::vertex_descriptor desc_t;
for (; it != end; ++ it) {
const desc_t desc = *it;
const vertex_t& vertex = boost::get(
boost::vertex_bundle, graph
)[desc];
if (vertex == name.data()) {
break;
}
}
assert(it != end);
std::cout << name << '\n';
} /* find_and_print */
它是如何工作的...
在步骤 1中,我们描述了我们的图必须是什么样子,以及它必须基于什么类型。boost::adjacency_list
是一个表示图为二维结构的类,其中第一维包含顶点,第二维包含该顶点的边。boost::adjacency_list
必须是表示图的默认选择,因为它适用于大多数情况。
第一个模板参数boost::adjacency_list
描述了用于表示每个顶点的边列表的结构。第二个描述了存储顶点的结构。我们可以使用特定选择器在这些结构中选择不同的标准库容器,如下表所列:
选择器 | 标准库容器 |
---|---|
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 版本中出现问题,因为这一步高度依赖于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++17 的一部分,也不会成为下一个 C++标准的一部分。当前的实现不支持 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
的效率取决于多个因素,如底层容器类型、图表表示、边缘和顶点数据类型。
另请参阅
阅读可视化图表的教程可以帮助您轻松处理图表。您还可以考虑阅读其官方文档,链接如下:boost.org/libs/graph
可视化图表
制作操作图表的程序从来都不容易,因为可视化存在问题。当我们使用标准库容器,如std::map
和std::vector
时,我们可以始终打印容器的内容并查看内部情况。但是,当我们使用复杂的图表时,很难以清晰的方式可视化内容;文本表示对人类不友好,因为它通常包含太多的顶点和边缘。
在本教程中,我们将使用Graphviz工具来可视化Boost.Graph
。
准备工作
要可视化图形,您将需要一个 Graphviz 可视化工具。还需要了解前面的食谱。
如何做...
可视化分为两个阶段。在第一阶段,我们使我们的程序以适合 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
结构必须定义如下:
#include <iosfwd>
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
类的实例,用于输出顶点名称。
正如我们在步骤 2中看到的,Graphviz 工具理解 DOT 格式。如果您希望为图形输出更多信息,则可能需要阅读有关 DOT 格式的 Graphviz 文档以获取更多信息。
如果您希望在可视化过程中向边添加一些数据,我们需要在boost::write_graphviz
的第四个参数中提供边可视化器的实例。
还有更多...
C++17 不包含Boost.Graph
或用于图形可视化的工具。但是您不必担心,因为还有很多其他图形格式和可视化工具,Boost.Graph
可以与其中许多工作。
另请参阅
-
使用图形食谱包含有关构建
Boost.Graphs
的信息 -
您可以在
www.graphviz.org/
找到有关 DOT 格式和 Graphviz 的大量信息 -
Boost.Graph
库的官方文档包含多个示例和有用信息,可以在boost.org/libs/graph
找到
使用真正的随机数生成器
我知道许多商业产品使用不正确的方法来获取随机数。一些公司仍然在密码学和银行软件中使用rand()
,这是令人遗憾的。
让我们看看如何使用Boost.Random
获取适用于银行软件的完全随机的均匀分布。
入门
本食谱需要基本的 C++知识。对于不同类型的分布的了解也将有所帮助。本食谱中的代码需要链接到boost_random
库。
如何做...
要创建真正的随机数,我们需要操作系统或处理器的帮助。以下是使用 Boost 的方法:
- 我们需要包括以下头文件:
#include <boost/config.hpp>
#include <boost/random/random_device.hpp>
#include <boost/random/uniform_int_distribution.hpp>
- 高级随机位提供程序在不同平台下有不同的名称:
int main() {
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
,因为后者在操作系统捕获足够的随机事件之前会保持阻塞状态。等待熵可能需要几秒钟,这通常不适用于应用程序。对于长期使用的GPG/SSL/SSH密钥,请使用/dev/random
。
现在我们已经完成了生成器,是时候转到步骤 4并讨论分布类了。如果生成器只生成均匀分布的位,分布类将从这些位生成一个随机数。在步骤 4中,我们创建了一个返回unsigned short
类型的随机数的均匀分布。参数1000
表示分布必须返回大于或等于1000
的数字。我们还可以提供最大数字作为第二个参数,该参数默认等于返回类型中可存储的最大值。
还有更多...
Boost.Random
有大量用于不同需求的真/伪随机位生成器和分布。避免复制分布和生成器。这可能是一个昂贵的操作。
C++11 支持不同的分布类和生成器。您可以在std::
命名空间的<random>
头文件中找到这个示例中的所有类。Boost.Random
库不使用 C++11 特性,而且该库也不真正需要。您应该使用 Boost 实现还是标准库?Boost 在各个系统之间提供更好的可移植性。但是,一些标准库可能具有汇编优化的实现,并且可能提供一些有用的扩展。
另请参阅
官方文档包含了一份带有描述的生成器和分布的完整列表。它可以在以下链接找到:boost.org/libs/random.
使用可移植数学函数
一些项目需要特定的三角函数、用于数值求解常微分方程和处理分布和常数的库。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 中?嗯,一些编译器供应商认为程序员不需要完全支持 C99,所以你在至少一个非常流行的编译器中找不到这些函数。另一个原因是Boost.Math
函数可能被用于行为类似数字的类。
Boost.Math
是一个非常快速、便携和可靠的库。数学特殊函数是Boost.Math
库的一部分,一些数学特殊函数已经被接受到 C++17 中。然而,Boost.Math
提供了更多的数学特殊函数,并且具有高度可用的递归版本,具有更好的复杂度,更适合一些任务(如数值积分)。
另请参阅
Boost 的官方文档包含许多有趣的示例和教程,这些将帮助你熟悉Boost.Math
。浏览boost.org/libs/math
了解更多信息。
编写测试用例
这个配方和下一个配方都致力于使用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_TEST_DYN_LINK
宏,并链接boost_unit_test_framework
和boost_system
库。
如何做...
老实说,在 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中的宏在旧版本的Boost.Test
库上提供了更易读和冗长的输出。
C++17 缺乏对单元测试的支持。然而,Boost.Test
库可以用来测试 C++17 和 C++11 之前的代码。
请记住,你拥有的测试越多,你得到的可靠代码就越多!
另请参阅
-
将多个测试用例组合在一个测试模块中配方包含了更多关于测试和
BOOST_TEST_MODULE
宏的信息。 -
请参阅 Boost 官方文档
boost.org/libs/test
以获取关于Boost.Test
的所有测试宏和高级功能的信息
将多个测试用例组合在一个测试模块中
编写自动测试对你的项目很有好处。然而,当项目很大并且有许多开发人员在上面工作时,管理测试用例是很困难的。在这个配方中,我们将看看如何运行单独的测试以及如何将多个测试用例组合在一个单一模块中。
假设有两个开发人员正在测试foo.hpp
头文件中声明的foo
结构,我们希望给他们单独的源文件来编写测试。在这种情况下,两个开发人员不会互相打扰,并且可以并行工作。然而,默认的测试运行必须执行两个开发人员的测试。
准备就绪
这个配方需要基本的 C++知识。这个配方部分地重用了上一个配方中的代码,还需要定义BOOST_TEST_DYN_LINK
宏,并链接boost_unit_test_framework
和boost_system
库。
如何做...
这个配方使用了上一个配方中的代码。这是一个非常有用的测试大型项目的配方。不要低估它。
- 从上一个配方的
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++17 标准不支持内置的测试支持,而且看起来 C++20 也不会采用Boost.Test
的类和方法。
另请参阅
-
编写测试用例配方包含了更多关于
Boost.Test
库的信息。阅读 Boost 官方文档boost.org/libs/test
以获取更多关于Boost.Test
的信息。 -
勇敢的人可以尝试查看 Boost 库中的一些测试用例。这些测试用例位于
boost
文件夹中的libs
子文件夹中。例如,Boost.LexicalCast
的测试用例位于boost_1_XX_0/libs/lexical_cast/test
。
操作图像
我已经为你留下了一些非常美味的甜点 - Boost 的通用图像库或者Boost.GIL
,它允许你在不太担心图像格式的情况下操作图像。
让我们做一些简单有趣的事情。例如,让我们制作一个对任何图片进行否定的程序。
准备工作
这个配方需要基本的 C++、模板和Boost.Variant
的知识。示例需要链接png
库。
如何做...
为了简化示例,我们将只使用 PNG 图像。
- 让我们从包含头文件开始:
#include <boost/gil/gil_all.hpp>
#include <boost/gil/extension/io/png_dynamic_io.hpp>
#include <string>
- 现在,我们需要定义我们希望使用的图像类型:
int main(nt argc, char *argv[]) {
typedef boost::mpl::vector<
boost::gil::gray8_image_t,
boost::gil::gray16_image_t,
boost::gil::rgb8_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 位的 RGB 图片。
boost::gil::any_image<img_types>
类是一种可以容纳img_types
变量之一的图像的Boost.Variant
。正如你可能已经猜到的那样,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
的 typedef。你可以在步骤 6中看到它。
一点理论知识:图像由称为像素的点组成。一个图像有相同类型的像素。然而,不同图像的像素可能在通道计数和单个通道的颜色位方面有所不同。通道代表主要颜色。在 RGB 图像的情况下,我们有一个由三个通道 - 红色、绿色和蓝色组成的像素。在灰度图像的情况下,我们有一个表示灰度的单个通道。
回到我们的图片。在步骤 2中,我们描述了我们希望使用的图像类型。在步骤 3中,其中一种图像类型从文件中读取并存储在源变量中。在步骤 4中,为所有图像类型实例化了negate
访问者的operator()
方法。
在步骤 7中,我们可以看到如何从图像视图中获取通道类型。
在步骤 8中,我们遍历像素和通道并对其进行否定。否定是通过max_val - source(x, y)[c]
来完成的,并将结果写回图像视图。
我们在步骤 5中将图像写回。
还有更多...
C++17 没有内置的方法来处理图像。目前正在进行工作,将 2D 绘图添加到 C++标准库中,尽管这是一种有点正交的功能。
Boost.GIL
库快速高效。编译器对其代码进行了良好的优化,我们甚至可以使用一些Boost.GIL
方法来帮助优化器展开循环。但本章仅讨论了库的一些基础知识,所以是时候停下来了。
另请参阅
-
有关
Boost.GIL
的更多信息可以在 Boost 的官方文档boost.org/libs/gil
中找到 -
在第一章的存储多种选择的类型在一个变量/容器中配方中查看更多关于
Boost.Variant
库的信息 -
请查看
isocpp.org/
了解更多关于 C++的新闻 -
查看
stdcpp.ru/
讨论关于 C++提案的俄语文章
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
2022-05-04 Impatient JavaScript 中文版校对活动期待大家的参与
2022-05-04 ApacheCN 翻译/校对活动进度公告 2022.5.4
2022-05-04 非安全系列教程 NPM、PYPI、DockerHub 备份
2022-05-04 UIUC CS241 系统编程中文讲义校对活动 | ApacheCN