Основным
постулатом работы с шаблонами является то, что
определение шаблона должно быть достижимо из места его инстанцирования.
Что это означает на практике. После
4й стадии трансляции формируется модуль трансляции, содержимое которого и компилируется в объектный файл. Код модуля трансляции должен быть исчерпывающим, в нем должны находиться все необходимые для компиляции определения. А определение шаблона относится именно к таким требуемым.
Значит определение используемого в модуле шаблона должно быть внесено в модуль трансляции. А это значит что или определение должно быть вписано в тот
.cpp
файл, в котором шаблон инстанцируется, или определение должно быть добавлено через директиву
#include
.
В первом случае, когда используемое в
.cpp
файле определение шаблона вписано в том же
.cpp
файле, определение шаблона будет достижимо только в данном
.cpp
файле и больше нигде. В этом случае попытка инстанцировать шаблон в другом модуле трансляции приведет к ошибке трансляции, т.к. там определние шаблона будет уже недостижимо.
Файлы исходного кода включать через директиву
#include
- это дурной тон и прямой путь к нарушению
ODR.
Во втором случае стоит сразу вспомнить про
ODR и правила его соблюдения. В частности про то, что определения методов по месту их объявления являются
встраиваемыми неявно. А если определение метода выполняется снаружи определения класса, то в виду требований к шаблонам функций и шаблонам методов, чтобы гарантировать ODR, встраиваемость определения нужно указывать уже явным образом.
Широкой практикой является разделение кода шаблонов на несколько типов файлов. В
.h
файлах обычно делают или объявления шаблонов функций, или определения шаблонов классов. В
.hpp
/
.inl
файлах обычно делают определения шаблонов функций и шаблонов методов.
При этом
.hpp
/
.inl
файл очень часто включается в самом низу
.h
файла с его объявлениями.
Моя личная рекомендация: использовать в таких случаях расширение
.inl
(от слова
inline), т.к. для
.hpp
столь же широко закреплено взаимоисключающее с
.h
значение заголовка C++ кода. И видеть эти два расширения в одном проекте обычно бывает странно.
Вектор же, например
в проекте LLVM, реализован так, что часть определений в нем сделаны по месту объявления, а часть - как внешние определения сразу после определения шаблона вектора. Все это сделано прямо в одном заголовке.