## 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并行算法 图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 的并行算法值得好好研究,这些并行算法在高性能领域非常有潜力。