Namespaces
Variants

Pack (since C++11)

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Template specialization
Parameter packs (C++11)
Miscellaneous

形参包是定义以下内容之一的C++实体:

  • 参数包
  • 模板参数包
  • 函数参数包
(自 C++20 起)
(自 C++26 起)

模板参数包是一种可接受零个或多个模板实参(常量、类型或模板)的模板参数。函数参数包是一种可接受零个或多个函数实参的函数参数。

lambda 初始化捕获包是一种 lambda 捕获,它会为其初始化器中的包展开的每个元素引入一个初始化捕获。

(since C++20)

结构化绑定包是结构化绑定声明中的标识符,用于引入零个或多个结构化绑定。

(since C++26)

包的元素数量等于:

  • 参数包提供的参数数量,如果该包是模板参数包或函数参数包,
  • 若该包是 lambda 初始化捕获包,则其初始化器包展开中的元素数量,
(since C++20)
  • 结构化绑定的初始化器大小减去结构化绑定声明中非包元素的数量,如果该包是结构化绑定包。
(since C++26)

具有至少一个参数包的模板被称为 可变参数模板

目录

语法

模板参数包(出现于 别名模板 类模板 变量模板 (C++14 起) 概念 (C++20 起) 函数模板 参数列表中)

类型 ... 包名  (可选) (1)
typename | class ... 包名  (可选) (2)
类型约束 ... 包名  (可选) (3) (自 C++20 起)
template < 参数列表 > class ... 包名  (可选) (4) (直至 C++17)
template < 参数列表 > typename | class ... 包名  (可选) (4) (自 C++17 起)

函数形参包(一种 声明符 形式,出现在可变参数函数模板的函数形参列表中)

包名 ... 包参数名  (可选) (5)

关于非参数包的语法,请参阅 lambda 初始化捕获包 结构化绑定包 (C++26 起)

(C++20 起)

包展开(出现在模板主体中)

模式 ... (6)
1) 带有可选名称的常量模板参数包
2) 带有可选名称的类型模板参数包
3) 带有可选名称的 受约束 类型模板参数包
(since C++20)
4) 带有可选名称的模板模板参数包
5) 带有可选名称的函数参数包
6) 包展开:展开为零个或多个 pattern 的列表。该模式必须包含至少一个包。

说明

可变参数类模板可以使用任意数量的模板参数进行实例化:

template<class... Types>
struct Tuple {};
Tuple<> t0;           // Types 不包含任何参数
Tuple<int> t1;        // Types 包含一个参数:int
Tuple<int, float> t2; // Types 包含两个参数:int 和 float
Tuple<0> t3;          // 错误:0 不是类型

可变参数函数模板可以使用任意数量的函数实参进行调用(模板参数通过 模板实参推导 进行推导):

template<class... Types>
void f(Types... args);
f();       // 正确:args 不包含任何参数
f(1);      // 正确:args 包含一个参数:int
f(2, 1.0); // 正确:args 包含两个参数:int 和 double

在主要类模板中,模板参数包必须是模板参数列表中的最终参数。在函数模板中,模板参数包可以出现在列表的较前位置,前提是所有后续参数可以从函数实参推导得出,或具有默认参数:

template<typename U, typename... Ts>    // 正确:可推导 U
struct valid;
// template<typename... Ts, typename U> // 错误:Ts... 不在末尾
// struct Invalid;
template<typename... Ts, typename U, typename=void>
void valid(U, Ts...);    // 正确:可推导 U
// void valid(Ts..., U); // 无法使用:在此位置 Ts... 属于不可推导上下文
valid(1.0, 1, 2, 3);     // 正确:将 U 推导为 double,Ts 推导为 {int, int, int}

如果变参模板的每个有效特化都需要一个空的模板参数包,则程序非良构,不要求诊断。

包展开

后跟省略号的模式(其中至少一个包名称至少出现一次)会被 展开 为零个或多个模式实例化,其中包名称按顺序被包中的每个元素替换。 对齐说明符 的实例化以空格分隔,其他实例化以逗号分隔。

template<class... Us>
void f(Us... pargs) {}
template<class... Ts>
void g(Ts... args)
{
    f(&args...); // “&args...” 是包展开
                 // “&args” 是其模式
}
g(1, 0.2, "a"); // Ts... args 展开为 int E1, double E2, const char* E3
                // &args... 展开为 &E1, &E2, &E3
                // Us... pargs 展开为 int* E1, double* E2, const char** E3

如果两个包的名字出现在同一模式中,它们会同时展开,并且必须具有相同的长度:

template<typename...>
struct Tuple {};
template<typename T1, typename T2>
struct Pair {};
template<class... Args1>
struct zip
{
    template<class... Args2>
    struct with
    {
        typedef Tuple<Pair<Args1, Args2>...> type;
        // Pair<Args1, Args2>... 是包展开
        // Pair<Args1, Args2> 是模式
    };
};
typedef zip<short, int>::with<unsigned short, unsigned>::type T1;
// Pair<Args1, Args2>... 展开为
// Pair<short, unsigned short>, Pair<int, unsigned int> 
// T1 是 Tuple<Pair<short, unsigned short>, Pair<int, unsigned>>
// typedef zip<short>::with<unsigned short, unsigned>::type T2;
// 错误:包展开包含长度不同的参数包

如果包展开嵌套在另一个包展开内部,出现在最内层包展开内部的包会由它展开,并且必须在外层包展开中提及另一个包,但该包不能出现在最内层包展开中:

template<class... Args>
void g(Args... args)
{
    f(const_cast<const Args*>(&args)...); 
    // const_cast<const Args*>(&args) 是展开模式,它会同时展开两个参数包
    // (Args 和 args)
    f(h(args...) + args...); // 嵌套包展开:
    // 内层包展开 "args..." 首先被展开
    // 外层包展开 h(E1, E2, E3) + args... 随后被展开
    // (展开为 h(E1, E2, E3) + E1, h(E1, E2, E3) + E2, h(E1, E2, E3) + E3)
}

当包中的元素数量为零(空包)时,包展开的实例化不会改变外围结构的语法解释,即使在完全省略包展开会导致格式错误或产生语法歧义的情况下也是如此。该实例化会产生一个空列表。

template<class... Bases> 
struct X : Bases... { };
template<class... Args> 
void f(Args... args) 
{
    X<Args...> x(args...);
}
template void f<>(); // 正确:X<> 没有基类
                     // x 是类型为 X<> 的变量,进行值初始化

展开位置

根据展开发生的位置,生成的逗号分隔(或对于 对齐说明符 为空格分隔)列表会形成不同类型的列表:函数参数列表、成员初始化列表、属性列表等。以下是所有允许的上下文列表:

函数参数列表

包展开可以出现在函数调用运算符的括号内,此时省略号左侧的最大表达式或 花括号初始化列表 即为被展开的模式:

f(args...);              // 展开为 f(E1, E2, E3)
f(&args...);             // 展开为 f(&E1, &E2, &E3)
f(n, ++args...);         // 展开为 f(n, ++E1, ++E2, ++E3);
f(++args..., n);         // 展开为 f(++E1, ++E2, ++E3, n);
f(const_cast<const Args*>(&args)...);
// f(const_cast<const E1*>(&X1), const_cast<const E2*>(&X2), const_cast<const E3*>(&X3))
f(h(args...) + args...); // 展开为 
// f(h(E1, E2, E3) + E1, h(E1, E2, E3) + E2, h(E1, E2, E3) + E3)

括号初始化器

包展开可以出现在 直接初始化 的圆括号内、 函数式转型 以及其他上下文( 成员初始化器 new表达式 等)中,此时规则与上述函数调用表达式的规则相同:

Class c1(&args...);             // 调用 Class::Class(&E1, &E2, &E3)
Class c2 = Class(n, ++args...); // 调用 Class::Class(n, ++E1, ++E2, ++E3);
::new((void *)p) U(std::forward<Args>(args)...) // std::allocator::allocate

花括号初始化器

在花括号包围的初始化列表中,包展开同样可以出现:

template<typename... Ts>
void func(Ts... args)
{
    const int size = sizeof...(args) + 2;
    int res[size] = {1, args..., 2};
    // 由于初始化列表保证顺序性,这可用于按顺序对参数包中的每个元素调用函数:
    int dummy[sizeof...(Ts)] = {(std::cout << args, 0)...};
}

模板实参列表

包展开可用于模板实参列表中的任意位置,前提是模板具有与展开相匹配的参数:

template<class A, class B, class... C>
void func(A arg1, B arg2, C... arg3)
{
    container<A, B, C...> t1; // 展开为 container<A, B, E1, E2, E3> 
    container<C..., A, B> t2; // 展开为 container<E1, E2, E3, A, B> 
    container<A, C..., B> t3; // 展开为 container<A, E1, E2, E3, B> 
}

函数参数列表

在函数参数列表中,若省略号出现在参数声明中(无论其是否命名函数参数包,例如 Args ... args ),该参数声明即为模式:

template<typename... Ts>
void f(Ts...) {}
f('a', 1); // Ts... 展开为 void f(char, int)
f(0.1);    // Ts... 展开为 void f(double)
template<typename... Ts, int... N>
void g(Ts (&...arr)[N]) {}
int n[1];
g<const char, int>("a", n); // Ts (&...arr)[N] 展开为 
                            // const char (&)[2], int(&)[1]

注意:在模式 Ts (&...arr)[N] 中,省略号是最内层元素,而非像其他所有包展开中那样作为末尾元素。

注意: Ts (&...)[N] 是不允许的,因为 C++11 语法要求带括号的省略号必须具有名称: CWG issue 1488

模板参数列表

包展开可以出现在模板参数列表中:

template<typename... T>
struct value_holder
{
    template<T... Values> // 扩展为常量模板参数
    struct apply {};      // 列表,例如 <int, char, int(&)[5]>
};

基类说明符与成员初始化器列表

包展开可以指定 类声明 中的基类列表。通常,这也意味着构造函数需要在 成员初始化列表 中使用包展开来调用这些基类的构造函数:

template<class... Mixins>
class X : public Mixins...
{
public:
    X(const Mixins&... mixins) : Mixins(mixins)... {}
};

Lambda 捕获

包展开可以出现在 lambda 表达式的捕获子句中:

template<class... Args>
void f(Args... args)
{
    auto lm = [&, args...] { return g(args...); };
    lm();
}

sizeof... 运算符

sizeof... 运算符同样被归类为包展开:

template<class... Types>
struct count
{
    static const std::size_t value = sizeof...(Types);
};

动态异常规范

动态异常规范 中的异常列表也可以是包展开:

template<class... X>
void func(int arg) throw(X...)
{
    // ... throw different Xs in different situations
}
(直到 C++17)

对齐说明符

包展开允许用于关键字 alignas 所用的类型列表和表达式列表。其实例化结果以空格分隔:

template<class... T>
struct Align
{
    alignas(T...) unsigned char buffer[128];
};
Align<int, short> a; // 展开后的对齐说明符为
                     // alignas(int) alignas(short)
                     // (中间没有逗号)

属性列表

如果属性规范允许, 属性 列表中允许使用包展开。例如:

template<int... args>
[[vendor::attr(args)...]] void* f();

折叠表达式

折叠表达式 中,模式是指不包含未展开包的整体子表达式。

using声明

using声明 中,省略号可以出现在声明符列表中,这在从模板参数包派生时特别有用:

template<typename... bases>
struct X : bases...
{
    using bases::g...;
};
X<B, D> x; // OK: B::g and D::g introduced
(since C++17)


包索引

包索引 中,包展开包含一个未展开的包后接省略号和下标。包索引表达式的模式是 标识符 ,而包索引说明符的模式是 类型定义名

consteval auto first_plus_last(auto... args)
{
    return args...[0] + args...[sizeof...(args) - 1];
}
static_assert(first_plus_last(5) == 10);
static_assert(first_plus_last(5, 4) == 9);
static_assert(first_plus_last(5, 6, 2) == 7);

友元声明

在类 友元声明 中,每个类型说明符后可以跟随省略号:

struct C {};
struct E { struct Nested; };
template<class... Ts>
class R
{
    friend Ts...;
};
template<class... Ts, class... Us>
class R<R<Ts...>, R<Us...>>
{
    friend Ts::Nested..., Us...;
};
R<C, E> rce;           // classes C and E are friends of R<C, E>
R<R<E>, R<C, int>> rr; // E::Nested and C are friends of R<R<E>, R<C, int>>

折叠展开约束

折叠展开约束 中,模式是该折叠展开约束的约束条件。

折叠展开约束不会被实例化。

(自 C++26 起)

注释

功能测试 标准 功能特性
__cpp_variadic_templates 200704L (C++11) 可变参数模板
__cpp_pack_indexing 202311L (C++26) 参数包索引

示例

以下示例定义了一个类似于 std::printf 的函数,该函数将格式字符串中每个出现的 % 字符替换为一个值。

第一个重载在仅传递格式字符串且没有参数展开时被调用。

第二个重载包含一个独立的模板参数用于参数头部和一个参数包,这使得递归调用能够仅传递参数的尾部直至其变为空。

Targs 是模板参数包,而 Fargs 是函数参数包。

#include <iostream>
void tprintf(const char* format) // 基础函数
{
    std::cout << format;
}
template<typename T, typename... Targs>
void tprintf(const char* format, T value, Targs... Fargs) // 递归可变参数函数
{
    for (; *format != '\0'; format++)
    {
        if (*format == '%')
        {
            std::cout << value;
            tprintf(format + 1, Fargs...); // 递归调用
            return;
        }
        std::cout << *format;
    }
}
int main()
{
    tprintf("% world% %\n", "Hello", '!', 123);
}

输出:

Hello world! 123

缺陷报告

以下行为变更缺陷报告被追溯应用于先前发布的C++标准。

缺陷报告 适用标准 发布时行为 正确行为
CWG 1533 C++11 成员初始化器中允许出现包展开 不允许
CWG 2717 C++11 对齐说明符的实例化采用逗号分隔 采用空格分隔

参见

函数模板 定义函数族
类模板 定义类族
sizeof... 查询参数包中的元素数量
C风格可变参数函数 接受可变数量的参数
预处理器宏 同样支持可变参数
折叠表达式 通过二元运算符规约参数包
参数包索引 访问参数包中指定索引的元素