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

#c #c 11 #variadic-templates

#c #c 11 #variadic-templates

Вопрос:

Вариативные шаблоны позволят переписать определенные функции в более чистые, типобезопасные версии. Это относится к printf , как пример, приведенный в Википедии:

 void printf(const char *s)
{
    while (*s) {
        if (*s == '%' amp;amp; *(  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 == '%' amp;amp; *(  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 шаблонов будут скопированы много раз. Это может быть ужасно для больших функций или классов.

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

Комментарии:

1. Конечно, вам не следует с самого начала писать «большие функции». Предпочитайте небольшие шаблоны.

2. По сути, вы получаете то же раздувание, что и при использовании шаблонов. Если вы создаете экземпляр шаблона для двух разных вещей, то получаете две копии. И для шаблонов, и для вариационных шаблонов оптимизатор может встроить что-то, что может снова увеличить размер (или уменьшить его, если на то пошло).

Ответ №1:

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

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

 #include <iostream>

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

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

// More unused overloads....
template <typename T1, typename T2, typename T3>
void func1(T1amp; v1, T2amp; v2, T3amp; 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(Tamp; v, Argsamp;... 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;
}
  

И здесь единственными заметными отличиями являются метки и названия символов.

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

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

Ответ №2:

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

 const char *process(const char *s)
{
  while (*s) {
      if (*s == '%' amp;amp; *(  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...);
}