可变参数模板

六月 20, 2022 [c++, 可变参数模板] #c++ #可变参数模板

可变参数模板

可变参数模板

一个可变参数模板就是:一个接受可变数目参数的模板函数或模板类

可变数目的参数被称为参数包。存在两种参数包: 模板参数包:表示零个或多个模板参数 函数参数包:表示零个或多个函数参数

语法格式: 用一个省略号来指出一个模板参数或函数参数表示一个包 在模板参数列表中:class...或typename...指出接下来的参数表示零个或多个类型的列表;一个类型名后面跟一个省略号表示零个多多个给定类型的非类型参数的列表 在函数参数列表中:如果一个参数的类型是一个模板参数包,则此参数也是一个函数参数包

模板参数列表中:声明一个名为T的类型参数,和一个名为Args的模板参数包(这个包表示零个或多个额外的类型参数) 函数参数列表中:声明一个const&类型的参数,指向T的类型,还包含一个名为reset的函数参数包(这个包表示零个或多个函数参数) 登录后复制 //Args是一个模板参数包;rest是一个函数参数包 //Args表示零个或多个模板类型参数 //rest表示零个或多个函数参数

template<typename T,typename... Args> void foo(const T &t, const Args& ... reset) { //... }

sizeof...运算符

当我们想要知道包中有多少元素时,可以使用sizeof...运算符,该运算符返回一个常量表达式,并且不会对其实参求值 下面是一个演示案例:

template<typename... Args>
void g(Args... args) 
{
    std::cout << sizeof...(Args) << std::endl; //类型参数的数目
    std::cout << sizeof...(args) << std::endl; //函数参数的数目
}

int main()
{
    int i = 0;
    double d = 3.14;
    string s = "how now brown cow";

    g(i, s, 42, d);
    g(s, 42, "hi");
    g(d, s);
    g("hi");

    return 0;
}

编写可变参数函数模板

普通函数的可变形参,可以使用一个initializer_list来定义一个可接受可变数目实参的函数。但是,initializer_list实参必须具有相同的类型(或者它们的类型可以庄园为同一类型) 当我们既不知道想要处理的实参的数目也不知道它们的类型时,可变参数函数是很有用的

可变参数模板的递归调用

下面我们先定义一个print模板函数,它在给定一个流上打印给定实参列表的内容: //使用参数1的输出流,打印参数2的内容(也用来结束下面的print函数)

template<typename T>
ostream &print(ostream &os, const T &t)
{
    return os << t;
}

现在我们定义一个可变参数模板函数,也名为print。它用来递归调用print函数,注意: 每次递归调用print的时候都会将Args参数的数量递减然后传递给print()函数 当递归到最后Args参数只剩一个的时候就会调用上面两个参数的print()函数(注意(重点):上面的print函数必须定义,否则下面的print函数就会永远递归下去,不能够结束)

template<typename T, typename... Args>
ostream &print(ostream &os, const T &t, const Args&... rest)
{
    os << t << ", ";              //打印第一个实参
    return print(os, rest...);    //然后递归调用print
}

假设我们调用下面的print语句,那么递归会依次执行下面的结果:

int i = 10; int s = 20; print(std::cout, i, s, 42);

包扩展

对于一个参数包,除了获取其大小外,我们能对它做的唯一事情就是扩展它。当扩展一个包时,我们还要提供用于每个扩展元素的模式。扩展一个包就是把它分解为构成的元素,对每个元素引用模式 我们通过在模式右边放一个省略号(...)来触发扩展操作。例如刚才我们在上面定义的print函数:. 第一个扩展操作:扩展模板参数包,为print生成函数参数列表 第二个扩展操作:出现在对print的调用中,目的是为print调用生成实参列表

template<typename T, typename... Args>
ostream &print(ostream &os, const T &t, const Args&... rest) //扩展Args
{
    os << t << ", ";
    return print(os, rest...);    //扩展Args
}

对Args的扩展中,编译器将模式const Args&应用到模板参数包Args中的每个元素。因此,此模式的扩展结果是一个逗号分隔的零个或多个类型的列表,每个类型都形如const type&,例如:

print(std::cout, i, s, 42); //包中有两个参数

最后两个实参的类型和模式一起确定了位置参数的类型。此调用被实例化为:

print(ostream&, const int&, const string&, const int&);

第二个扩展发生在对print的递归调用中。再次情况下,模式时函数参数包的名字(即rest)。此模式扩展处一个由包中元素组成的、逗号分隔的列表。因此,这个调用等价于

print(std::cout, s, 42);
print(ostream&, const string&, const int&);

理解包扩展

上面的print函数参数包扩展仅仅将包扩展为其构成元素,C++语言还允许更复杂的扩展模式 例如,我们编写第二个可变参数函数errorMsg,在其内部使用print函数,然后每个实参调用debug_rep()函数打印结果:

template<typename T>
string debug_rep(const T &t)
{
    ostringstream ret;
    ret << t;
    return ret.str();
}

template<typename... Args>
ostream &errorMsg(ostream &os, const Args&... rest)
{
    //print(os,debug_rep(a1),debug_rep(a2),...,debug_rep(an))
    return print(os, debug_rep(rset)...);
}

上面的print调用使用了模式debug_rep(rset)。此模式表示我们希望对函数参数包rest中的每个元素调用debug_rep 但是,下面的形式是错误的: 如果将rset...传递给debuf_rep()。代表debug_rep是一个可变参数模板,但是debug_rep不是 正确的做法是每次将一个rset参数传递给debug_rep

template<typename... Args>
ostream &errorMsg(ostream &os, const Args&... rest)
{
    return print(os, debug_rep(rset...)); //错误的
}

转发参数包

在C++11标准下,我们可以组合使用可变参数模板与forword机制来编写函数,实现将其实参不变地传递给其他函数

作为例子,我们将StrVec类添加一个emplace_back成员,标准库容器的emplace_back成员是一个可变参数成员模板,它用其实参在容器管理的内存空间中直接构造一个元素 我们为StrVec设计的emplace_back版本也应该是可变参数的,因为string有多个构造函数,参数各不相同。由于我们希望能使用string的移动构造函数,因此还需要保持传递给emplace_back的实参的所有类型信息 因此StrVec类的emplace_back成员定义如下: 模板参数包扩展的模式是&&,意味着每个函数参数将是一个指向其对应实参的右值引用

class StrVec {
public:
    template<class... Args>
    void emplace_back(Args&&...);
};

其次,当emplace_back将这些实参传递给construct时,必须使用forward来保持实参的原始类型:

template<class... Args>
void StrVec::emplace_back(Args&&... args)
{
    chk_n_alloc(); //判断空间是否充足
    alloc.construct(first_free++,std::forward<Args>(args)...)
}

construct在first_free指向的位置中创建一个元素。construct调用中的扩展为: 其中Ti表示模板参数包中第i个元素的类型,ti表示函数参数包中第i个元素

std::forward<Ti>(ti);

如果我们有下面的调用:

StrVec svec;
svec.emplace_back(10, 'c'); //将cccccccccc添加为新的尾元素

construct调用中的模式会扩展出:

std::forward<int>(10),std::forward<char>(c);

通过在此调用中使用forward,我们保证如果一个右值调用emplace_back,则construct也会得到一个右值。例如,在下面的调用中:

svec.emplace_back(s1+s2); //使用移动构造函数

传递给emplace_back的实参是一个右值,它将以如下形式传递给construct:

std::forward<string>(string("the end"));

forward<stirng>的结果类型是string&&,因此construct将得到一个右值引用实参,construct会继续将此实参传递给string的移动构造函数来创建新元素 

折叠表达式

C++17中引入了折叠表达式,主要是方便模板编程,分为左右折叠,下图为其解包形式

template <typename... Args> auto sub_right(Args... args) {
  return (args - ...);
}

template <typename... Args> auto sub_left(Args... args) { return (... - args); }

template <typename... Args> auto sum_right(Args... args) {
  return (args + ...);
}

int main() {
  std::cout << sub_right(8, 4, 2) << std::endl; // (8 - (4 - 2)) = 6
  std::cout << sub_left(8, 4, 2) << std::endl;  // ((8 - 4) - 2) = 2

  std::cout << sum_right(std::string{"hello "}, std::string{"world"})
            << std::endl; // hello world

  return 0;
}