参数包与折叠表达式傻傻分不清
开始接触可变参数的时候,...
的位置总是整得我晕头转向,特别是一下几个例子
- 不知道可变参数类型怎么声明
template<typename... Args> void foo(Args args...){}
- 不知到参数包、类型包应该怎么传递
std::forward<Args...>(args...)
- 折叠表达式和参数包展开傻傻分不清
(std::cout << "[prefix]" << Val)...
希望看完本文,能够让你明白上面几处错在哪里,正确的写法应该是什么样子的
参数包
作用说明
...
在参数包中,大致上有两种作用:
- 声明一个包(出现在类型或者参数名的左边,表示这是一个包)
template<typename... Args>
,声明了Args
是一个类型的包template<auto... Ns>
声明了Ns
是一个非类型参数包void foo(Args... args)
声明了args
是一个函数参数包
- 展开一个包(出现在参数包名的右边,表示展开这个包为一个逗号分割的元素列表)
foo(args...)
表示把args
这个参数包,展开成一个个逗号分隔的参数,也就是类似foo(arg_0, arg_1, arg_2)
foo(std::forward<Arg>(arg)...)
把args
这个参数包,展开为一个个的forward包起来的样子进行传参,即foo(forward<Arg_1>(arg_1), forward<Arg_2>(arg2), forward<Arg_3>(arg_3))
个人记忆方式
上面说了一堆,但是用的时候还是容易搞混。单独的声明和展开的场景很好区分,但是我本人就是在std::forward<Args...>(args...)
这个错误写法上老是晕。
所以,我用自己的办法来对...
做出精简的理解:(但是不一定是精准,如果你发现这么想有什么致命错误,还望不吝赐教🫰
在处理参数包的场景中,...
的作用就是展开它左边的东西。是的,只关心左边
<typename... Args>
:左边是typename
,说明是把 typename 展开成了多个,也就是说声明了多个 typename,那不就是类型包void foo(Args&&... args){}
:左边是类型Args
,说明把Args展开了,那就是定义了多个arg参数,用一个args来表示它们bar(args...);
:左边是args
,说明把args展开了,这是一个参数包,就是展成了一个一个的参数,用逗号分隔,传给了bar函数std::forward<Arg>(args)...
:左边是一个forward
完美转发参数,说明把args
展开了,而且是展开成完美转发的样子,用逗号分隔
核心就是只关心...
左边的东西就好了,不管左边是啥,你就按照展开它的方式去理解。是typename展开了就是声明类型包,是类型你展开了那就是声明了该类型的参数包,是参数包你展开了那肯定是用去传参了
折叠表达式
其实折叠表达式我不晕,比较好理解,直接去 Fold expressions (since C++17) - cppreference.com 看看就好了,在此不赘述
之所以在本文中提到,实际上是为了下一节,和参数包进行区分
二者区分的场景
首先定义一个接收可变参数的函数,它没什么用
template<typename... Args>
void foo(Args&&... args){}
然后考虑下面两组代码:
template<typename... Args>
void bar(Args&&... args){
foo(std::forward<Arg>(args)...); // ✅ 没问题,对每个参数都进行完美转发
((std::cout << "[prefix]:" << args), ...); // ✅ 也没问题,加上前缀打印每个参数
}
template<typename... Args>
void bar(Args&&... args){
// ❌ 错误!并不能像上面cout一样用折叠表达式,把forward用表达式展开成一堆逗号分隔的参数
foo((std::forward<Arg>(args), ...));
// ❌ 错误!这个也不会像上面forward一样用参数包展开,把cout展开成一堆cout分别输出参数
(std::cout << "[prefix]:" << args)...;
}
奇了怪了,为什么cout
就只能用折叠表达式,根据,
操作符进行展开;而forward
就只能用参数包展开,默认展开成逗号分割呢?难道forward不算表达式?
或许这里是我想的少了,也或许它确实是一个容易混淆的点,但答案肯定是背后另有原因。原因是:
上下文类型 | 示例 | 是否直接支持参数包 | 说明 |
---|---|---|---|
函数调用参数列表 | func(args...) |
✅ 是 | 自然接受逗号分隔参数 |
初始化列表 | {args...} |
✅ 是 | 自然接受逗号分隔值 |
模板参数列表 | Class<Args...> |
✅ 是 | 自然接受逗号分隔类型 |
独立表达式 | 函数体内的表达式 | ❌ 否 | 需要特殊处理 |
所以上面的cout
和forward
的区别,其实是因为它们所处的位置不同:
-
forward
处在函数调用参数列表中,可以当成参数包直接展开; -
而
cout
是一个函数内的独立表达式,所以不能当参数包展开。而折叠表达式的引入,不正是为了解决表达式处理参数包不方便的问题么?
造成迷惑的,好像为什么这个只能这样用,其实也是因为,forward大多都用于完美转发参数,它确实就是常出现在参数列表,而一般没人写一堆cout去传参吧。实际上cout当然可以按照forward那样写:
template<typename... Args>
void bar(Args&&... args){
// ✅ 没问题,直接把cout用参数包形式展开传给foo;此时foo的参数列表类型是一堆的ostream类型
foo((std::cout << "[prefix]:" << args)...);
foo(std::forward<Arg>(args)...); // 是不是跟forward一样了
}
深入代码进行验证
通过 C++ Insights 可以很方便地查看这些语法糖经过编译器处理后地真实地样子
考虑这份源码,它这些参数包展开、表达式折叠等处理后,实际上是什么样子呢?
#include<iostream>
#include<utility>
template<typename ... Args>
void foo(Args&&... args){}
template<typename ... Args>
void log(Args&&... args){
((std::cout << "[P]:"<<std::forward<Args>(args)),...);
}
template<typename ... Args>
void bar(Args&&... args){
log(std::forward<Args>(args)...);
foo((std::cout<<args)...);
}
int main(){
int a = 10;
int& b = a;
bar(a, b, 100);
}
实际上这样:
// ... 省略头文件和模板原始函数,直接看模板实例化出来的东西
// foo果然特化出了一个全是ostream的版本
// 所以即使是cout,在参数上下文中,直接被参数包展开也是没问题的
template<>
void foo<std::basic_ostream<char> &, std::basic_ostream<char> &, std::basic_ostream<char> &>
(std::basic_ostream<char> & __args0,
std::basic_ostream<char> & __args1,
std::basic_ostream<char> & __args2)
{}
// 这是逗号这个单目操作符,被右折叠表达式展开后的样子
template<>
void log<int &, int &, int>(int & __args0, int & __args1, int && __args2)
{
(std::operator<<(std::cout, "[P]:").operator<<(std::forward<int &>(__args0))) ,
( (std::operator<<(std::cout, "[P]:").operator<<(std::forward<int &>(__args1))) ,
(std::operator<<(std::cout, "[P]:").operator<<(std::forward<int>(__args2))) ) ;
}
// 这里是参数包展开的样子
template<>
void bar<int &, int &, int>(int & __args0, int & __args1, int && __args2)
{
log(
std::forward<int &>(__args0),
std::forward<int &>(__args1),
std::forward<int>(__args2)
);
foo(
(std::cout.operator<<(__args0)),
(std::cout.operator<<(__args1)),
(std::cout.operator<<(__args2))
);
}
// ... 省略main函数