Являются ли вариативные шаблоны потенциальным раздуванием кода?

Шаблоны Variadic позволяют переписывать определенные виды функций в более четкие и безопасные версии. Это случай 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");
}

Но... Насколько я понимаю шаблоны, они подразумевают дублирование кода для каждой комбинации типов. Таким образом, вариационная версия вышеуказанных printfs будет копироваться много раз. Это может быть ужасно для больших функций или классов.

Являются ли вариативные шаблоны такими же опасными для дублирования кода, как и стандартные шаблоны? Если да, может ли еще помочь трюк с наследованием?


person Gabriel    schedule 10.11.2011    source источник
comment
Конечно, вы не должны начинать с написания больших функций. Отдавайте предпочтение маленьким.   -  person Martin Ba    schedule 10.11.2011
comment
Вы в основном получаете те же навороты, что и при использовании шаблонов. Если вы создадите экземпляр шаблона для двух разных вещей, вы получите две копии. И для шаблонов, и для вариативных шаблонов оптимизатор может встраивать вещи, которые могут снова увеличить размер (или уменьшить его, если на то пошло).   -  person bames53    schedule 10.11.2011


Ответы (2)


Краткий ответ таков: принцип «вы платите только за то, что используете» по-прежнему действует точно так же, как и раньше.

Более длинный ответ можно увидеть, сравнив сгенерированный код для двух гипотетических реализаций, например.

#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;
}

Это также создает почти идентичный код - некоторые метки и искаженные имена отличаются, как и следовало ожидать, но различия сгенерированного asm, созданного 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;
}

Здесь снова единственные заметные различия — это метки и имена символов.

Дело в том, что современный компилятор может делать "что правильно" без особых хлопот в коде шаблона. Если то, что вы выражаете, является простым под всей механикой шаблона, результат будет простым. Если это не так, результат будет более существенным, но таким же был бы результат, если бы вы полностью избегали шаблонов.

Однако это становится интересным (на мой взгляд), вот что: все мои утверждения были дополнены чем-то вроде «с приличным современным компилятором». Если вы пишете шаблоны с переменным числом аргументов, вы можете быть почти уверены, что то, что вы используете для компиляции, является приличным современным компилятором. Никакие старые неуклюжие реликтовые компиляторы не поддерживают шаблоны с переменным числом аргументов.

person Flexo    schedule 10.11.2011

Это, безусловно, может быть проблемой. Одна вещь, которая может помочь, - это выделить общие части:

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...);
}
person Vaughn Cato    schedule 10.11.2011