原文:
annas-archive.org/md5/8a1821d22bcd421390c328e6f1d92500
译者:飞龙
第七章:操作字符串
在本章中,我们将涵盖:
-
更改大小写和不区分大小写比较
-
使用正则表达式匹配字符串
-
使用正则表达式搜索和替换字符串
-
使用安全的 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
库。