总的来说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种语义:
unary right fold (pack op …)
unary left fold (… op pack)
binary right fold (pack op … op init)
binary left fold (init op … op pack)
其中pack代表变参,比如args,op代表操作符,fold expression支持32种操作符:
引用
1 |
+ - * / % ^ & | = > += -= *= /= %= ^= &= |= >= == != = && || , .* ->* |
unary right fold的含义
fold (E op …) 意味着 E1 op (… op (EN-1 op EN)).
顾名思义,从右边开始fold,看它是left fold还是right fold我们可以根据参数包…所在的位置来判断,当参数包…在操作符右边的时候就是right fold,在左边的时候就是left fold。我们来看一个具体的例子:
1 2 3 4 5 6 |
template<typename... Args> 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。
1 2 3 4 5 6 |
template<typename... Args> auto add_val(Args&&... args) { return (... + args); } auto t = add_val(1,2,3,4); //10 |
对于不满足交换律的操作符来说就要注意了,比如减法。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
template<typename... Args> auto sub_val_right(Args&&... args) { return (args - ...); } template<typename... Args> 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的语义是相同的,看一个二元操作符的例子:
1 2 3 4 5 6 7 8 9 10 11 12 |
template<typename... Args> auto sub_one_left(Args&&... args) { return (1 - ... - args); } template<typename... Args> 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来将变参一个个传入一个函数。比如像下面这个例子:
1 2 3 4 5 6 7 8 9 10 11 12 |
template<typename T> void print_arg(T t) { std::cout << t << std::endl; } template<typename... Args> void print2(Args... args) { //int a[] = { (printarg(args), 0)... }; std::initializer_list<int>{(print_arg(args), 0)...}; } |
这种写法比较繁琐,用fold expression就会变得很简单了。
1 2 3 4 5 |
template<typename... Args> void print3(Args... args) { (print_arg(args), ...); } |
这是right fold,你也可以写成left fold,对于comma来说两种写法是一样的,参数都是从左至右传入print_arg函数。
1 2 3 4 5 |
template<typename... Args> void print3(Args... args) { (..., print_arg(args)); } |
你也可以通过binary fold这样写:
1 2 3 4 |
template<typename ...Args> void printer(Args&&... args) { (std::cout << ... << args) << '\n'; } |
也许你会觉得能写成这样:
1 2 3 4 |
template<typename ...Args> void printer(Args&&... args) { (std::cout << args << ...) << '\n'; } |
但这样写是不合法的,根据binary fold的语法,参数包…必须在操作符中间,因此上面的这种写法不符合语法要求。
借助comma fold我们可以简化代码,假如我们希望实现tuple的for_each算法,像这样:
1 |
for_each(std::make_tuple(2.5, 10, 'a'),[](auto e) { std::cout << e<< '\n'; }); |
这个for_each将会遍历tuple的元素并打印出来。在C++17之前我们如果要实现这个算法的话,需要借助逗号表达式和std::initializer_list来实现,类似于这样:
1 2 3 4 |
template <typename... Args, typename Func, std::size_t... Idx> void for_each(const std::tuple& t, Func&& f, std::index_sequence<Idx...>) { (void)std::initializer_list<int> { (f(std::get<Idx>(t)), void(), 0)...}; } |
这样写比较繁琐不直观,现在借助fold expression我们可以简化代码了。
1 2 3 4 |
template <typename... Args, typename Func, std::size_t... Idx> void for_each(const std::tuple<Args...>& t, Func&& f, std::index_sequence<Idx...>) { (f(std::get<Idx>(t)), ...); } |
借助coma fold我们可以写很简洁的代码了。