fold expression
- 虽然C++11引入了可变参数,但是没有引入对可变参数进行操作的语言支持。所以在C++17之前,我们只能使用旧的语言特性来解决。
- 我们常使用递归和初始化来解决可变参数的解包问题。
void print(){} //处理边界
template <class First, class ... Rest>
void print(First first_arg, Rest...rest_args){
cout << first_arg;
print(rest_args...); //将参数包传递
}
- 上面是经典的First-Rest技巧(据说是meyers大神起的名字。。。)
- 将一个参数包分解成1+N,然后处理那个1,递归处理N。
- 最终我们需要一个简单的版本来处理边界。
逗号表达式
template <class...Args>
void print(Args...args){
std::initializer_list<int>{((cout << args), 0)...};
}
- 这里也可以将initializer_list换成数组,或者lambda表达式。但是我最喜欢使用这个。
C++17 && later
template <class...Args>
void print(Args...args){
(cout << ... << args); //折叠表达式
}
- 上面就是折叠表达式的一种情况。
- 折叠表达式一共有4种形式,
- 其中I是参数包为空包时的默认初始值。
- 注意,小括号也是折叠表达式的一部分。
template <class ... Args>
auto u_minus_left(Args ... args) { //一元左折叠
return (... - args); //括号是折叠表达式的一部分 !!!
}
template <class ...Args>
auto u_minus_right(Args ... args) { //一元右折叠
return (args - ...); //需要括号!!
}
u_minus_left(1, 2, 3); // ((1 - 2) -3 ) == -4
u_minus_left(); // error !
u_minus_right(1, 2, 3); // (1 - (2 - 3)) == 2
u_minus_right(); //error !
- 可以看到左折叠和右折叠的解包方式是不同的。
- 一元折叠表达式不支持空包。
- 但是有3个运算符可以。 && , ||, 逗号表达式。
- &&空包时为true, || 为false, 逗号表达式为void()。
template <class ... Args>
auto b_minus_left(Args ... args) { //二元左折叠
return (0 - ... - args);
}
template <class ...Args>
auto b_minus_right(Args ... args) { //二元右折叠
return (args - ...- 0);
}
b_minus_left(1, 2, 3); // (((0 - 1) - 2) - 3) == -6
b_minus_left(); // 0 , ok
b_minus_right(1, 2, 3); // (1 - (2 - (3 - 0))) == 2
b_minus_right(); //0 ,ok
- 可以看到二元折叠表达式的结果可能和一元的完全不同!
- 二元折叠表达式有初始值,当为空包时,匹配初始值。
constexpr if
- 这也是我比较喜欢的C++17特性之一。
- 在上面我们的递归版本的print例子中,我们必须使用普通版本的print来结束递归。我们无法使用if-else语句来实现分流。
template<class First, class ... Rest>
void print(First first_arg, Rest...rest_args){
if(sizeof...(rest_args) == 0) //error !
cout << first_arg;
else
print(rest_args...);
}
print(3);
- 但是这不起作用,因为if-else是运行期的代码。
- 编译器尝试去实例化的时候先去实例化void print(),然后发现你去调用了void print(),编译器就又去寻找该函数,但是你没有该函数。编译器不会考虑if语句。
- 在C++17我们引入了if constexpr,编译器if表达式。
template<class First, class ... Rest>
void print(First first_arg, Rest...rest_args){
if constexpr(sizeof...(rest_args) == 0)
cout << first_arg;
else //必要的else !!!
print(rest_args...);
}
print(3); //ok
- if constexpr表达式的条件必须是编译期条件。
- 这里要注意的是,最好将所有函数语句都包进if constexpr-else语句,不要有另外的语句,否则可能引发错误。
template<class First, class ... Rest>
void print(First first_arg, Rest...rest_args){
if constexpr(sizeof...(rest_args) == 0)
cout << first_arg;
//没有else,危险!!
print(rest_args...); //这条语句独立于if constexpr之外!!
}
- 这样会导致出现这样的一个函数,
void print(First first_args, /*空包*/){
cout << first_arg;
print(); //这条语句独立于if constexpr之外!!
}
- 仍然导致递归失败。
if constexpr更多的作用:
- 我们使用if-constexpr可以实现一个函数拥有不同类型的返回值。
template <class T>
constexpr auto type_value() { //return decltype(T::value) or string
if constexpr (T::value)
return T::value;
else //不可缺少的else !否则return string就会被两个函数共享!!
return std::string("T::value == 0");
}
struct test_struct {
static constexpr int value = 1;
};
struct test_struct_string {
static constexpr int value = 0;
};
int main() {
cout << typeid(type_value<test_struct>()).name();
cout << typeid(type_value<test_struct_string>()).name();
return 0;
}
- 我们使用if constexpr实现了一个函数多个返回值。
- type_value()在编译期实际上会被分成两个函数,
/*
template <class T>
constexpr auto type_value() {
return T::value;
}
template <class T>
constexpr auto type_value() {
return std::string("T::value == 0");
}
*/
- 然后根据if constexpr的条件来决定调用哪个函数。
- 如果if constexpr的条件为false,那么该部分就会被丢弃,不会被实例化。 - 这里我们也可以看出else的不可缺少,否则两个函数都会有对string的return表达式。
more:
-
实际上,if constexpr还有更多有用的地方。
-
这与type_traits关系匪浅。为什么我们会有type_traits?因为我们想要在编译期间知道更多的信息,好用来帮助我们进行代码分流。
-
我使用经典的advance来举栗子。
-
advance函数,接受一个迭代器iterator,一个整数n,表示将该迭代器前进(也有可能后退,取决于n的正负)n个长度。
-
但是,不同的迭代器,前进的方式不同。我想对不同的迭代器采取不同的应对方式,这样就能增加效率。
template <class InputIterator, class Distance>
void advance (InputIterator& it, Distance n){
typename std::iteraotr_traits<InputIterator>::iterator_catagory c;
_advance(it, n, c);
}
template <class InputIterator, class Distance>
void _advance(InputIterator& it, Distance n, random_access_iterator_tag){
it += n;
}
template <class InputIterator, class Distance>
void _advance(InputIterator& it, Distance n, bidirectional_iterator_tag){
if(n > 0){
while(n-- != 0)
++it;
}
else{
while(n++ != 0)
--it;
}
}
template <class InputIterator, class Distance>
void _advance(InputIterator& it, Distance n, forward_iterator_tag){
while(n-- != 0){
++it;
}
}
- 我们的手法就是对iterator进行分类,利用函数重载来解决不同的迭代器类型。
- 但是我们有了if constexpr后,就可以更简单的实现。
template <class InputIterator, class Distance>
void advance (InputIterator& it, Distance n){
using t = typename iterator_tratis<InputIterator>::iterator_catagory;
if constexpr(std::is_same_v<t, random_access_iterator_tag>){
it += n;
}
else if constexpr(std::is_same_v<t, bidirectional_iterator_tag>){
if(n > 0){ //双向迭代器
while(n-- != 0)
++it;
}
else{
while(n++ != 0)
--it;
}
}
else{ //单向迭代器
while(n-- != 0){
++it;
}
}
}
- VS2019下的advnace正是使用的if constexpr重写了,有兴趣的可以查看。