命名空间
变体
操作

参数包 (自 C++11 起)

来自 cppreference.com
< cpp‎ | 语言
 
 
C++ 语言
 
 
 
 

模板参数包是一个模板参数,可以接受零个或多个模板参数(非类型、类型或模板)。函数参数包是一个函数参数,可以接受零个或多个函数参数。

至少包含一个参数包的模板称为可变参数模板

内容

[edit] 语法

模板参数包(出现在别名模板类模板, 变量模板(自 C++14 起)函数模板参数列表中)

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

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

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

参数包展开(出现在可变参数模板的函数体中)

模式 ... (6)
1) 带有可选名称的非类型模板参数包
2) 带有可选名称的类型模板参数包
3) 带有可选名称的受约束的类型模板参数包
(自 C++20 起)
4) 带有可选名称的模板模板参数包
5) 带有可选名称的函数参数包
6) 参数包展开:展开为零个或多个用逗号分隔的 模式 列表。模式必须至少包含一个参数包。

[edit] 解释

可变参数类模板可以实例化为任意数量的模板参数

template<class... Types>
struct Tuple {};
 
Tuple<> t0;           // Types contains no arguments
Tuple<int> t1;        // Types contains one argument: int
Tuple<int, float> t2; // Types contains two arguments: int and float
Tuple<0> t3;          // error: 0 is not a type

可变参数函数模板可以调用任意数量的函数参数(模板参数通过模板参数推断推断)

template<class... Types>
void f(Types... args);
 
f();       // OK: args contains no arguments
f(1);      // OK: args contains one argument: int
f(2, 1.0); // OK: args contains two arguments: int and double

在主类模板中,模板参数包必须是模板参数列表中的最后一个参数。在函数模板中,模板参数包可以出现在列表中的更早位置,前提是所有后续参数都可以从函数参数推断出来,或者具有默认参数

template<typename U, typename... Ts>    // OK: can deduce U
struct valid;
// template<typename... Ts, typename U> // Error: Ts... not at the end
// struct Invalid;
 
template<typename... Ts, typename U, typename=void>
void valid(U, Ts...);    // OK: can deduce U
// void valid(Ts..., U); // Can't be used: Ts... is a non-deduced context in this position
 
valid(1.0, 1, 2, 3);     // OK: deduces U as double, Ts as {int, int, int}

如果可变参数模板的所有有效特化都需要一个空模板参数包,则程序格式错误,不需要诊断。

[edit] 包展开

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

template<class... Us>
void f(Us... pargs) {}
 
template<class... Ts>
void g(Ts... args)
{
    f(&args...); // “&args...” is a pack expansion
                 // “&args” is its pattern
}
 
g(1, 0.2, "a"); // Ts... args expand to int E1, double E2, const char* E3
                // &args... expands to &E1, &E2, &E3
                // Us... pargs expand to 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>... is the pack expansion
        // Pair<Args1, Args2> is the pattern
    };
};
 
typedef zip<short, int>::with<unsigned short, unsigned>::type T1;
// Pair<Args1, Args2>... expands to
// Pair<short, unsigned short>, Pair<int, unsigned int> 
// T1 is Tuple<Pair<short, unsigned short>, Pair<int, unsigned>>
 
// typedef zip<short>::with<unsigned short, unsigned>::type T2;
// error: pack expansion contains parameter packs of different lengths

如果包展开嵌套在另一个包展开中,则出现在最内层包展开中的参数包由它展开,并且必须在包含包展开中提及另一个包,但在最内层包展开中没有提及

template<class... Args>
void g(Args... args)
{
    f(const_cast<const Args*>(&args)...); 
    // const_cast<const Args*>(&args) is the pattern, it expands two packs
    // (Args and args) simultaneously
 
    f(h(args...) + args...); // Nested pack expansion:
    // inner pack expansion is "args...", it is expanded first
    // outer pack expansion is h(E1, E2, E3) + args..., it is expanded
    // second (as h(E1, E2, E3) + E1, h(E1, E2, E3) + E2, h(E1, E2, E3) + E3)
}

[edit] 展开位置

展开发生的位置取决于结果用逗号分隔(或对于 对齐说明符,用空格分隔)的列表是什么类型的列表:函数参数列表、成员初始化列表、属性列表等。以下是所有允许上下文的列表

[edit] 函数参数列表

包展开可以出现在函数调用运算符的括号内,在这种情况下,省略号左侧最大的表达式或大括号封闭的初始化器列表是展开的模式

f(args...);              // expands to f(E1, E2, E3)
f(&args...);             // expands to f(&E1, &E2, &E3)
f(n, ++args...);         // expands to f(n, ++E1, ++E2, ++E3);
f(++args..., n);         // expands to 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...); // expands to 
// f(h(E1, E2, E3) + E1, h(E1, E2, E3) + E2, h(E1, E2, E3) + E3)

[edit] 带括号的初始化器

包展开可以出现在直接初始化函数式转换和其他上下文的括号内(成员初始化new 表达式 等),在这种情况下,规则与上面的函数调用表达式的规则相同

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

[edit] 大括号封闭的初始化器

在大括号封闭的初始化器列表中,也可以出现包展开

template<typename... Ts>
void func(Ts... args)
{
    const int size = sizeof...(args) + 2;
    int res[size] = {1, args..., 2};
 
    // since initializer lists guarantee sequencing, this can be used to
    // call a function on each element of a pack, in order:
    int dummy[sizeof...(Ts)] = {(std::cout << args, 0)...};
}

[edit] 模板参数列表

包展开可以在模板参数列表中的任何位置使用,只要模板具有与展开匹配的参数即可

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

[edit] 函数参数列表

在函数参数列表中,如果在参数声明中出现省略号(无论它是否命名函数参数包(例如,Args... args)),则参数声明就是模式

template<typename... Ts>
void f(Ts...) {}
 
f('a', 1); // Ts... expands to void f(char, int)
f(0.1);    // Ts... expands to 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] expands to 
                            // const char (&)[2], int(&)[1]

注意:在模式 Ts (&...arr)[N] 中,省略号是最内层的元素,而不是像其他所有包展开那样是最后一个元素。

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

[edit] 模板参数列表

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

template<typename... T>
struct value_holder
{
    template<T... Values> // expands to a non-type template parameter 
    struct apply {};      // list, such as <int, char, int(&)[5]>
};

[edit] 基类说明符和成员初始化列表

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

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

[edit] lambda 捕获

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

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

[edit] 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)

[edit] 对齐说明符

包展开在关键字 alignas 使用的类型列表和表达式列表中都是允许的。实例化用空格分隔

template<class... T>
struct Align
{
    alignas(T...) unsigned char buffer[128];
};
 
Align<int, short> a; // the alignment specifiers after expansion are
                     // alignas(int) alignas(short)
                     // (no comma in between)

[edit] 属性列表

包展开在属性列表中是允许的,如果属性的规范允许。例如

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

折叠表达式

折叠表达式中,模式是不包含未展开参数包的整个子表达式。

using 声明

使用声明中,省略号可以出现在声明符列表中,这在从参数包派生时很有用。

template<typename... bases>
struct X : bases...
{
    using bases::g...;
};
X<B, D> x; // OK: B::g and D::g introduced
(自 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);

友元声明

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

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>
 
struct E { struct Nested; };
 
R<R<E>, R<C, int>> rr; // E::Nested and C are friends of R<R<E>, R<C, int>>

折叠展开约束

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

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

(自 C++26 起)

[编辑] 备注

功能测试宏 Std 功能
__cpp_variadic_templates 200704L (C++11) 可变参数模板

[编辑] 示例

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

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

第二个重载包含一个单独的模板参数用于参数头和一个参数包,这允许递归调用仅传递参数尾部直到它为空。

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

#include <iostream>
 
void tprintf(const char* format) // base function
{
    std::cout << format;
}
 
template<typename T, typename... Targs>
void tprintf(const char* format, T value, Targs... Fargs) // recursive variadic function
{
    for (; *format != '\0'; format++)
    {
        if (*format == '%')
        {
            std::cout << value;
            tprintf(format + 1, Fargs...); // recursive call
            return;
        }
        std::cout << *format;
    }
}
 
int main()
{
    tprintf("% world% %\n", "Hello", '!', 123);
}

输出

Hello world! 123

[编辑] 缺陷报告

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

DR 应用于 发布时的行为 正确行为
CWG 1533 C++11 包展开可以出现在成员的成员初始化器中 不允许
CWG 2717 C++11 对齐说明符的实例化用逗号分隔 它们用空格分隔

[编辑] 参见

函数模板 定义函数族
类模板 定义类族
sizeof... 查询参数包中的元素数量
C 样式可变参数函数 接受可变数量的参数
预处理器宏 也可以是可变参数的
折叠表达式 在二元运算符上减少参数包
包索引 访问指定索引处的参数包元素