Синтаксис си/с++

Специализация шаблонов: полная и частичная

Введение

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

Концепция

Полная специализация (template<> class Box<bool> { ... }) предоставляет совершенно отдельную, независимую реализацию шаблона для одного конкретного, явно указанного типа — компилятор использует эту специализированную версию вместо обычной, обобщённой реализации именно для этого конкретного типа, что полезно, когда конкретный тип требует принципиально иной реализации (классический пример — специализация std::vector<bool> стандартной библиотеки, использующая упаковку отдельных битов для экономии памяти вместо обычного хранения отдельных bool элементов). Частичная специализация применима только к шаблонам классов (не функций) и позволяет предоставить отдельную реализацию для целого семейства типов, соответствующих определённому паттерну (например, для всех указателей T*, независимо от конкретного типа T), оставаясь при этом параметризованной по той части типа, которая не зафиксирована паттерном специализации.

Пример кода

#include <iostream>

template<typename T>
class TypeDescriber {
public:
    static void describe() { std::cout << "Обычный тип" << std::endl; }
};

// Полная специализация — для КОНКРЕТНОГО типа bool
template<>
class TypeDescriber<bool> {
public:
    static void describe() { std::cout << "Это булев тип!" << std::endl; }
};

int main()
{
    TypeDescriber<int>::describe();    // "Обычный тип" — обычная, неспециализированная версия
    TypeDescriber<bool>::describe();   // "Это булев тип!" — используется ПОЛНАЯ специализация
    return 0;
}
// Частичная специализация — для ВСЕХ указателей, независимо от конкретного типа, на который они указывают
#include <iostream>

template<typename T>
class TypeDescriber2 {
public:
    static void describe() { std::cout << "Обычный тип" << std::endl; }
};

template<typename T>
class TypeDescriber2<T*> { // частичная специализация — паттерн "T*", T остаётся параметром
public:
    static void describe() { std::cout << "Это указатель!" << std::endl; }
};

int main()
{
    TypeDescriber2<int>::describe();      // "Обычный тип"
    TypeDescriber2<int*>::describe();      // "Это указатель!" — частичная специализация по паттерну T*
    TypeDescriber2<double*>::describe();   // "Это указатель!" — РАБОТАЕТ для ЛЮБОГО типа указателя
    return 0;
}

Пояснения к коду

template<> class TypeDescriber<bool> показывает синтаксис полной специализации — пустые угловые скобки template<> сигнализируют, что для этого конкретного, явно указанного типа (bool) предоставляется отдельная, не связанная напрямую с обобщённой реализацией версия класса, которую компилятор выбирает вместо обычного шаблона исключительно для этого конкретного типа. template<typename T> class TypeDescriber2<T*> показывает частичную специализацию — параметр шаблона T остаётся «открытым», параметризованным, но специализация применяется к ЛЮБОМУ типу, соответствующему паттерну «указатель на T» (int*, double*, указатель на любой другой тип), что демонстрируется работой специализации одинаково для int* и double* без необходимости писать отдельную полную специализацию для каждого конкретного типа указателя по отдельности.

Подводные камни

  • Попытка применить частичную специализацию к шаблону функции — в отличие от шаблонов классов, шаблоны функций в C++ поддерживают только полную специализацию, а не частичную, и для достижения похожего на частичную специализацию эффекта для функций обычно используется перегрузка функций в сочетании с SFINAE или, в современном C++, ограничениями через concepts — это часто становящееся неожиданным для разработчиков, ожидающих симметрии между шаблонами классов и функций, ограничение синтаксиса C++.
  • Создание специализации с поведением, существенно нарушающим ожидания, заданные обобщённой, неспециализированной версией шаблона (нарушение принципа подстановки, аналогичного принципу Лисков для наследования) — хотя специализация технически может реализовывать совершенно произвольную, отличную логику, пользователи шаблона, ожидающие согласованного поведения независимо от конкретного типа (особенно если специализация неочевидна и не явно документирована), могут быть удивлены и сбиты с толку существенно отличающимся поведением для конкретного, специализированного типа по сравнению с поведением для всех остальных типов.
  • Конфликт между несколькими частичными специализациями, одновременно подходящими для одного конкретного, реально используемого типа — если объявлены, например, частичные специализации и для T*, и для const T* отдельно, и компилятор сталкивается с необходимостью выбрать одну из них для конкретного использования, потенциально неоднозначно подходящего под оба паттерна, это приводит к ошибке компиляции о неоднозначности, разрешение которой требует либо более точного, явного указания нужной специализации, либо пересмотра набора объявленных частичных специализаций для устранения этой неоднозначности.
  • Чрезмерное использование специализации шаблонов вместо более простых, современных альтернатив (concepts или просто явная перегрузка функций/несколько разных, явно названных классов вместо специализации одного общего шаблона) — для не самых сложных случаев явная, более прямая альтернатива часто читается и понимается легче, чем механизм специализации шаблонов, который, особенно в случае множественных, взаимодействующих специализаций, может стать существенным источником сложности кода без соразмерной выгоды по сравнению с более простыми, явными альтернативами для конкретной, не предельно сложной задачи.