Variadic模板示例

Variadic Templates example

本文关键字:Variadic      更新时间:2023-10-16

考虑下面的代码,我不明白为什么必须定义print的空函数。

#include <iostream>
using namespace std;
void print()
{   
}   
    template <typename T, typename... Types>
void print (const T& firstArg, const Types&... args)
{   
    cout << firstArg << endl; // print first argument
    print(args...); // call print() for remaining arguments
}
int main()
{   
    int i=1;
    int  j=2;
    char c = 'T';
    print(i,"hello",j,i,c,"word");
}   

正确方式:

变分模板与induction(一个数学概念)严格相关。

编译器解析以下函数调用

print('a', 3, 4.0f);

进入

std::cout<< 'a' <<std::endl;
print(3, 4.0f);

它被分解为

std::cout<< 'a' <<std::endl;
std::cout<< 3 <<std::endl;
print( 4.0f);

它被分解为

std::cout<< 'a' <<std::endl;
std::cout<< 3 <<std::endl;
std::cout<< 4.0f <<std::endl;
print();

在这一点上,它搜索匹配为空函数的函数重载。

  • 所有具有1个或多个参数的函数都与可变参数模板匹配
  • 所有没有参数的函数都与空函数匹配

罪魁祸首是,对于每个可能的参数组合,必须只有一个函数。


错误1:

执行以下操作将是一个错误

template< typename T>
void print( const T& arg) // first version
{   
    cout<< arg<<endl;
}   
template <typename T, typename... Types>
void print (const T& firstArg, const Types&... args) // second version
{   
    cout << firstArg << endl; // print first argument
    print(args...); // call print() for remaining arguments
}

因为当您调用print时,编译器不知道该调用哪个函数。

print(3)是指"第一个"版本还是"第二个"版本?两者都是有效的,因为第一个有1个参数,第二个也可以接受一个参数。

print(3); // error, ambiguous, which one you want to call, the 1st or the 2nd?

错误2:

以下无论如何都会是一个错误

// No empty function
template <typename T, typename... Types>
void print (const T& firstArg, const Types&... args) 
{   
    cout << firstArg << endl; // print first argument
    print(args...); // call print() for remaining arguments
}

事实上,如果你在没有编译器的情况下单独使用它,就会进行

 print('k', 0, 6.5);

它被分解为

 std::cout<<'k'<<std::endl;
 print(0, 6.5);

它被分解为

 std::cout<<'k'<<std::endl;
 std::cout<< 0 <<std::endl;
 print( 6.5);

它被分解为

 std::cout<<'k'<<std::endl;
 std::cout<< 0 <<std::endl;
 std::cout<< 6.5 <<std::endl;
 print(); //Oops error, no function 'print' to call with no arguments

正如您在上一次尝试中看到的那样,编译器尝试在没有参数的情况下调用print()。然而,如果这样的函数不存在,它就不会被调用,这就是为什么你应该提供空函数的原因(别担心,编译器会优化代码,这样空函数就不会降低性能)。

如果没有空的print函数,想象一个有两个参数的调用:

  1. print(a,b)=>cout<lt;a<lt;endl和call print(b)
  2. print(b)=>cout<lt;b<lt;endl并调用print()

oups,print()不存在,因为只有至少有一个参数的print存在!所以你需要一个没有参数的print

没有任何参数的print是您的最终调用

因为(使用非常"简单"的解释级别)可变模板机制的工作方式类似于递归(它是NOT递推,但这是理解它的最简单方法),它"消耗"可变参数列表,因此您必须定义一个"stop"函数,当"consumped"参数列表为""时,它将在递归的最后一步中重复出现。这是我发现最容易理解这个相当复杂的概念的解释。

在第一步(在main中),您将获得一个具有参数的函数:(int, const char*, int, int, char, const char*)

然后变差参数在变差函数本身中慢慢处理,让您在第二步中使用(const char*, int, int, char, const char*),然后使用(int, int, char, const char*),依此类推。。。直到你到达最后一个元素(const char*),当它在下一步中也被处理时,你会得到(),编译器需要这个函数作为"终止符"

(是的,这是非常非技术性的,听起来像青蛙爷爷给小青蛙讲故事…)

递归是对可变模板进行编程的最常用方法,但它远不是唯一的方法。对于像这样的简单用例,直接在支持的初始值设定项列表中进行包扩展会更短,而且编译起来可能更快。

template <typename... Types>
void print (const Types&... args)
{   
    using expander = int[];
    (void) expander { 0, (void(cout << args << endl), 0) ...};
}

在C++17中,我们将能够使用一个折叠表达式:

template <typename... Types>
void print (const Types&... args)
{   
    (void(cout << args << endl) , ...);
}

为了补充其他答案,我想展示编译器必须为模板调用生成什么。

nm -g -C ./a.out(非优化构建)给出:

void print<char [5]>(char const (&) [5])
void print<char [5]>(char const (&) [5])
void print<char [6], int, int, char, char [5]>(char const (&) [6], int const&, int const&, char const&, char const (&) [5])
void print<char [6], int, int, char, char [5]>(char const (&) [6], int const&, int const&, char const&, char const (&) [5])
void print<char, char [5]>(char const&, char const (&) [5])
void print<char, char [5]>(char const&, char const (&) [5])
void print<int, char [6], int, int, char, char [5]>(int const&, char const (&) [6], int const&, int const&, char const&, char const (&) [5])
void print<int, char, char [5]>(int const&, char const&, char const (&) [5])
void print<int, int, char, char [5]>(int const&, int const&, char const&, char const (&) [5])
void print<int, char [6], int, int, char, char [5]>(int const&, char const (&) [6], int const&, int const&, char const&, char const (&) [5])
void print<int, char, char [5]>(int const&, char const&, char const (&) [5])
void print<int, int, char, char [5]>(int const&, int const&, char const&, char const (&) [5])
print()

您可以看到print函数的所有实例化。最终调用print()的最后一个函数是void print<char [5]>(char const (&) [5])>

您可以看到,当传递一个空参数包时,模板参数列表必须为空。因此它只调用print()。如果显式指定模板参数,如print<T, Args...>(t, args...),则会得到无限递归。