🗓️ 2025-06-05 🎖️ 快速上手 "modern c++" 🗂️ C++笔记 🏷️ #C++

参数包与折叠表达式傻傻分不清

开始接触可变参数的时候,...的位置总是整得我晕头转向,特别是一下几个例子

以下都是错误写法
  1. 不知道可变参数类型怎么声明 template<typename... Args> void foo(Args args...){}
  2. 不知到参数包、类型包应该怎么传递 std::forward<Args...>(args...)
  3. 折叠表达式和参数包展开傻傻分不清 (std::cout << "[prefix]" << Val)...

希望看完本文,能够让你明白上面几处错在哪里,正确的写法应该是什么样子的

参数包

作用说明

...在参数包中,大致上有两种作用:

  1. 声明一个包(出现在类型或者参数名左边,表示这是一个包)
    • template<typename... Args>,声明了 Args 是一个类型的包
    • template<auto... Ns> 声明了 Ns 是一个非类型参数包
    • void foo(Args... args) 声明了 args 是一个函数参数包
  2. 展开一个包(出现在参数包名右边,表示展开这个包为一个逗号分割的元素列表)
    • 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...)这个错误写法上老是晕。

所以,我用自己的办法来对...做出精简的理解:(但是不一定是精准,如果你发现这么想有什么致命错误,还望不吝赐教🫰

Note

在处理参数包的场景中,...的作用就是展开它左边的东西。是的,只关心左边

  1. <typename... Args>:左边是typename,说明是把 typename 展开成了多个,也就是说声明了多个 typename,那不就是类型包
  2. void foo(Args&&... args){}:左边是类型Args,说明把Args展开了,那就是定义了多个arg参数,用一个args来表示它们
  3. bar(args...);:左边是args,说明把args展开了,这是一个参数包,就是展成了一个一个的参数,用逗号分隔,传给了bar函数
  4. 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...> ✅ 是 自然接受逗号分隔类型
独立表达式 函数体内的表达式 ❌ 否 需要特殊处理

所以上面的coutforward的区别,其实是因为它们所处的位置不同:

造成迷惑的,好像为什么这个只能这样用,其实也是因为,forward大多都用于完美转发参数,它确实就是常出现在参数列表,而一般没人写一堆cout去传参吧。实际上cout当然可以按照forward那样写:

Success
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函数

Comment