可变参数模板是否会导致代码膨胀?

7

可变参数模板将使某些函数能够重写为更加清晰、类型安全的版本。以 printf 为例,就像在 维基百科 上给出的示例一样:

void printf(const char *s)
{
    while (*s) {
        if (*s == '%' && *(++s) != '%')
            throw std::runtime_error("invalid format string: missing arguments");
        std::cout << *s++;
    }
}

template<typename T, typename... Args>
void printf(const char *s, T value, Args... args)
{
    while (*s) {
        if (*s == '%' && *(++s) != '%') {
            std::cout << value;
            ++s;
            printf(s, args...); // call even when *s == 0 to detect extra arguments
            return;
        }
        std::cout << *s++;
    }
    throw std::logic_error("extra arguments provided to printf");
}

但是,就我所了解的模板而言,它们意味着每种类型组合都需要重复编写代码。因此上述printf的可变参数版本将被复制多次。对于大型函数或类,这可能会很糟糕。

可变参数模板是否像标准模板那样危险,容易造成代码重复? 如果是,继承技巧是否仍然适用?


当然,你一开始就不应该写“大函数”,而是应该选择小的函数。 - Martin Ba
1
你基本上会得到与使用模板相同的膨胀。如果你为两个不同的事物实例化一个模板,那么你就会得到两个副本。对于模板和可变参数模板,优化器可以内联函数,这可能会再次增加大小(或减小它)。 - bames53
2个回答

11
简短回答是:仍然适用“按使用量付费”的原则。
通过比较两个假设实现的生成代码,可以看到更详细的答案。
#include <iostream>

template <typename T>
void func1(T& v) {
  v = -10;
}

template <typename T1, typename T2>
void func1(T1& v1, T2& v2) {
  func1(v1); func1(v2);
}

// More unused overloads....
template <typename T1, typename T2, typename T3>
void func1(T1& v1, T2& v2, T3& v3) {
  func1(v1); func1(v2); func1(v3);
}

int main() {
  double d;
  int i;
  func1(d);
  func1(i);
  std::cout << "i=" << i << ", d=" << d << std::endl;
  func1(d,i);
  std::cout << "i=" << i << ", d=" << d << std::endl;
}

使用现代编译器,这基本上就等同于你想要避免使用模板时所编写的代码。在这个“传统”的 C++03 模板化代码中,我的 g++ 版本会将整个代码都内联(在编译器而不是关键字意义上),并且没有明显的迹象表明初始化是通过引用在模板函数中进行的,以不同的方式多次执行。

与等效的可变参数方法相比:

#include <iostream>
#include <functional>

void func1() {
  // end recursion
}

template <typename T, typename ...Args>
void func1(T& v, Args&... args) {
  v = -10;
  func1(args...);
}

int main() {
  double d;
  int i;
  func1(d);
  func1(i);
  std::cout << "i=" << i << ", d=" << d << std::endl;
  func1(d,i);
  std::cout << "i=" << i << ", d=" << d << std::endl;
}

这也会生成几乎相同的代码-一些标签和混淆的名称不同,但是通过 g ++ -Wall-Wextra-S (4.7快照)生成的汇编代码的差异没有显着差异。编译器基本上正在动态地编写程序所需的所有重载,然后像以前一样进行优化。以下非模板代码也产生几乎相同的输出:
#include <iostream>
#include <functional>

int main() {
  double d;
  int i;
  d= -10; i=-10;
  std::cout << "i=" << i << ", d=" << d << std::endl;
  d= -10; i=-10;
  std::cout << "i=" << i << ", d=" << d << std::endl;
}

在这里,唯一显著的区别是标签和符号名称。
关键是现代编译器可以在模板代码中轻松地执行“正确”的操作。如果你所表达的内容在所有模板机制下都很简单,那么输出也将是简单的。如果不是,那么输出将更加实质化,但如果完全避免使用模板,则输出也将如此。
然而,这个有趣的地方(在我的看法中)在于这一点:所有我的陈述都带有类似“使用一个良好的现代编译器”的限定语。如果你正在编写可变参数模板,几乎可以肯定你所用的编译器是一个良好的现代编译器。没有笨重的旧式编译器支持可变参数模板。

6

这可能是一个问题。其中一件有帮助的事情是将共同部分因式分解:

const char *process(const char *s)
{
  while (*s) {
      if (*s == '%' && *(++s) != '%') {
          ++s;
          return s;
      }
      std::cout << *s++;
  }
  throw std::logic_error("extra arguments provided to printf");
}

template<typename T>
inline const char *process(const char *s,T value)
{
  s = process(s);
  std::cout << value;
  return s;
}

template<typename T, typename... Args>
inline void printf(const char *s, T value, Args... args)
{
  printf(process(s,value),args...);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接