BLOG

Record, summarize, and improve.

C++可变参数

省略号和可变参数模板

本文介绍了如何将省略号 (...) 与 C++ 可变参数模板一起使用。 省略号在 C 和 C++ 中有多种用法。 其中包括函数的变量参数列表。 C 运行时库中的 printf() 函数是最著名的示例之一。

“可变参数模板”是支持任意数量的参数的类或函数模板。 此机制对 C++ 库开发人员特别有用:你可以将其应用于类模板和函数模板,从而提供广泛的类型安全和非简单功能和灵活性。

  • 可变参数模板(variadic template)为一个接受可变数目参数的模板函数或模板类。
  • 参数包(parameter packet)可变数目的参数。
  • 模板参数包(template parameter packet)表示零个或多个模板参数。
  • 函数参数包(function parameter packet)表示零个或多个函数参数。

语法

可变参数模板以两种方式使用省略号。 在参数名称的左侧,表示“参数包”,在参数名称的右侧,将参数包扩展为单独的名称。

可变类模板

下面是可变类模板定义语法的基本示例:

template<typename... Arguments> class classname;

对于参数包和扩展,可以根据偏好在省略号周围添加空格,如以下示例所示:

template<typename ...Arguments> class classname;

或以下示例:

template<typename ... Arguments> class classname;

本文使用第一个示例中显示的约定, (省略号附加到 typename) 。

在前面的示例中, Arguments 是一个参数包。 类 classname 可以接受可变数量的参数,如以下示例所示:

template<typename... Arguments> class vtclass;

vtclass< > vtinstance1;
vtclass<int> vtinstance2;
vtclass<float, bool> vtinstance3;
vtclass<long, std::vector<int>, std::string> vtinstance4;

通过使用可变类模板定义,还可以至少需要一个参数:

template <typename First, typename... Rest> class classname;
可变函数模板

下面是可变函数模板语法的基本示例:

template <typename... Arguments> returntype functionname(Arguments... args);

然后,参数 Arguments 包将展开以供使用,如下一部分所示。

可以采用其他类型的可变函数模板语法,包括但不限于以下示例:

template <typename... Arguments> returntype functionname(Arguments&... args);
template <typename... Arguments> returntype functionname(Arguments&&... args);
template <typename... Arguments> returntype functionname(Arguments*... args);

也允许使用说明符(如 const):

template <typename... Arguments> returntype functionname(const Arguments&... args);

与可变参数模板类定义一样,可以生成要求至少一个参数的函数:

template <typename First, typename... Rest> returntype functionname(const First& first, const Rest&... args);

可变参数模板使用 sizeof...() 运算符(与旧的 sizeof() 运算符无关):

template<typename... Arguments>
void tfunc(const Arguments&... args)
{
    constexpr auto numargs{ sizeof...(Arguments) };

    X xobj[numargs]; // array of some previously defined type X

    helper_func(xobj, args...);
}

有关省略号位置的更多信息

此前,本文介绍以以下形式定义参数包和扩展的省略号放置:“在参数名称左侧,它表示参数包,在参数名称右侧,将参数包展开为单独的名称”。 虽然从技术上讲如此,但在翻译到代码时可能会让人困惑。 请注意以下几点:

  • 在模板参数列表 (template <parameter-list>) 中,typename... 引入了模板参数包
  • 在参数声明子句 (func(parameter-list)) 中,“顶级”省略号引入了函数参数包,并且省略号定位很重要:
    // v1 is NOT a function parameter pack:
    template <typename... Types> void func1(std::vector<Types...> v1);
    
    // v2 IS a function parameter pack:
    template <typename... Types> void func2(std::vector<Types>... v2);
  • 其中省略号显示在参数名称的后面,并且您具有一个参数包扩展。

示例

说明可变函数模板机制的一个好方法是在重写某些功能的 printf重写中使用它:

#include <iostream>using namespace std;

void print() {
    cout << endl;
}

template <typename T> void print(const T& t) {
    cout << t << endl;
}

template <typename First, typename... Rest> void print(const First& first, const Rest&... rest) {
    cout << first << ", ";
    print(rest...); // recursive call using pack expansion syntax
}

int main()
{
    print(); // calls first overload, outputting only a newline
    print(1); // calls second overload

    // these call the third overload, the variadic template,
    // which uses recursion as needed.
    print(10, 20);
    print(100, 200, 300);
    print("first", 2, "third", 3.14159);
}

输出

1
10, 20
100, 200, 300
first, 2, third, 3.14159

可变参数模板通常是递归的,因为你虽然传入了多个可变参数,但有正因为参数是可变的,你不知道如何在一个函数内去处理所有的参数,所以只能通过递归的方式,将参数包里面的参数一个一个来处理。为了终止递归,还需要定义一个非可变参数的函数模板

模板参数包是一个模板参数,它接受零或多个模板参数(非类型、类型或模板)。函数参数包是一个函数参数,它接受零个或多个函数参数。具有至少一个参数包的模板称为variadic template.

参数包Syntax

模板参数包

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

type...pack-name(optional) (1)
typename|class...pack-name(optional) (2)
type-constraint...pack-name(optional) (3) (since C++20)
template<parameter-list>class...pack-name(optional) (4) (until C++17)
template<parameter-list>typename|class...pack-name(optional) (4) (since C++17)

功能参数包

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

pack-name...pack-param-name(optional) (5)

参数包扩展

参数包扩展(出现在变量模板的主体中)。

pattern... (6)

1)非类型模板参数包,名称可选。

2)类型模板参数包,名称可选。

3)具有可选名称的约束类型模板参数包,(since C++20)

4)一个模板参数包,名称可选

5)一个带有可选名称的函数参数包

6) 参数包扩展:扩展为逗号分隔的零个或多个 pattern 的列表。模式必须至少包含一个参数包。

Explanation

一个变量类模板可以用任意数量的模板参数来实例化。

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

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

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

在主类模板中,模板参数包必须是模板参数列表中的最后一个参数。在函数模板中,模板参数包可以出现在列表的前面,前提是所有下面的参数都可以从函数参数中推导出来,或者有默认参数。

template<typename U, typename... Ts>    // OK:可以推断出U
struct valid;
// template<typename... Ts, typename U> // 错误:Ts... 不在末尾
// 结构无效;
 
template<typename... Ts, typename U, typename=void>
void valid(U, Ts...);    // OK:可以推断出U
// void valid(Ts ...,U); //不能使用:Ts ...是此位置的非推论上下文
 
valid(1.0, 1, 2, 3);     // OK:推导出 U 为 double,Ts 为 {int, int, int}

如果一个变量模板的每一个有效的特殊化都需要一个空的模板参数包,那么程序是不合格的,不需要进行诊断。

Pack expansion

一个带省略号的模式,其中至少有一个参数包的名称至少出现一次,就是expanded变成零个或多个以逗号分隔的模式实例,其中参数包的名称被包中的每个元素依次替换。

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
                //我们... 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 ...
    // 秒 (as h(E1, E2, E3) + E1, h(E1, E2, E3) + E2, h(E1, E2, E3) + E3)
}

Expansion loci

根据扩展发生的位置,产生的逗号分隔的列表是不同类型的列表:函数参数列表、成员初始化器列表、属性列表等。下面是所有允许的上下文的列表。

函数参数列表

一个包的扩展可能出现在一个函数调用操作符的括号内,在这种情况下,省略号左边最大的表达式或括号内的init-list就是被扩展的模式。

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)

Formally,the expression-list在函数调用表达式中被归为initializer-list,而模式是initializer-clause,要么是assignment-expressionor abraced-init-list.

Parenthesized initializers

包扩展可能出现在直接初始化程序函数样式强制转换和其他上下文(成员初始化程序新表达式等)的括号内,在这种情况下,规则与上述函数调用表达式的规则相同:

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
Brace-enclosed initializers

In abraced-init-list(用大括号括起来的初始化程序列表和其他braced-init-list,用于列表初始化和其他一些上下文),包扩展也可能会出现:

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; // 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> 
}
功能参数表

在函数参数列表中,如果在参数声明中出现省略号(无论是否命名为函数参数包(如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] ,省略号是最里面的元素,而不是所有其他pack扩展中的最后一个元素。

注意:不允许使用 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 captures

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

template<class... Args>
void f(Args... args)
{
    auto lm = [&, args...] { return g(args...); };
    lm();
}
运营商的大小...

sizeof... 运算符也被归类为包扩展:

template<class... Types>
struct count
{
    static const std::size_t value = sizeof...(Types);
};
动态异常规格
动态异常规范中的异常列表也可以是包扩展:

template<class... X>
void func(int arg) throw(X...)
{
// ...在不同情况下抛出不同的X
}
(until C++17)
Alignment specifier

alignas 使用的表达式列表中都允许进行包扩展。

Attribute list

在 [[attributes...]] 中,属性列表中允许扩展包。例如: void [[attributes...]] function() 。

Fold-expressions
fold-expressions中,模式是不包含未扩展参数包的整个子表达式。
Using-declarations
使用声明时,省略号可能出现在声明符列表中,这在从参数包派生时非常有用:

template<typename... bases>
struct X : bases...
{
using bases::g...;
};
X<B, D> x; //确定:引入了B :: g和D :: g
(since C++17)
Notes
Example

下面的示例定义了一个类似于 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);
}

Output:

Hello world! 123

Defect reports

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

DR Applied to 公布的行为 Correct behavior
CWG 1533 C++11 在一个成员的初始化程序中,可能会出现一个包的扩展。 not allowed