C++ 17如何从Boost库中获益
一、简介
本文展示一些久经考验的特性,这些特性来自于著名的Boost库,它们被引入到了C++ 17中。Boost中的许多元素现在都是标准库的一部分,随着标准库中元素数量的增加,在Boost经验的支持下,可以编写更流畅的C++代码。
Boost库提供了大量标准库中没有的方便算法、类型和特性。许多功能被“移植”到C++的核心中。例如,在C++ 11中,获得了std::regex
、线程和智能指针等。
因此,可以将Boost视为标准库之前的测试战场。C++的新标准中有很多元素是从Boost中迁移过来的。例如:
- 词汇类型:
std::variant
,std::any
,std::optional
。 string_view
。- 搜索算法:
Boyer Moore
和Boyer Moore Horspool
。 std::filesystem
。- 特殊数学函数。
template
的改进。
这带来的好处是,如果只使用Boost的一小部分,如Boost::variant
或Boost::optional
,现在可以使用几乎相同的代码并转换为标准库类型(std::variant
和std::optional
)。
让我们一起来了解C++中一些很酷的东西吧!
二、词汇类型
第一个是“词汇类型(Vocabulary Types)”。能够编写富有表现力的代码是一项引人注目的能力。有时,仅使用内置类型并不能提供这些选项。例如,可以设置一些数字并将其赋值为“NOT_NUMBER”,或者将-1
的值视为空项。作为“终极解决方案”,甚至可以使用指针并将nullptr
视为null
,但是从标准中获得显式类型不是更好吗?
如果使用过Boost,那么可能会遇到Boost::optional
、Boost::variant
和Boost::any
这样的类型。
不是将-1
视为“空数”,而是利用optional<int>
:如果optional
是“空”,那么没有一个数字,这就很简单。
另外,variant<string, int, float>
是允许存储三种可能的类型并在运行时在它们之间切换的类型。
最后,还有一些类似于动态语言中的var
类型;它可以存储任何类型并动态更改它们。它可能是整型,之后可以把它转换成字符串。
2.1、std::optional
第一个是std::optional
,先看一下代码:
template <typename Map, typename Key>
std::optional<typename Map::value_type::second_type> TryFind(const Map& m, const Key& k) {
auto it = m.find(k);
if (it != m.end())
return std::make_optional(it->second);
return std::nullopt;
}
TryFind
返回存储在映射中的可选值或空值。
TryFind
使用方式:
std::map<std::string, int> mm { {"hello", 10}, { "super", 42 }};
auto ov = TryFind(mm, "hello");
// one:
std::cout << ov.value_or(0) << '\n';
// two:
if (ov)
std::cout << *ov << '\n';
如果可选的ov
包含一个值,可以通过.value()
成员函数或操作符*
访问它。在上面的代码中,使用了另一种替代方法,即value_or()
函数,它返回存在的值或返回传递的参数。
示例代码:
#include <iostream>
#include <map>
#include <optional>
#include <string>
template <typename Map, typename Key>
std::optional<typename Map::value_type::second_type> TryFind(const Map& m, const Key& k) {
auto it = m.find(k);
if (it != m.end())
return std::make_optional(it->second);
return std::nullopt;
}
int main() {
std::map<std::string, int> mm { {"hello", 10}, { "super", 12 }};
auto ov = TryFind(mm, "hello");
std::cout << ov.value_or(0) << '\n';
return 0;
}
2.2、std::variant
std::optional
存储一个值或者什么都不存储,那么在安全联合类型中存储更多类型如何?示例代码:
std::variant<int, float, std::string> TryParseString(std::string_view sv) {
// try with float first
float fResult = 0.0f;
const auto last = sv.data() + sv.size();
const auto res = std::from_chars(sv.data(), last, fResult);
if (res.ec != std::errc{} || res.ptr != last) {
// if not possible, then just assume it's a string
return std::string{sv};
}
// no fraction part? then just cast to integer
if (static_cast<int>(fResult) == fResult)
return static_cast<int>(fResult);
return fResult;
}
std::variant
可用于将不同类型存储为解析结果。一个常见的案例是解析命令行或某些配置文件。函数TryParseString
接受一个字符串视图,然后尝试将其解析为float
、int
或string
。如果浮点值没有分数部分,则将其存储为整数。否则,它就是float
。如果不能执行数值转换,则该函数复制该字符串。
要访问存储在变体中的值,首先必须知道活动类型。下面的代码展示了如何做到这一点,并使用TryParseString
的返回值:
const auto var = TryParseString("12345.98");
try {
if (std::holds_alternative<int>(var))
std::cout << "parsed as int: " << std::get<int>(var) << '\n';
else if (std::holds_alternative<float>(var))
std::cout << "parsed as float: " << std::get<float>(var) << '\n';
else if (std::holds_alternative<string>(var))
std::cout << "parsed as string: " << std::get<std::string>(var) << '\n';
}
catch (std::bad_variant_access&) {
std::cout << "bad variant access...\n";
}
主要思想是使用std::holds_alternative()
,它允许检查存在的类型。variant
还提供了.index()
成员函数,该函数返回从0…到存储类型的最大num
的数字。
但是,最酷的用法之一是std::visit()
。使用这个新功能,可以传递一个变体并访问主动存储的类型。要做到这一点,需要提供一个对给定变体中所有可能类型具有调用操作符的函子:
struct PrintInfo {
void operator()(const int& i) const { cout << "parsed as int" << i << '\n'; }
void operator()(const float& f) const { cout << "parsed as float" << f << '\n'; }
void operator()(const string& s) const { cout << "parsed as str" << s << '\n'; }
};
auto PrintVisitorAuto = [](const auto& t) { std::cout << t << '\n'; };
const auto var = TryParseString("Hello World");
std::visit(PrintVisitorAuto , var);
std::visit(PrintInfo{}, var);
在上面的例子中,使用了两种类型的访问者。第一个是PrintInfo
,它是一个结构体,提供了调用操作符的所有覆盖;可以使用它来显示关于给定类型的更多信息,并执行唯一的实现。另一个是PrintVisitorAuto
,利用泛型lambda
,如果所有类型的实现都是相同的,这是很方便的。
2.3、std::any
std::any
可能是最不为人所知的词汇表类型,而且这种灵活类型的用例并不多。它很像JavaScript中的var
,因为它可以保存任何东西。
使用示例:
struct property {
property();
property(const std::string &, const std::any &);
std::string name;
std::any value;
};
typedef std::vector<property> properties;
有了这样的属性类,可以存储任何类型。但是,如果可以知道可能类型的数量,那么最好使用std::variant
,因为它比std::any
执行得快(不需要额外的动态内存分配)。
更多用例可以参考Any Library Proposal for TR2 - https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n1939.html
2.4、其他
想了解更多的词汇类型以及它们的特性、用法、案例,可以阅读我的历史博客中写的单独的文章。
三、非所属字符串 std::string_view
std::string_view
是连续字符序列的非归属视图。它在Boost中已经准备好几年了(参见Boost utils string_view)。现在boost版本与C++ 17版本string_view
的接口是一致的。
从概念上讲,string_view
由指向字符序列的指针和大小组成:
struct BasicCharStringView {
char* dataptr;
size_t size;
};
std::string_view
有什么独特之处?
首先,string_view
是char*
参数的自然替代品。如果函数接受const char*
,然后对它执行一些操作,那么也可以使用视图,并且它的接口类似字符串的接口。
示例:
#include <iostream>
#include <string_view>
size_t CStyle(const char* str, char ch) {
auto chptr = strchr(str, ch);
if (chptr != nullptr)
return strlen(str) + (chptr - str);
return strlen(str);
}
size_t CppStyle(std::string_view sv, char ch) {
auto pos = sv.find(ch);
if (pos != std::string_view::npos)
return sv.length() + pos;
return sv.length();
}
int main() {
std::cout << CStyle("Hello World", 'X') << '\n';
std::cout << CppStyle("Hello World", 'X') << '\n';
return 0;
}
类似地,有许多类似字符串的类实现。例如CString
, QString
等等,如果代码需要处理很多类型,string_view
可能会有所帮助。那些其他类型可以提供对数据指针和大小的访问,然后可以创建一个string_view
对象。
视图在处理大型字符串和切割较小的部分时也可能很有帮助。例如,在文件解析中,可以将文件内容加载到单个std::string
对象中,然后使用视图执行处理。这可能会显示出很好的性能提升,因为不需要任何额外的字符串副本。
不过需要记住,因为string_view
不拥有数据,也可能不是null
终止,使用它有一些风险:
- 小心(非)以NULL结尾的字符串:
string_view
可能在字符串的末尾不包含NULL。所以必须为这种情况做好准备。因为在调用像atoi
,printf
这样接受以null
结束的字符串的函数时会出现问题。 - 引用和临时对象问题:
string_view
不拥有内存,所以在处理临时对象时必须非常小心。例如从函数返回string_view
时,或者将string_view
存储在对象或容器中;都可能会出现问题。
另一个好消息:Boost中的starts_with()/ends_with()
算法现在是C++ 20的一部分,许多编译器已经实现了它们。它们可用于string_view
和std::string
。
四、总结
希望这篇博文能给你更多开始使用C++ 17的动力。最新的C++17标准不仅提供了许多语言特性(如if constexpr
、结构化绑定、折叠表达式、……),而且还提供了来自标准库的一组广泛的实用程序。现在可以使用多种词汇表类型:variant
、optional
、any
。使用字符串视图,甚至使用一个重要的组件std::filesystem
。所有这些都不需要引用一些外部库。
互动一下吧,在评论中分享你的经验。
- 你最喜欢的Boost功能是什么?也许它们也会被合并到标准中?
- 你是否将一些boost代码移植到C++ 17中?