【问题标题】:Variadic Templates example可变参数模板示例
【发布时间】:2016-09-14 20:50:12
【问题描述】:

考虑下面的代码,我不明白为什么必须定义 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");

}   

【问题讨论】:

  • 这是经典的递归,print() 是您的基本情况。

标签: c++ c++11


【解决方案1】:

正确的方式:

可变参数模板与 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 个函数。


错误 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()。但是,如果这样的函数不存在,它不会被调用,这就是为什么你应该提供那个空函数(别担心,编译器会优化代码,所以空函数不会降低性能)。

【讨论】:

  • ,为什么使用递归树?还有其他方法可以遍历可变参数模板的 Args 吗?
  • 不,没有其他办法,有一些工具可以做到这一点args[x] 但是这些工具也可以在自己的递归中使用,因此只会增加额外的编译时间。使用递归是因为编译器的工作原理,它替换代码,然后再次替换代码,直到出现错误,直到它替换所有内容或直到达到最大替换限制
  • 您的“错误 1”实际上完全没问题。部分排序正确地处理了这种情况。
  • @T.C.部分排序处理这种情况,所以你的权利,这绝对不是一个错误。但这不是一个好习惯:声明的顺序很重要,并且有用的代码在两个函数中都重复了。我们精确地使用函数来避免代码重复。所以,恕我直言,这是一个设计错误
【解决方案2】:

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

  1. print (a, b) => cout
  2. print (b) => cout

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

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

【讨论】:

  • variadic 表示至少一个参数?
  • @tobi303 不,可变参数并不意味着至少一个参数,但print(const T&amp; firstArg, const Types&amp;... args) 意味着至少一个参数。如果您写print(const Types&amp; ...args),则表示0个或多个参数
  • @tobi303 没有。但是看看 OP 的 print 的原型:它需要一个固定参数,然后是一个(可能是空的)可变参数包。
  • ups 抱歉,我忽略了 firstArg。声明一个 print 接受一个参数并以此停止“回避”是否也有效?
【解决方案3】:

因为(使用非常“简单”的解释)可变参数模板机制的工作原理类似于递归(它是不是递归,但这是理解的最简单方法它),它“消耗”可变参数列表,因此您必须定义一个“stop”函数,当“消耗”参数列表为“”。这是我发现这个非常复杂的概念最容易理解的解释。

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

然后在可变参数函数本身中慢慢处理可变参数参数,让您进入第二步(const char*, int, int, char, const char*)然后(int, int, char, const char*)等等......直到你到达最后一个元素(const char*),当这个也在下一步处理你最终得到(),编译器需要这个函数作为“终结者”

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

【讨论】:

  • 为什么说这不是递归呢?同一个函数不是随着参数数量的减少而递归调用的吗?
  • @tobi303 您正在调用print(),但没有任何模板参数。它们是自动推导出来的。当模板参数用完时会发生什么?
  • @tobi303 这不是同一个函数:在编译时,模板用于创建许多不同的函数。所以根据参数和类型的数量,编译器会创建尽可能多的函数
  • @Garf365 啊现在我明白了。我仍然称它为递归,但也许对这样的词要小心。
【解决方案4】:

递归是编程可变参数模板最通用的方法,但它远非唯一的方法。对于像这样的简单用例,直接在花括号初始化器列表中进行包扩展会更短,并且编译速度可能更快。

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) , ...);
}

【讨论】:

    【解决方案5】:

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

    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&lt;char [5]&gt;(char const (&amp;) [5])&gt;

    可以看到,当传递一个空的参数包时,模板参数列表一定是空的。因此它只调用print()。如果您明确指定模板参数,例如print&lt;T, Args...&gt;(t, args...),您将获得无限递归。

    【讨论】:

    • 这是编译器的输出吗?它欺骗我它重复每个重载(为什么前 2 行是相等的?)
    • @DarioOO 来自nm。它比objdump 更方便,后者会显示使用的实际符号。或者可以只使用gdb,但这太吵了,无法回答。
    • 好的,谢谢:)。不知道追踪符号的工具/技巧。很有价值
    猜你喜欢
    • 2011-06-13
    • 2012-06-04
    • 1970-01-01
    • 2016-12-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-09-14
    • 2012-03-28
    相关资源
    最近更新 更多