从C 17开始,可以使用二元操作符对形参包中的参数进行计算,这一特性主要针对可变参数模板进行提升。支持的二元操作符多达32个。例如,下面的函数将会返回传入的所有的参数的和。
代码语言:javascript复制template <typename ...T>
auto Sum(T ...args)
{
return (... args);
}
在上面的代码中,return后的语句折叠表达式的一部分,它被称为左折叠。形如下面的调用方式后,函数会返回所有参数的和值。
代码语言:javascript复制int main()
{
cout<<Sum(1,3,4)<<endl;
return 0;
}
运行后上面的代码将会输出传入参数的和值:8.实际上,Sum展开时,折叠表达式展开后的形式为:((1 3) 4)。
同样,如果将Sum函数进行改写,重新运行后虽然结果相同,但是折叠表达式展开后各个参数的组合就发生了变化。如下代码所示:
代码语言:javascript复制template <typename ...T>
auto Sum(T ...args)
{
return (args ...);
}
重新运行后,函数输出依然是:8.但是折叠表达式展开后的形式为:(1 (3 4))。由此可见,折叠表达式里的参数位置直接对后续折叠表达式的展开起着决定性作用。
1 折叠表达式缘起
折叠表达式对编程的直接影响为:在使用递归进行实例化函数参数模板的场景中可以直接使用折叠表达式,使用后代码更加清晰也更加简便。如下面代码:
代码语言:javascript复制template<typename T>
auto sum_s(T arg) {
return arg;
}
template<typename T1, typename... Ts>
auto sum_c(T1 arg1, Ts... otherArgs) {
return arg1 sum_s(otherArgs...);
}
有了折叠表达式后,可以直接写成:
代码语言:javascript复制template <typename ...T>
auto sum_c(T ...args)
{
return (args ...);
}
2 使用折叠表达式
在上面的例子中,给定一个参数和一个操作符后不管是左折叠还是右折叠都能够将折叠表达式展开为下面的形状:
左折叠:((arg1 op arg2) op arg3) op … 右折叠:arg1 op (arg2 op … (argN1 op argN))
前面的代码我们是对数值型数据进行求和,如果要在字符传进行 运算会不会产生预期的结果呢?如下所示:
代码语言:javascript复制cout<<sum_c(std::string("hello"),"world","!")<<endl;
运行后,上面的代码会输出什么结果呢?这里先卖个关子,思考一个问题:两个字符串相加。如下表示:
代码语言:javascript复制int main()
{
cout<<"hello" "world"<<endl;
cout<<std::string("hello") "world"<<endl;
cout<<std::string("hello") std::string("world")<<endl;
return 0;
}
上面的代码很简单,第一个cout里面的语句编译时就会报错,因为两个字符面量相加是非法运算的。
在回到上面的例子中,如果对sum_c中的折叠表达式进行调用,当前面传入两个字符面量时,编译器会报错。如果想要编译正常,只需要按如下方式调用即可:
代码语言:javascript复制cout<<sum_c("hello","world",std::string("!"))<<endl;
如上所述,在实际编写代码时,当传入参数的顺序发生变化时,左折叠或者右折叠也会产生不同的编译结果。
在实际使用时,我们更推荐使用左折叠。因为这个更加符合大众的思维。
2.1 处理空包参数
折叠表达式处理空参数包将会遵循如下规则: • 如果使用了 && 运算符,值为 true。 • 如果使用了 || 运算符,值为 false。 • 如果使用了逗号运算符,值为 void()。 • 使用所有其他的运算符,都会引发格式错误 对于其他的情况,可以添加一个初始值:给定一个参数包 args,一个初始值 value,一个操作符 op。如下面的书写方式:
代码语言:javascript复制template<typename ... T>
double sum_left(T ... arg)
{
return ((8*2) ... arg);//左折叠
}
如果调用sum_left传入参数为空,将会返回8*2的值。既:16;需要注意的是在省略号的两边,数据类型需要保持一致。
对于二元的折叠表达式,我们可以按照上面的方法进行编写,从实际编程角度来说也更加推荐使用左折叠的方式。不妨考虑一下,一元表达式是怎么处理的呢?实际上,对一元表达式使用折叠时需要注意参数的顺序,不同的顺序输出的结果可能是不同的,如下面的表达式所示:
代码语言:javascript复制template<typename... T>
void print(const T&... args)
{
std::cout <<(args<<...<< 'n');
std::cout<<std::endl;
std::cout<<"......"<<std::endl;
std::cout<<(...<<args)<<'n';
}
int main()
{
print(1);
return 0;
}
运行代码上,代码输出结果如下:
代码语言:javascript复制1024
......
1
在main函数中,调用print输出1,在print中,使用std::cout输出参数,不同的是在cout里面args参数的顺序是不一样的,从运行结果看,代码输出了两个完全不同的结果。
出乎我们意外的是下面这句代码输出了1024。
代码语言:javascript复制 std::cout <<(args<<...<< 'n');//print(1)输出1024
这个1024是怎么产生的呢?实际上这段代码在打印时打印的值是1左移'n'之后的值,'n'的asc码值为10,所以最后输出的是1<<10位,即1024。第7行代码运行后则输出了我们期望的值,即:1.
2.2 支持的运算符
在C 中,除了以下二元运算符,所有的二元操作符都可以使用折叠表达式。如下所示:.、 ->、 []。
折叠函数的调用
折叠表达式可以使用逗号运算符,这样就可以在一行调用多个函数。如下面的代码:
代码语言:javascript复制template<typename T>
void print(const T t)
{
std::cout<<t<<'n';
}
template<typename... Types>
void callPrint(const Types&... args)
{
(...,print(args));
}
int main()
{
callPrint(1,2,4);
return 0;
}
//函数输出结果为:1,2,4
在callprint中,通过使用折叠表达式,可以根据传入参数的个数多次调用函数。从而对函数进行输出。
再继续讨论下,如何在callprint中使用移动语义,上面的callprint可以修改成如下代码:
代码语言:javascript复制template<typename... Types>
void callPrint(Types&&... args)
{
(...,print(std::forward<Types>(args)));
}
运行后,代码输出的结果和之前保持一致。在此需要明确一点的是对于逗号运算符不管是左折叠还是右折叠输出的结果都是一样的。函数的执行顺序都是从左向右。
上面是将折叠应用在函数中,下面将讨论将折叠使用在类中,作为类的基类进行调用。
折叠基类的函数调用 敲黑板了,折叠使用的场景越来越复杂了,不过也可以给我们的编码带来便利,将其应用在基类中可以调用具有可变参数的基类成员函数。如下面的代码所示:
代码语言:javascript复制template<typename... Bases>
class MultiBase : private Bases...
{
public:
void print()
{
(... , Bases::print());
}
};
class A {
public:
void print() { std::cout << "A::print()"<<std::endl; }
};
class B {
public:
void print() { std::cout << "B::print()"<<std::endl; }
};
int main()
{
MultiBase<A,B> mB;
mB.print();//输出结果为:A::print() B::print()
return 0;
}
在上面的代码中通过定义mB,然后再通过mB调用print()触发每个派生类中print函数的调用。
除此之外,折叠还可以使用在其它复杂的场景,比如:二叉树的遍历、哈希等场景中,欢迎大家留言评论实现方式。被选中的话可以获得我们提供的奖品哦。
2.3 使用折叠处理类型
通过使用类型特征,可以判断类或者函数中传入的参数类型是否相同。实现方式如下:
代码语言:javascript复制template<typename T1, typename... TN>
constexpr bool isSameType(T1, TN...)
{
return (std::is_same_v<T1, TN> && ...);
}
int main()
{
std::cout<<boolalpha<<isSameType(2,4,3.0)<<std::endl;
return 0;
}
上面的代码输出结果为:false,同理,如果传入参数为:isSameType(2,4,3)输出结果将为:true。
在类中也一样,将上面的代码封装成类,封装后代码如下所示:
代码语言:javascript复制template<typename T1, typename... TN>
struct C
{
static constexpr bool bValue = (std::is_same_v<T1, TN> && ...);
};
int main()
{
std::cout<<boolalpha<<C<int,float,decltype(80)>::bValue<<std::endl;
return 0;
}
上面的代码展开后将会变成下面的表达式:
代码语言:javascript复制std::is_same_v<int, float> && std::is_same_v<int, decltype(42)>
运行后代码输出为false。
3 后记
对于表达式,很难用这去取几千字来详细说明,本文权且当做抛砖引玉,如果大家有什么建议欢迎大家留言评论一起讨论。