Различия между специализацией шаблона и перегрузкой для функций? - PullRequest
21 голосов
/ 03 октября 2009

Итак, я знаю, что между этими двумя частями кода есть разница:

template <typename T>
T inc(const T& t)
{
    return t + 1;
}

template <>
int inc(const int& t)
{
    return t + 1;
}

и

template <typename T>
T inc(const T& t)
{
    return t + 1;
}

int inc(const int& t)
{
    return t + 1;
}

Я не понимаю, каковы функциональные различия между этими двумя. Может кто-нибудь показать какие-то ситуации, когда эти фрагменты отличаются друг от друга?

Ответы [ 5 ]

14 голосов
/ 03 октября 2009

Я могу думать только о нескольких различиях - вот несколько примеров, которые не обязательно причиняют вред (я думаю). Я опускаю определения для краткости

template <typename T> T inc(const T& t);
namespace G { using ::inc; }
template <> int inc(const int& t);
namespace G { void f() { G::inc(10); } } // uses explicit specialization

// --- against ---

template <typename T> T inc(const T& t);
namespace G { using ::inc; }
int inc(const int& t);
namespace G { void f() { G::inc(10); } } // uses template

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

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

template <typename T> void f(T t); // called for non-pointers
template <typename T> void f(T *t); // called for pointers.

int a;
void e() {
  f(a); // calls the non-pointer version
  f(&a); // calls the pointer version
}

Это было бы невозможно при явной специализации шаблона функции. Другой пример - когда задействованы ссылки, что приводит к выводу аргумента шаблона для поиска точного соответствия задействованных типов (по модулю базовых / производных отношений классов и константности):

template<typename T> void f(T const &);
template<> void f(int * const &);

template<typename T> void g(T const &);
void g(int * const &);

int a[5];
void e() {
  // calls the primary template, not the explicit specialization
  // because `T` is `int[5]`, not `int *`
  f(a);

  // calls the function, not the template, because the function is an
  // exact match too (pointer conversion isn't costly enough), and it's 
  // preferred. 
  g(a);
}

Я рекомендую вам всегда использовать перегрузку, потому что она богаче (допускает что-то вроде частичной специализации), и, кроме того, вы можете поместить функцию в любое пространство имен, которое вы хотите (хотя тогда это больше не будет строго перегружено). Например, вместо того, чтобы специализировать std::swap в пространстве имен std::, вы можете поместить перегрузку swap в свое собственное пространство имен и сделать ее вызываемой по ADL.

Что бы вы ни делали, никогда не смешивает специализацию и перегрузку , это будет адский беспорядок, подобный , на который указывает эта статья . Стандарт имеет прекрасный параграф об этом

Размещение явных объявлений специализации для шаблонов функций, шаблонов классов, функций-членов шаблонов классов, статических данных-членов шаблонов классов, классов-членов шаблонов классов, шаблонов классов-членов шаблонов классов, шаблонов функций-членов шаблонов классов, члена функции шаблонов-членов шаблонов классов, функции-члены шаблонов-членов не шаблонных классов, шаблоны функций-членов классов-членов шаблонов классов и т. д., а также размещение объявлений частичной специализации шаблонов классов, шаблонов классов-членов не-шаблона классы, шаблоны классов членов шаблонов классов и т. д. могут влиять на то, правильно ли сформирована программа в соответствии с относительным расположением явных объявлений специализации и их точек создания в единице перевода, как указано выше и ниже. При написании специализации будьте осторожны с ее местоположением; или сделать его компиляцией будет таким испытанием, чтобы разжечь его самосожжение.

5 голосов
/ 03 октября 2009

Специализация шаблона является более общей, чем просто перегрузка. Вы можете специализировать такие вещи, как классы, а не просто простые функции. Перегрузка относится только к функциям.

ОБНОВЛЕНИЕ: Чтобы пояснить больше по комментарию Арака, вы действительно сравниваете яблоки и апельсины здесь. Перегрузка функций используется для предоставления возможности иметь разные функции совместно использовать одно имя, если они имеют разные подписи. Специализация шаблона используется для определения фрагмента кода для определенного параметра типа. Вы не можете иметь специализацию шаблона, если у вас нет шаблона. Если вы удалите первый фрагмент кода, который объявляет универсальный шаблон, вы получите ошибку времени компиляции, если попытаетесь использовать специализацию шаблона.

Итак, цель специализации шаблона сильно отличается от перегрузки функции. Они просто ведут себя одинаково в вашем примере, хотя они принципиально отличаются.

Если вы предоставляете перегрузку, вы объявляете независимый метод с таким же именем. Вы не запрещаете использовать шаблон с параметром определенного типа. Чтобы продемонстрировать этот факт, попробуйте:

template <typename T>
T inc(const T& t)
{
    return t + 1;
}

int inc(const int& t)
{
    return t + 42;
}

#include <iostream>
int main() {
   int x = 0;
   x = inc<int>(x);
   std::cout << "Template: " << x << std::endl; // prints 1.

   x = 0;
   x = inc(x);
   std::cout << "Overload: " << x << std::endl; // prints 42.
}

Как видите, в этом примере есть две различные функции inc для значений int: inc(const int&) и inc<int>(const int&). Вы не могли бы расширить шаблон с помощью int, если бы использовали специализацию шаблона.

4 голосов
/ 03 октября 2009

Один такой пример:

#include <cstdio>

template <class T>
void foo(T )
{
    puts("T");
}

//template <>
void foo(int*)
{
    puts("int*");
}

template <class T>
void foo(T*)
{
    puts("T*");
}

int main()
{
    int* a;
    foo(a);
}

На самом деле предлагается использовать не шаблонные перегрузки для функций и оставить специализацию для классов. Более подробно это обсуждается в Почему бы не специализировать шаблоны функций?

1 голос
/ 03 октября 2009

Просто для уточнения первого пункта, упомянутого Литбом в его ответе . Специализации проверяются только после того, как разрешение перегрузки фактически выбрало основной шаблон. Результат может привести к некоторым неожиданностям, когда функция перегружена и имеет явные специализации:

template <typename T> void foo (T);  // Primary #1
template <> void foo<int*> (int*);   // Specialization of #1

template <typename T> void foo (T*); // Primary #2

void bar (int * i)
{
  foo(i);
}

При выборе функции для вызова выполняются следующие шаги:

  1. Поиск имени находит оба основных шаблона.
  2. Каждый шаблон специализирован, и при разрешении перегрузки делается попытка выбрать лучшую функцию на основе преобразований между аргументами и параметрами.
  3. В этом случае нет разницы в качестве конверсий.
  4. Затем для выбора наиболее специализированного шаблона используются правила частичного упорядочения. В данном случае это вторая паримария "foo (T *)".

Только после этих шагов, когда будет выбрана лучшая функция, будут рассматриваться явные специализации функции selected . (В этом случае основной № 2 не имеет ни одного, поэтому ни один не рассматривается).

Единственный способ вызвать вышеуказанную явную специализацию здесь - это использовать явные аргументы шаблона в вызове:

void bar (int * i)
{
  foo<int*> (i);  // expliit template argument forces use of primary #1
}

Хорошее эмпирическое правило - стараться избегать перегрузок, которые также явно специализированы, поскольку результирующие правила довольно сложные.

1 голос
/ 03 октября 2009

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

...