## C++17 中那些值得关注的特性(上) 文/祁宇 >C++ 17 标准在2017上半年已经讨论确定,正在形成 ISO 标准文档,今年晚些时候会正式发布。本文将介绍最新标准中值得开发者关注的新特新和基本用法。 总的来说 C++ 17 相比 C++ 11 的新特性不算多,做了一些小幅改进。C++ 17 增加了数十项新特性,值得关注的大概有下面这些: - constexpr if - constexpr lambda - fold expression - void _ t - structured binding - std::apply, std::invoke - string _ view - parallel STL - inline variable 剩下的有一些来自于 boost 库,比如 variant、any、optional 和 filesystem 等特性,string _ view 其实在 boost 里也有。还有一些是语法糖,比如 if init、deduction guide、guaranteed copy Elision、`template`、nested namespace、single param static _ assert 等特性。我接下来会介绍 C++ 17 主要的一些特性,它们的基本用法和作用,让读者对 C++ 17 的新特性有一个基本的了解。 ### fold expression C++ 11 增加了一个新特性可变模版参数(variadic template),它可以接受任意个模版参数在参数包中,参数包是三个点……,它不能直接展开,需要通过一些特殊的方法,导致在使用的时候有点难度。现在 C++ 17 解决了这个问题,让参数包的展开变得容易了,Fold expression 就是方便展开参数包的。 #### fold expression 的语义 fold expression 有4种语义: 1. unary right fold (pack op ...) 2. unary left fold (... op pack) 3. binary right fold (pack op ... op init) 4. binary left fold (init op ... op pack) 其中 pack 代表变参,比如 args,op 代表操作符,fold expression 支持32种操作符: + - * / % ^ & | = < > << >> += -= *= /= %= ^= &= |= <<= >>= == != <= >= && || , .* ->* #### unary right fold 的含义 fold (E op …) 意味着 `E1 op (… op (EN-1 op EN))`。 顾名思义,从右边开始 fold,看它是 left fold 还是 right fold 我们可以根据参数包……所在的位置来判断,当参数包……在操作符右边的时候就是 right fold,在左边的时候就是 left fold。我们来看一个具体的例子: ``` template auto add_val(Args&&... args) { return (args + ...); } auto t = add_val(1,2,3,4); //10 ``` right fold 的过程是这样的:(1+(2+(3+4))),从右边开始 fold。 #### unary left fold 的含义 fold (... op E) 意味着 `((E1 op E2) op ...) op EN`。 对于+这种满足交换律的操作符来说 left fold 和 right fold 是一样的,比如上面的例子你也可以写成 left fold。 ``` template auto add_val(Args&&... args) { return (... + args); } auto t = add_val(1,2,3,4); //10 ``` 对于不满足交换律的操作符来说就要注意了,比如减法。 ``` template auto sub_val_right(Args&&... args) { return (args - ...); } template auto sub_val_left(Args&&... args) { return (... - args); } auto t = sub_val_right(2,3,4); //(2-(3-4)) = 3 auto t1 = sub_val_left(2,3,4); //((2-3)-4) = -5 ``` 这次 right fold 和 left fold 的结果就不一样。 #### binary fold 的含义 Binary right fold (E op ... op I) 意味着 `E1 op (... op (EN-1 op (EN op I)))`。 Binary left fold (I op ... op E) 意味着 `(((I op E1) op E2) op ...) op E2`。 其中 E 代表变参,比如 args,op 代表操作符,I 代表一个初始变量。 二元 fold 的语义和一元 fold 的语义是相同的,看一个二元操作符的例子: ``` template auto sub_one_left(Args&&... args) { return (1 - ... - args); } template auto sub_one_right(Args&&... args) { return (args - ... - 1); } auto t = sub_one_left(2,3,4);// (((1-2)-3)-4) = -8 auto t1 = sub_one_right(2,3,4);//(2-(3-(4-1))) = 2 ``` 相信通过这个例子大家应该对 C++ 17 的 fold expression 有了基本的了解。 #### comma fold 在 C++ 17 之前,我们经常使用逗号表达式和 std::initializer _ list 来将变参一个个传入一个函数。比如像下面这个例子: ``` template void print_arg(T t) { std::cout << t << std::endl; } template void print2(Args... args) { //int a[] = { (printarg(args), 0)... }; std::initializer_list{(print_arg(args), 0)...}; } ``` 这种写法比较繁琐,用 fold expression 就会变得很简单了。 ``` template void print3(Args... args) { (print_arg(args), ...); } ``` 这是 right fold,你也可以写成 left fold,对于 comma 来说两种写法是一样的,参数都是从左至右传入 print _ arg 函数。 ``` template void print3(Args... args) { (..., print_arg(args)); } ``` 你也可以通过 binary fold 这样写: ``` template void printer(Args&&... args) { (std::cout << ... << args) << '\n'; } ``` 也许你会觉得能写成这样: ``` template void printer(Args&&... args) { (std::cout << args << ...) << '\n'; } ``` 但这样写是不合法的,根据 binary fold 的语法,参数包……必须在操作符中间,因此上面的这种写法不符合语法要求。 借助 comma fold 我们可以简化代码,假如我们希望实现 tuple 的 for _ each 算法,像这样: ``` for_each(std::make_tuple(2.5, 10, 'a'),[](auto e) { std::cout << e<< '\n'; }); ``` 这个 for _ each 将会遍历 tuple 的元素并打印出来。在 C++ 17 之前我们如果要实现这个算法的话,需要借助逗号表达式和 std::initializer _ list 来实现,类似于这样: ``` template void for_each(const std::tuple& t, Func&& f, std::index_sequence) { (void)std::initializer_list { (f(std::get(t)), void(), 0)...}; } ``` 这样写比较繁琐不直观,现在借助 fold expression 我们可以简化代码了。 ``` template void for_each(const std::tuple& t, Func&& f, std::index_sequence) { (f(std::get(t)), ...); } ``` 借助 coma fold 我们可以写很简洁的代码了。 ### constexpr if constexpr 标记一个表达式或一个函数的返回结果是编译期常量,它保证函数会在编译期执行。相比模版来说,实现编译期循环或递归,C++ 17 中的 constexpr if 会让代码变得更简洁易懂。比如实现一个编译期整数加法: ``` template constexpr int sum() { return N; } template constexpr int sum() { return N + sum(); } ``` C++ 17 之前你可能需要像上面这样写,但是现在你可以写更简洁的代码了。 ``` template constexpr auto sum17() { if constexpr (sizeof...(Ns) == 0) return N; else return N + sum17(); } ``` 当然,你也可以用 C++ 17 的 fold expression: ``` template constexpr int sum(Args... args) { return (0 + ... + args); } ``` constexpr 还可以用来消除 enable _ if 了,对于讨厌写一长串 enable _ if 的人来说会非常开心。比如我需要根据类型来选择函数的时候: ``` template std::enable_if_t::value, std::string> to_str(T t) { return std::to_string(t); } template std::enable_if_t::value, std::string> to_str(T t) { return t; } ``` 经常不得不分开几个函数来写,还需要写长长的 enable _ if,比较繁琐,通过 if constexpr 可以消除 enable _ if 了。 ``` template auto to_str17(T t) { if constexpr(std::is_integral::value) return std::to_string(t); else return t; } ``` constexpr if 让 C++ 的模版具备 if-else if-else功能了,是不是很酷,C++ 程序员的好日子来了。 不过需要注意的是下面这种写法是有问题的。 ``` template auto to_str17(T t) { if constexpr(std::is_integral::value) return std::to_string(t); return t; } ``` 这个代码把 else 去掉了,当输入如果是非数字类型时代码可以编译过,以为 if constexpr 在模版实例化的时候会丢弃不满足条件的部分,因此函数体中的前两行代码将失效,只有最后一句有效。当输入的为数字的时候就会产生编译错误了,因为 if constexpr 满足条件了,这时候就会有两个 return 了,就会导致编译错误。 constexpr if 还可以用来替换 #ifdef 宏,看下面的例子 ``` enum class OS { Linux, Mac, Windows }; //Translate the macros to C++ at a single point in the application #ifdef __linux__ constexpr OS the_os = OS::Linux; #elif __APPLE__ constexpr OS the_os = OS::Mac; #elif __WIN32 constexpr OS the_os = OS::Windows; #endif void do_something() { //do something general if constexpr (the_os == OS::Linux) { //do something Linuxy } else if constexpr (the_os == OS::Mac) { //do something Appley } else if constexpr (the_os == OS::Windows) { //do something Windowsy } //do something general } //备注:这个例子摘自https://blog.tartanllama.xyz/c++/2016/12/12/if-constexpr/ ``` 代码变得更清爽了,再也不需要像以前一样写 #ifdef 那样难看的代码块了。 #### constexpr lambda constexpr lambda 其实很简单,它的意思就是可以在 constexpr 函数中用 lambda 表达式了,这在 C++ 17 之前是不允许的。这样使用 constexpr 函数和普通函数没多大区别了,使用起来非常舒服。下面是 constexpr lambda 的例子: ``` template constexpr auto func(I i) { //use a lambda in constexpr context return [i](auto j){ return i + j; }; } ``` constexpr if 和 constexpr lambda 是 C++ 17 提供的非常棒的特性,enjoy it. ### string _ view #### string _ view 的基本用法 C++ 17 中的 string _ view 是一个 char 数据的视图或者说引用,它并不拥有该数据,是为了避免拷贝,因此使用 string _ view 可以用来做性能优化。你应该用 string _ view 来代替 const char 和 const string了。string _ view 的方法和 string 类似,用法很简单: ``` const char* data = "test"; std::string_view str1(data, 4); std::cout< ``` 构造 string _ view 的时候用 char* 和长度来构造,这个长度可以自由确定,它表示 string _ view 希望引用的字符串的长度。因为它只是引用其他字符串,所以它不会分配内存,不会像 string 那样容易产生临时变量。我们通过一个测试程序来看看 string _ view 如何来帮我们优化性能的。 ``` using namespace std::literals; constexpr auto s = "it is a test"sv; auto str = "it is a test"s; constexpr int LEN = 1000000; boost::timer t; for (int i = 0; i < LEN; ++i) { constexpr auto s1 = s.substr(3); } std::cout< ``` 我们可以通过字面量""sv来初始化 string _ view。string _ view 的 substr 和 string 的 substr 相比,快了50多倍,根本原因是它不会分配内存。 #### string _ view 的生命周期 由于 string _ vew 并不拥有锁引用的字符串,所以它也不会去关注被引用字符串的生命周期,用户在使用的时候需要注意,不要将一个临时变量给一个 string _ view,那样会导致 string _ view 引用的内容也失效。 ``` std::string_view str_v; { std::string temp = "test"; str_v = {temp}; } ``` 这样的代码是有问题的,因为出了作用域之后,string _ view 引用的内容已经失效了。 ### 总结 本文介绍了 C++ 17 的 fold expression、constexpr if、constexpr lambda 和 string _ view。fold expression 为了简化可变模板参数的展开,让可以模板参数的使用变得更简单直观;constexpr if 让模板具备 if-else 功能,非常强大。它也避免了写冗长的 enable _ if 代码,让代码变得简洁易懂了;string _ view 则是用来做性能优化的,应该用它来代替 const char* 和 const string。 这些特性对之前的 C++ 14 和 C++ 11 做了改进和增强,非常酷,后续文章会接着介绍其他 C++ 17 中值得关注的新特性。