Перегрузка и специализация шаблонных функций.

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

Почему не специализируются шаблоны функций?

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

Большая разница: перегрузка против специализации.

Краткий обзор того, что мы, возможно забыли.

В С++ имеются шаблонные классы (class templates) и шаблонные функции(function templates). Эти две вещи не работают совершенно одинаковыми путями, и наиболее очевидная разница это перегрузка: Старые не-шаблонные классы не перегружаются, как не перегружаются и шаблонные. С другой стороны, старые не-шаблонные функции, имеющие одинаковые имена перегружаются, и поэтому шаблонные функции перегружаются тоже. Всё это совеоршенно естественно. Вот что мы можем обобщить в Примере 1:

// Пример 1.
// Шаблонный класс
template<class T>
class X

        /*...*/
}; // (a)

// Шаблонная функция с двумя перегрузками
template<class T>
void f( T ) // (b)
{
    std::cout << "f( T ) (b)" <<std::endl;;
}

template<class T>
void f( int, T, double ) // (c)
{
     std::cout << "f( int, T, double ) (c)" <<std::endl;;
}

Эти неспециализированные шаблоны также называют базовыми шаблонами (base templates).

В дальнейшем, базовые шаблоны могут быть специализированы (specialized). В этом шаблонные классы и шаблонные функции расходятся, важность способов специализации будет показана далее в этой статье. Шаблонный класс может быть частично или полностью специализирован. Шаблонная функция может быть только полностью специализирована, но поскольку шаблонные функции могут быть перегружены - мы можем получить почти тот же эффект за счет перегрузки, который могли бы получить при частичной специализации. Следующий код иллюстрирует эту разницу:

//Пример 1, продолжение: Специализация шаблонов.
// Частичная специализация (a) для указателей 
template<class T> class X<T*> { /*...*/ };

// Полная специализация (a) для целых
template<> class X<int> { /*...*/ };
  


//Отдельный базовый шаблон, который перегружает (b) и (c).
// НЕ частичная специализация (b), поскольку
// нет такой штуки как частичная специализация
// для шаблонной функции!
template<class T> void f( T* ) // (d)
{
     std::cout << "f( T* ) (d)" <<std::endl;;
}

// Полная специализация (b) для int
template<> void f<int>( int ) // (e)
{
         std::cout << "f<int>( int ) (e)" <<std::endl;;
}

//Старая добрая не-шаблонная функция, которая перегружается с 
// (b), (c), и (d) -- но не с (e), что
// мы сейчас обсудим
void f( double ) // (f)
{
         std::cout << "f( double ) (f)" <<std::endl;;
}

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

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

Собрав все правила воедино - мы получаем пример

//Пример 1, продолжение. Разрешение перегрузки.
//
bool b;
int i;
double d;

f(b);//вызов (b) c T=bool
f(i,42,d);//вызов (c) c T=int
f(&i);//вызов (d) c T=int
f(i);//вызов (e)
f(d);//вызов (f)  

До этого мы сознательно рассматривали наиболее простые случаи, теперь ныряем глубже.

Почему не специализировать : Пример Димова-Абрахамса.

// Пример 2: Явная специализация
//
template<class T> // (a) базовый шаблон
void f( T );


template<class T> // (b) второй базовый шаблон, перегружает (a)
void f( T* ); // (шаблонные функции не могут быть частично специализированы
// Вместо этого они перегружаются)


template<> // (c) явная специализация (b)
void f<>(int*);
// ...
int *p;
f( p ); // вызывается  (c)
  


Результат последней строки в Примере 2 - тот, который мы ожидали. Вопрос, однако, заключается в том, почему мы его ожидали. Если мы его ожидали по ложным соображениям, то мы будем сильно удивлены, увидев дальнейшее. "Так и что", - скажет некто, - "я написал специализацию для указателя на int, поэтому, очевидно, это то, что нужно вызвать», - и это точно неправильная причина.

// Пример 3: Поменяем строки (b) и (с) 
//
template<class T> // (a) то же самое
void f( T );



template<>
void f<>(int*);
// (c) явная специализация, на этот раз (a)

template<class T> // (b) второй базовый шаблон, перегружает (a)
void f( T* );

//................

int * p;
f (p); // вызывает (b)! игнорируется разрешение перегрузки

Такой результат удивляет многих. Ключ для понимания очень прост: Специализации не перегружаются (Specializations don't overload).

Перегружаются только базовые шаблоны (как и не-шаблонные функции, конечно). Рассмотрим ещё раз приведенные выше правило, подкрасив важные слова в этот раз.
...
Если  отсутствуют подходящие для выбора не-шаблонные функции, то базовые шаблонные функции, как граждане второго сорта будут рассмотрены. Какая базовая шаблонная функция будет выбрана зависит от того, какое соответствие лучше и "более специализировано" [...] в соответствии с набором правил:
Если ясно, что имеется "наиболее специализированная" базовая шаблонная функция, то она и используется. Если эта базовая шаблонная функция специализирована для используемых типов, то используется эта специализация, в противном случае базовый шаблон инстанцируется с правильными типами для использования.
...
Разрешение перегрузки выбирает только базовый шаблон (или не-шаблонную функцию, если она доступна). Только после того, как решено, какой базовый шаблон выбран, компилятор будет искать - а нет ли наиболее подходящей специализации для выбранного шаблона и, в успешном случае, эта специализация будет использована.

...........

По рассматриваемой теме можно почитать ещё тут.

Комментарии

Популярные сообщения из этого блога

Полезные новации С++17. Выражения свертки (Fold expressions)

Поддержка декомпозиции при объявлении (structural bindings) для классов