Перегрузка и специализация шаблонных функций.
Попалась на глаза не новая статья на Хабре, в которой автор (достаточно туманно - по моему мнению) разъясняет старую проблему. То, что проблема с бородой, показывает статья Герба Саттера. Ну что же, новое - хорошо забытое старое. Ниже мой вольный перевод статьи Саттера.
Почему не специализируются шаблоны функций?
В вопросе, поставленном в заголовке статьи одновременно содержится утверждение - когда и почему не стоит специализировать шаблоны.
Большая разница: перегрузка против специализации.
Краткий обзор того, что мы, возможно забыли.
В С++ имеются шаблонные классы (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<class T> // (b) второй базовый шаблон, перегружает (a)
void f( T* );
//................
int * p;
f (p); // вызывает (b)! игнорируется разрешение перегрузки
Такой результат удивляет многих. Ключ для понимания очень прост: Специализации не перегружаются (Specializations don't overload).
Перегружаются только базовые шаблоны (как и не-шаблонные функции, конечно). Рассмотрим ещё раз приведенные выше правило, подкрасив важные слова в этот раз.
...
Если отсутствуют подходящие для выбора не-шаблонные функции, то базовые шаблонные функции, как граждане второго сорта будут рассмотрены. Какая базовая шаблонная функция будет выбрана зависит от того, какое соответствие лучше и "более специализировано" [...] в соответствии с набором правил:
Если ясно, что имеется "наиболее специализированная" базовая шаблонная функция, то она и используется. Если эта базовая шаблонная функция специализирована для используемых типов, то используется эта специализация, в противном случае базовый шаблон инстанцируется с правильными типами для использования.
...
Разрешение перегрузки выбирает только базовый шаблон (или не-шаблонную функцию, если она доступна). Только после того, как решено, какой базовый шаблон выбран, компилятор будет искать - а нет ли наиболее подходящей специализации для выбранного шаблона и, в успешном случае, эта специализация будет использована.
...........
По рассматриваемой теме можно почитать ещё тут.
Почему не специализируются шаблоны функций?
В вопросе, поставленном в заголовке статьи одновременно содержится утверждение - когда и почему не стоит специализировать шаблоны.
Большая разница: перегрузка против специализации.
Краткий обзор того, что мы, возможно забыли.
В С++ имеются шаблонные классы (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).
Перегружаются только базовые шаблоны (как и не-шаблонные функции, конечно). Рассмотрим ещё раз приведенные выше правило, подкрасив важные слова в этот раз.
...
Если отсутствуют подходящие для выбора не-шаблонные функции, то базовые шаблонные функции, как граждане второго сорта будут рассмотрены. Какая базовая шаблонная функция будет выбрана зависит от того, какое соответствие лучше и "более специализировано" [...] в соответствии с набором правил:
Если ясно, что имеется "наиболее специализированная" базовая шаблонная функция, то она и используется. Если эта базовая шаблонная функция специализирована для используемых типов, то используется эта специализация, в противном случае базовый шаблон инстанцируется с правильными типами для использования.
...
Разрешение перегрузки выбирает только базовый шаблон (или не-шаблонную функцию, если она доступна). Только после того, как решено, какой базовый шаблон выбран, компилятор будет искать - а нет ли наиболее подходящей специализации для выбранного шаблона и, в успешном случае, эта специализация будет использована.
...........
По рассматриваемой теме можно почитать ещё тут.
Комментарии
Отправить комментарий