## C++17 中那些值得关注的特性 (下)
文/祁宇
>接着前两篇文章,本文将介绍剩下的一些 C++ 17 特性,这些特性一部分来自于 boost 库,一部分则是用来让代码写得更加简洁便利,算是语法糖;还有一部分是新增算法。来自于 boost 库的特性有:variant、any、optional、filesystem 和 string _ view(string _ view 已在前文介绍过);让代码更加简洁便利的特性有:nested namespaces、single param staticassert、if init、deduction guide、capture this 等;还有一些是新增加的实用算法和并行算法。
### 来自 boost 库的特性
#### std::any
std::any 可以用来存放任意类型的对象,在某些时候可以用来做类型擦除。std::any 的用法比较简单,和 boost.any 的用法几乎是一致的,下面是它的基本用法。
```
void test_any()
{
std::any v1;
int a = 2;
v1 = a;
if(v1.has_value())
std::cout<(v1)<<'\n';
double b = 3.0;
v1 = b;
if(v1.has_value())
std::cout<(v1)<<'\n';
try
{
std::cout<(v1)<<'\n';
}
catch(std::bad_any_cast& e)
{
std::cout<
```
将输出:
```
2
3
bad any_cast
no value
```
std::any 虽然能很方便地保存任意类型的对象,但是从 any 中取出原来保存的对象时需要调用 `any_cast`,any _ cast 要求传入原始的精确的类型,如果类型不一致将会抛出一个 std::bad _ any _ cast 的异常,由于这一对类型强烈渴求的特点,使得 any 在做类型擦出时多有不便。
很多人对于 any 能保存任意对象的特点非常喜欢,所以喜欢到处用 any,这会导致 any 的滥用,因为 any 保存对象时会有堆内存分配,如果在性能敏感的场景下使用势必会得不偿失。对于 any 这个特性建议谨慎使用,类型擦除可以优先使用 std:: variant(这个特性我们稍后会介绍)。
#### std::optional
optional 体现了一个“可能值”的概念,即某一个值可能被初始化也可能没有被初始化。它的用法也很简单:
```
std::optional divide_d(double a,
double b)
{
if(b==0)
return {};
return a/b;
}
void test_optional()
{
std::optional r = divide_d(6, 2);
if(r.has_value())
std::cout<<"has init"<<'\n';
if(r)
std::cout<<*r<<'\n';
std::optional r1 = divide_d(6, 0);
if(r1==std::nullopt)
std::cout<<"not init"<<'\n';
}
```
optional 很适合不方便返回不合法值的场景,比如本来要返回一个 int 值,但函数内部发现输入参数有问题,这时应该返回一个 int 值告诉调用者,但返回什么值都不合适,这时就用 optional 来表达一个可能的值让调用者知道如果这个返回值没有初始化的话,那说明这个接口调用是失败的。还有一些情况,比如解析 xml 或者其他协议的文本时,有些节点是可能不存在的,这时用 optional 就非常合适了,刚好表达了“可能值”这个概念。
#### std:: variant
varaint 你可以把它看成是一个类型安全的 union,也可以看成是一个类型容器,比较适合做类型擦除。它的用法相比之前的 optional 和 any 要复杂一些:
```
void test_variant()
{
std::variant v, w;
v = 1;
int i = std::get(v);
w = "abc";
auto s = std::get(w);
try
{
std::get(w);
}
catch(const std::bad_variant_access& e)
{
std::cout<> vec;
vec.push_back(v);
vec.push_back(w);
}
```
从上面的例子中可以看到 variant 可以保存不同类型的对象,不过这些对象的类型必须是在 variant 定义的类型范围之内,并且不允许有重复类型存在。由于 variant 做了类型擦除,所以我们可以借助它将不同类型的对象放到容器中。
上面的例子中是通过 std::get 来访问 variant 的,在很多时候我们希望通过一种更加泛化的方式来访问 variant,而不是显示指定类型来访问。C++ 17 提供了泛化的专门用来访问 variant 的方法 std::visit,通过 std::visit 我们可以很方便地访问 variant 对象。下面是 std::visit 访问 variant 的方法:
```
void test_variant()
{
std::variant v, w;
v = 1;
w = "abc";
std::vector> vec;
vec.push_back(v);
vec.push_back(w);
for(auto& item : vec)
{
std::visit([](auto&& arg){std::cout<;
if constexpr(std::is_same_v)
std::cout<<"int value";
else if constexpr (std::is_same_v)
std::cout<<"string value";
else
static_assert("no matching");
}, item);
}
}
```
std::visit 可以直接访问 variant 内部的对象,因为使用了 auto lambda,对于 auto&& arg 来说我们不清楚这个 arg 的类型到底是什么,借助 C++ 17 的 if constexpr,我们可以清楚的知道 arg 具体是什么类型了。
std:: variant 的构造没有堆内存分配,相比 std::any 来说性能更好,而且也不像 any 那样访问内部对象时对类型那么渴求,所以一般情况下应该用 variant 来代替 any 做类型擦除。当然 variant 也有一个不足之处,你需要实现定义所要擦除的类型,有可能需要擦除的类型在开始的时候是无法预知的。
#### std::filesystem
std::filesystem 和 boost::filesystem 用法差不多,主要是用来对路径、文件和目录进行查询和操作。用法也比较简单:
```
namespace fs = std::filesystem;
std::string path = "C:\\test";
for(auto& p : fs::directory_iterator("test"))
std::cout<
```
### 让代码更加简洁便利的特性
#### nested namespaces
C++ 中如果有多重命名空间的时候,需要嵌套多层,写法上比较难看,现在 C++ 17 让多重命名空间的写法变得简单多了。下面是一个 C++ 11/14 和 C++ 17 多重命名空间写法上的对比:
C++ 17 之前的写法
```
namespace A {
namespace B {
namespace C {
struct Foo { };
//...
}
}
}
//C++17的写法
namespace A::B::C {
struct Foo { };
//...
}
```
C++ 17 的写法摆脱了冗长的嵌套和重复,孰繁熟简一目了然。
#### single param staticassert
C++ 11/14 中 static _ assert 断言的时候,必须要填两个参数,前面填条件,后面填断言失败时的提示信息,这在写一些测试代码的时候不够方便,现在 C++ 17 支持了单参数的 static _ assert,当你不想写任何编译期断言失败提示时,就可以不写交给编译器,让编译器告诉用户哪里断言失败了,代码可以写得更简洁了。
```
static_assert(false, "something wrong");
static_assert(false); //省略提示信息,让编译器告诉用户
```
#### if init
C++ 17 允许在 if 条件表达式中初始化变量了,这会让代码写起来更方便,比如在 C++ 11 中我们这样写:
```
std::map mp;
auto pair = mp.insert({1, false});
if(pair.second)
{
std::cout<< (*pair.first).first <<'\n';
}
else
{
std::cout<<"duplicate"<<'\n';
}
```
我们要先定义一个变量,再判断这个变量值做处理。在 C++ 17 中,这个变量可以放到 if 的条件中定义了,不需要单独拿出来定义,像这样:
```
std::map mp;
if(auto pair = mp.insert({1, false}); pair.second)
{
std::cout<< (*pair.first).first <<'\n';
}
else
{
std::cout<<"duplicate"<<'\n';
}
```
if 表达式中,先定义了一个变量 pair,这个 pair 随后用来做判断条件。注意 if init 中定义的变量作用域就在这个 if 语句中,出了这个 if 语句,定义的变量 pair 就会析构。我们可以利用这个特性来实现自动加锁保护 if 代码段。
```
std::mutex mtx;
std::vector v;
if(std::lock_guard lock(mtx);v.empty())
{
v.push_back(1);
}
```
上面的代码利用了 if init 定义的变量的作用域实现了对 if 语句自动加解锁。在 C++ 11 中你需要这样写:
```
std::mutex mtx;
std::vector v;
{
std::lock_guard lock(mtx);
if(v.empty())
{
v.push_back(1);
}
}
```
相比之下,C++ 17 利用 if init 的写法更加简洁和紧凑,在语意上更加容易理解。
#### deduction guide
deduction guide 可以根据参数自动推导出对应的类型,这可以让我们的代码变得更加简洁,看下面的写法:
```
std::pair p(1, 1.5); //推导为std::pair
std::tuple t(1, 2, 2.5); //推导为std::tuple
std::vector v{1,2,3}; //推导为std::vector
std::array ar{1,2}; //推导为std::array
```
从上面的例子中可以看到隐式的 deduction guide 可以让我们的代码写得更加简洁,不用再写模版参数等细节了,有一种写动态语言的感觉。
除了隐式的 deduction guide,还有一种显式的 deduction guide,作用和隐式 deduction guide 差不多,也是让写法变得更简洁。下面是显式 deduction guide 的例子:
```
template
struct Dummy { T t; };
Dummy(double) -> Dummy;
Dummy(std::pair) -> Dummy>;
void test()
{
Dummy dm{2.5};
Dummy dm1{2};
std::pair pr{1,2};
Dummy dm2{pr};
}
```
如果没有通过 Dummy(double) -> `Dummy` 显式地做 deduction guide,我们在定义 Dummy 的时候是需要显式带着模版参数的。也许有人觉得就为了省掉一个模版参数却要多定义一个显式的 deduction guide,似乎还变麻烦了。其实显式 deduction guide 主要是为了简化定义可变模版参数的变量,之前介绍过通过 std::visit 来访问 variant,通过 auto lambda 和 if constexpr 来访问 variant 的做法不是很方便,我们可以通过可变模板和显式 deduction guide 的来实现一个访问 variant 的更好的方法。
```
template struct overloaded : Ts... { using Ts::operator()...; };
template overloaded(Ts...) -> overloaded;
void visit_variant()
{
std::variant v, w;
v = 1;
w = "abc";
std::vector> vec;
vec.push_back(v);
vec.push_back(w);
for (auto& it: vec) {
std::visit(overloaded {
[](auto arg) { std::cout << arg << '\n'; },
[](double arg) { std::cout << std::fixed << arg << '\n'; },
[](const std::string& arg) { std::cout << arg << '\n'; },
}, it);
}
}
```
上面的例子中先通过可变模版参数的 CRTP 获得参数的 operater(),然后通过显式的 deduction guide 来省去定义含可变模版参数 overloaded 的变量时,需指定变参类型的麻烦,让代码变得非常简洁。这里不仅仅是让代码变得简洁,还直接避免了一个难题,因为 overloaded 参数是 lambda,它是一个匿名类型,你无法获取 lambda 的类型并实例化 overloaded 模版。当然我们还可以像 boost 中定一个函数对象的方式来访问 variant,像下面这样:
```
struct visitor
{
void operator()(int arg){ std::cout << arg << '\n'; }
void operator()(double arg){ std::cout << arg << '\n'; }
void operator()(const std::string& arg){ std::cout << arg << '\n'; }
};
void visit_variant()
{
std::variant v, w;
v = 1;
w = "abc";
std::vector> vec;
vec.push_back(v);
vec.push_back(w);
visitor vt;
for (auto& it: vec) {
std::visit(vt, it);
}
}
```
#### capture *this
C++ 17 中的 capture *this 允许我们拷贝 this 对象,在 C++ 17 之前,如果我们需要拷贝 this 对象需要这样写:
```
[=,copy=*this]{ }
```
显得比较烦琐,C++ 17 里就变得很简洁了,直接写:
```
[=,*this]{ }
```
也许有人会有疑惑,我为什么需要拷贝 this 对象呢,因为在有些场景下,执行 lambda 的时候 this 对象内部的状态可能会发生变化,我不希望执行 lambda 的时候这个状态变了,这时就希望通过拷贝 this 对象来确保之前的状态不会变。
### 新增加的算法
#### std::search
C++ 17 中新增了一些实用算法,std::search 就是在一个 range 中查找一个子 range,这很容易让我们联想到查找公共子串的算法。下面是 std::search 的用法:
```
void test_new_alg()
{
std::string in = "Lorem ipsum dolor sit amet, consectetur adipiscing elit,"
" sed do eiusmod tempor incididunt ut labore et dolore magna aliqua";
std::string needle = "pisci";
auto it = std::search(in.begin(), in.end(), needle.begin(), needle.end());
if (it != in.end())
std::cout << "Found at " << (it - in.begin())<<'\n';
}
```
也许有人会疑惑,这个 std::search 和 std::find _ first _ of 算法有什么不同吗,二者最大的不同是:std::search 是按一个子 range 范围查找的,而 std::find _ first _ of 是按 range 中的任意一个元素去超找的。
#### 并行算法
C++ 17 中增加了很多并行算法,主要在 algorithm,numeric 和 memory 下面,见图1。
图1 C++ 17 并行算法
由于目前的编译器还没有完全支持 C++ 17 的并行算法,而且并行算法比较多,这里仅用 cppreference.com 上的一个例子来展示并行算法的用法:
```
std::vector v(10'000'007, 0.5);
{
auto t1 = std::chrono::high_resolution_clock::now();
double result = std::accumulate(v.begin(), v.end(), 0.0);
auto t2 = std::chrono::high_resolution_clock::now();
std::chrono::duration ms = t2 - t1;
std::cout << std::fixed << "std::accumulate result " << result
<< " took " << ms.count() << " ms\n";
}
{
auto t1 = std::chrono::high_resolution_clock::now();
double result = std::reduce(std::execution::par, v.begin(), v.end());
auto t2 = std::chrono::high_resolution_clock::now();
std::chrono::duration ms = t2 - t1;
std::cout << "std::reduce result "
<< result << " took " << ms.count() << " ms\n";
}
//output
std::accumulate result 5000003.50000 took 12.7365 ms
std::reduce result 5000003.50000 took 5.06423 ms
```
可以看到并行算法比非并行算法效率提升了两倍多,可以看到 C++ 17 的并行算法将进一步提高程序的计算效率,将进一步提升 C++ 在高性能计算领域的能力!
### 总结
本文主要介绍了来自 boost 库的特性如 any、optional、variant 和 filesystem,这些特性在 boost 中存在已久并且挺实用的,加入到标准库中作为一些便利地工具;还介绍了让代码变得简洁便利的一些特性,比如 nested namespace、if init、dedution guide 等特性确实能让我们的代码写得更加简洁优雅;最后介绍了新增的算法,尤其是并行算法可以大幅提升程序的计算效率,C++ 17 的并行算法值得好好研究,这些并行算法在高性能领域非常有潜力。