Почему шаблоны могут быть реализованы только в заголовочном файле?
Цитата из стандартной библиотеки C++: учебное пособие и справочник:
Единственный переносимый способ использования шаблонов на данный момент - это реализовать их в заголовочных файлах с помощью встроенных функций.
Почему это?
(Пояснение: заголовочные файлы - не единственное переносимое решение. Но это наиболее удобное переносимое решение.)
19 ответов
Нет необходимости помещать реализацию в заголовочный файл, см. Альтернативное решение в конце этого ответа.
В любом случае, причина вашего кода заключается в том, что при создании экземпляра шаблона компилятор создает новый класс с заданным аргументом шаблона. Например:
template<typename T>
struct Foo
{
T bar;
void doSomething(T param) {/* do stuff using T */}
};
// somewhere in a .cpp
Foo<int> f;
При чтении этой строки компилятор создаст новый класс (назовем его FooInt
), что эквивалентно следующему:
struct FooInt
{
int bar;
void doSomething(int param) {/* do stuff using int */}
}
Следовательно, компилятор должен иметь доступ к реализации методов, чтобы создавать их экземпляры с помощью аргумента шаблона (в этом случае int
). Если бы эти реализации не были в заголовке, они не были бы доступны, и поэтому компилятор не смог бы создать экземпляр шаблона.
Распространенным решением этой проблемы является запись объявления шаблона в файл заголовка, затем реализация класса в файле реализации (например,.tpp) и включение этого файла реализации в конец заголовка.
// Foo.h
template <typename T>
struct Foo
{
void doSomething(T param);
};
#include "Foo.tpp"
// Foo.tpp
template <typename T>
void Foo<T>::doSomething(T param)
{
//implementation
}
Таким образом, реализация по-прежнему отделена от объявления, но доступна для компилятора.
Другое решение состоит в том, чтобы отделить реализацию и явно создать экземпляр всех необходимых вам шаблонов:
// Foo.h
// no implementation
template <typename T> struct Foo { ... };
//----------------------------------------
// Foo.cpp
// implementation of Foo's methods
// explicit instantiations
template class Foo<int>;
template class Foo<float>;
// You will only be able to use Foo with int or float
Если мое объяснение недостаточно ясно, вы можете взглянуть на C++ Super-FAQ по этому вопросу.
Это связано с требованием отдельной компиляции и тем, что шаблоны являются полиморфизмом в стиле экземпляров.
Давайте немного приблизимся к конкретному для объяснения. Скажем, у меня есть следующие файлы:
- foo.h
- объявляет интерфейс
class MyClass<T>
- объявляет интерфейс
- foo.cpp
- определяет реализацию
class MyClass<T>
- определяет реализацию
- bar.cpp
- использования
MyClass<int>
- использования
Отдельная компиляция означает, что я должен иметь возможность компилировать foo.cpp независимо от bar.cpp. Компилятор выполняет всю тяжелую работу по анализу, оптимизации и генерации кода на каждом модуле компиляции полностью независимо; нам не нужно делать анализ всей программы. Только компоновщик должен обрабатывать всю программу одновременно, и работа компоновщика существенно проще.
bar.cpp даже не должен существовать, когда я компилирую foo.cpp, но я все еще должен иметь возможность связать foo.o, который я уже имел вместе с bar.o, который я только что создал, без необходимости перекомпилировать foo.cpp. foo.cpp может даже быть скомпилирован в динамическую библиотеку, распространяться где-то еще без foo.cpp и связан с кодом, который они пишут спустя годы после того, как я написал foo.cpp.
"Полиморфизм стиля реализации" означает, что шаблон MyClass<T>
на самом деле не является универсальным классом, который может быть скомпилирован в код, который может работать для любого значения T
, Это добавило бы дополнительные издержки, такие как бокс, необходимость передавать указатели на функции распределителям и конструкторам и т. Д. Цель шаблонов C++ состоит в том, чтобы избежать необходимости писать почти идентичные class MyClass_int
, class MyClass_float
и т. д., но мы все равно можем получить скомпилированный код, который, как правило, выглядит так, как будто мы написали каждую версию отдельно. Таким образом, шаблон буквально шаблон; шаблон класса - это не класс, это рецепт создания нового класса для каждого T
мы сталкиваемся. Шаблон не может быть скомпилирован в код, может быть скомпилирован только результат создания шаблона.
Поэтому, когдаfoo.cpp компилируется, компилятор не может видеть bar.cpp, чтобы знать, что MyClass<int>
нужно. Это может видеть шаблонMyClass<T>
, но он не может генерировать код для этого (это шаблон, а не класс). И когда bar.cpp компилируется, компилятор может видеть, что ему нужно создать MyClass<int>
, но он не видит шаблонMyClass<T>
(только его интерфейс в foo.h), поэтому он не может его создать.
Еслиfoo.cpp сам использует MyClass<int>
, тогда код для этого будет сгенерирован при компиляцииfoo.cpp, поэтому, когда bar.o связан с foo.o, они могут быть подключены и будут работать. Мы можем использовать этот факт, чтобы разрешить реализацию конечного набора шаблонов в файле.cpp, написав один шаблон. Но bar.cpp не может использовать шаблон в качестве шаблона и создавать его экземпляры для любых типов; он может использовать только существующие версии шаблонного класса, которые автор foo.cpp подумал предоставить.
Вы можете подумать, что при компиляции шаблона компилятор должен "генерировать все версии", а те, которые никогда не используются, отфильтровываются во время компоновки. Помимо огромных накладных расходов и чрезвычайных трудностей, с которыми столкнулся бы такой подход, потому что функции "модификатора типа", такие как указатели и массивы, позволяют даже только встроенным типам создавать бесконечное число типов, что происходит, когда я теперь расширяю свою программу добавляя:
- baz.cpp
- объявляет и реализует
class BazPrivate
и используетMyClass<BazPrivate>
- объявляет и реализует
Нет никакого способа, которым это могло бы работать, если мы или
- Приходится перекомпилировать foo.cpp каждый раз, когда мы меняем любой другой файл в программе, на случай, если он добавит новый новый экземпляр
MyClass<T>
- Требовать, чтобы baz.cpp содержал (возможно, через заголовок) полный шаблон
MyClass<T>
, так что компилятор может генерироватьMyClass<BazPrivate>
во время компиляции baz.cpp.
Никому не нравится (1), потому что системам компиляции анализа всей программы требуется вечность для компиляции, и потому что это делает невозможным распространение скомпилированных библиотек без исходного кода. Таким образом, мы имеем (2) вместо этого.
Здесь много правильных ответов, но я хотел бы добавить это (для полноты):
Если вы, в нижней части файла cpp реализации, сделаете явное создание экземпляров всех типов, с которыми будет использоваться шаблон, компоновщик сможет найти их как обычно.
Редактировать: Добавление примера явной реализации шаблона. Используется после определения шаблона и определения всех функций-членов.
template class vector<int>;
Это создаст экземпляр (и, следовательно, сделает доступным для компоновщика) класс и все его функции-члены (только). Аналогичный синтаксис работает для шаблонных функций, поэтому, если у вас есть перегрузки операторов, не являющихся членами, вам может потребоваться сделать то же самое для них.
Приведенный выше пример довольно бесполезен, поскольку вектор полностью определен в заголовках, за исключением случаев, когда используется общий включаемый файл (предварительно скомпилированный заголовок?) extern template class vector<int>
чтобы не создавать его экземпляры во всех других (1000?) файлах, которые используют vector.
Шаблоны должны быть созданы компилятором до фактической компиляции их в объектный код. Эта реализация может быть достигнута только в том случае, если известны аргументы шаблона. Теперь представьте сценарий, в котором функция шаблона объявлена в a.h
, определенный в a.cpp
и используется в b.cpp
, когда a.cpp
компилируется, не обязательно известно, что предстоящая компиляция b.cpp
потребует экземпляр шаблона, не говоря уже о том, какой конкретный экземпляр будет. Для большего количества заголовочных и исходных файлов ситуация может быстро усложниться.
Можно утверждать, что компиляторы могут быть умнее, чтобы "смотреть в будущее" для всех применений шаблона, но я уверен, что не будет трудно создавать рекурсивные или иные сложные сценарии. AFAIK, компиляторы не делают такой взгляд вперед. Как указал Антон, некоторые компиляторы поддерживают явные объявления экспорта экземпляров шаблона, но не все компиляторы поддерживают его (пока?).
На самом деле, до C++11 стандарт определял export
ключевое слово, которое позволило бы объявить шаблоны в заголовочном файле и внедрить их в другом месте.
Ни один из популярных компиляторов не реализовал это ключевое слово. Единственный, о котором я знаю, - это интерфейс, написанный Edison Design Group, который используется компилятором Comeau C++. Все остальные требовали, чтобы вы писали шаблоны в заголовочных файлах, потому что компилятору нужно определение шаблона для правильной реализации (как уже указывали другие).
В результате комитет по стандарту ISO C++ решил удалить export
особенность шаблонов с C++11.
Хотя в стандарте C++ такого требования нет, некоторые компиляторы требуют, чтобы все шаблоны функций и классов были доступны в каждом используемом модуле перевода. По сути, для этих компиляторов тела шаблонных функций должны быть доступны в заголовочном файле. Повторим: это означает, что эти компиляторы не позволят им быть определены в файлах без заголовка, таких как файлы.cpp
Существует ключевое слово экспорта, которое должно смягчить эту проблему, но оно далеко от того, чтобы быть переносимым.
Шаблоны должны использоваться в заголовках, потому что компилятор должен создавать различные версии кода в зависимости от параметров, заданных / выведенных для параметров шаблона. Помните, что шаблон не представляет код напрямую, а шаблон для нескольких версий этого кода. Когда вы компилируете не шаблонную функцию в .cpp
файл, вы компилируете конкретную функцию / класс. Это не относится к шаблонам, которые могут быть созданы с различными типами, а именно, конкретный код должен генерироваться при замене параметров шаблона на конкретные типы.
Была особенность с export
Ключевое слово, которое должно было использоваться для отдельной компиляции. export
функция устарела в C++11
и, AFAIK, только один компилятор реализовал это. Вы не должны использовать export
, Отдельная компиляция невозможна в C++
или же C++11
но возможно в C++17
, если концепции делают это, у нас мог бы быть некоторый способ отдельной компиляции.
Для достижения отдельной компиляции должна быть возможна отдельная проверка тела шаблона. Кажется, что решение возможно с концепциями. Взгляните на этот документ, недавно представленный на заседании комитета по стандартам. Я думаю, что это не единственное требование, так как вам все еще нужно создать экземпляр кода для шаблона в пользовательском коде.
Отдельная проблема компиляции для шаблонов, я думаю, это также проблема, возникающая при переходе на модули, которая в настоящее время работает.
Несмотря на множество хороших объяснений выше, мне не хватает практического способа разделения шаблонов на заголовок и тело.
Моя главная задача - избегать перекомпиляции всех пользователей шаблона, когда я изменяю его определение.
Наличие всех экземпляров шаблона в теле шаблона не является для меня жизнеспособным решением, так как автор шаблона может не знать все, если он используется, и пользователь шаблона может не иметь права изменять его.
Я выбрал следующий подход, который работает и для старых компиляторов (gcc 4.3.4, aCC A.03.13).
Для каждого использования шаблона в его собственном заголовочном файле есть typedef (сгенерированный из модели UML). Его тело содержит экземпляр (который заканчивается в библиотеке, которая связана в конце).
Каждый пользователь шаблона включает этот заголовочный файл и использует typedef.
Схематический пример:
MyTemplate.h:
#ifndef MyTemplate_h
#define MyTemplate_h 1
template <class T>
class MyTemplate
{
public:
MyTemplate(const T& rt);
void dump();
T t;
};
#endif
MyTemplate.cpp:
#include "MyTemplate.h"
#include <iostream>
template <class T>
MyTemplate<T>::MyTemplate(const T& rt)
: t(rt)
{
}
template <class T>
void MyTemplate<T>::dump()
{
cerr << t << endl;
}
MyInstantiatedTemplate.h:
#ifndef MyInstantiatedTemplate_h
#define MyInstantiatedTemplate_h 1
#include "MyTemplate.h"
typedef MyTemplate< int > MyInstantiatedTemplate;
#endif
MyInstantiatedTemplate.cpp:
#include "MyTemplate.cpp"
template class MyTemplate< int >;
main.cpp:
#include "MyInstantiatedTemplate.h"
int main()
{
MyInstantiatedTemplate m(100);
m.dump();
return 0;
}
Таким образом, потребуется перекомпиляция только экземпляров шаблона, а не всех пользователей шаблона (и зависимостей).
Это означает, что наиболее переносимым способом определения реализаций методов шаблонных классов является определение их внутри определения класса шаблона.
template < typename ... >
class MyClass
{
int myMethod()
{
// Not just declaration. Add method implementation here
}
};
Компилятор будет генерировать код для каждого экземпляра шаблона при использовании шаблона на этапе компиляции. В процессе компиляции и компоновки.cpp файлы преобразуются в чистый объектный или машинный код, который содержит ссылки или неопределенные символы, потому что.h файлы, которые включены в ваш main.cpp, не имеют реализации YET. Они готовы быть связаны с другим объектным файлом, который определяет реализацию для вашего шаблона, и, таким образом, у вас есть полный исполняемый файл a.out. Однако, поскольку шаблоны должны быть обработаны на этапе компиляции, чтобы генерировать код для каждого экземпляра шаблона, который вы делаете в своей основной программе, линковка не поможет, потому что компиляция main.cpp в main.o, а затем компиляция вашего шаблона.cpp в template.o, и тогда связывание не достигнет цели шаблона, потому что я связываю инстанцирование другого шаблона с одной и той же реализацией шаблона! И шаблоны должны делать противоположное, то есть иметь ОДНУ реализацию, но допускают множество доступных реализаций посредством использования одного класса.
Имея в виду typename T
get заменяется на этапе компиляции, а не на этапе компоновки, поэтому, если я попытаюсь скомпилировать шаблон без T
заменяется конкретным типом значения, поэтому он не будет работать, потому что это определение шаблонов, это процесс времени компиляции, и, кстати, метапрограммирование - все об использовании этого определения.
Просто чтобы добавить что-то примечательное здесь. Можно точно определить методы шаблонного класса в файле реализации, когда они не являются шаблонами функций.
myQueue.hpp:
template <class T>
class QueueA {
int size;
...
public:
template <class T> T dequeue() {
// implementation here
}
bool isEmpty();
...
}
myQueue.cpp:
// implementation of regular methods goes like this:
template <class T> bool QueueA<T>::isEmpty() {
return this->size == 0;
}
main()
{
QueueA<char> Q;
...
}
Если проблема заключается в дополнительном времени компиляции и разложении двоичного размера, создаваемом путем компиляции.h как части всех модулей.cpp, использующих его, во многих случаях вы можете сделать так, чтобы класс шаблона происходил из базового класса без шаблонов для нетипозависимые части интерфейса, и этот базовый класс может иметь свою реализацию в файле.cpp.
Способ иметь отдельную реализацию заключается в следующем.
//inner_foo.h
template <typename T>
struct Foo
{
void doSomething(T param);
};
//foo.tpp
#include "inner_foo.h"
template <typename T>
void Foo<T>::doSomething(T param)
{
//implementation
}
//foo.h
#include <foo.tpp>
//main.cpp
#include <foo.h>
inner_foo имеет предварительные объявления. foo.tpp имеет реализацию и включает inner_foo.h; и foo.h будет иметь только одну строку, чтобы включить foo.tpp.
Во время компиляции содержимое файла foo.h копируется в foo.tpp, а затем весь файл копируется в файл foo.h, после чего он компилируется. Таким образом, нет никаких ограничений, и наименование является последовательным, в обмен на один дополнительный файл.
Я делаю это потому, что статические анализаторы кода ломаются, когда он не видит предварительные объявления класса в *.tpp. Это раздражает, когда вы пишете код в любой IDE или используете YouCompleteMe или другие.
Это совершенно правильно, потому что компилятор должен знать, какой он тип для выделения. Таким образом, классы шаблонов, функции, перечисления и т. Д. Должны быть также реализованы в заголовочном файле, если он должен быть общедоступным или частью библиотеки (статической или динамической), поскольку заголовочные файлы НЕ компилируются в отличие от файлов c/cpp, которые являются. Если компилятор не знает, тип не может скомпилировать его. В.Net это возможно, потому что все объекты являются производными от класса Object. Это не.Net.
Я предлагаю взглянуть на эту страницу gcc, где обсуждаются компромиссы между моделями cfront и borland для создания экземпляров шаблонов.
https://gcc.gnu.org/onlinedocs/gcc-4.6.4/gcc/Template-Instantiation.html
Модель "borland" соответствует тому, что предлагает автор, обеспечивая полное определение шаблона и многократно компилируя вещи.
Он содержит подробные рекомендации по использованию ручного и автоматического создания экземпляров шаблона. Например, параметр "-repo" можно использовать для сбора шаблонов, которые необходимо создать. Или другой вариант - отключить автоматическое создание экземпляров шаблонов с помощью "-fno-implicit-templates" для принудительного создания экземпляров шаблона вручную.
По моему опыту, я полагаюсь на стандартные библиотеки C++ и шаблоны Boost, которые создаются для каждой единицы компиляции (с использованием библиотеки шаблонов). Для моих больших классов шаблонов я вручную создаю экземпляр шаблона один раз для нужных мне типов.
Это мой подход, потому что я предоставляю рабочую программу, а не библиотеку шаблонов для использования в других программах. Автор книги, Йосуттис, много работает над библиотеками шаблонов.
Если бы меня действительно волновала скорость, я бы подумал об использовании предварительно скомпилированных заголовков https://gcc.gnu.org/onlinedocs/gcc/Precompiled-Headers.html
который получает поддержку во многих компиляторах. Однако я думаю, что с предварительно скомпилированными заголовками будет сложно работать с файлами заголовков шаблонов.
(копирую сюда из закрытого дубликата )
Я предпочитаю хранить в файле все свои функции, независимо от того, являются ли они функциями шаблона или обычными функциями. И есть способ сделать это с помощью некоторых базовых#ifndef
магия. Вот что вы можете сделать:
main.cpp
#include "myclass.hpp"
int main()
{
// ...
}
myclass.hpp
#ifndef MYCLASS
#define MYCLASS
template<class T>
class MyClass
{
T val;
public:
MyClass(T val_);
}
#define MYCLASS_FUNCTIONS
#include "myclass.cpp"
#endif
мойкласс.cpp
#ifndef MYCLASS_FUNCTIONS
#include "myclass.hpp"
// regular functions:
// ...
#else
// template functions:
template<class T>
MyClass<T>::MyClass(T val_)
:val(val_)
{}
// ...
#endif
Вот как это видит прекомпилятор. У нас есть два.cpp
файлы.
- Когда мы компилируем main.cpp, мы:
- включать
- проверьте, что это неопределенно, и это
- определить это
- дать компилятору определения сгенерированного класса (из класса шаблона)
- включать
- определять
- проверьте, определено ли это, это
- дать компилятору определения сгенерированных функций (из функций шаблона)
- Когда мы компилируем myclass.cpp
- проверьте, определено ли это, это не так
- включать
- проверьте, что это неопределенно, и это
- определить это
- дать компилятору определения класса
- включать
- включать
myclass.hpp
снова - на этот раз
MYCLASS
определен, поэтому ничего не делайте внутри, вернитесь кmyclass.cpp
- проверить, если
MYCLASS_FUNCTIONS
определяется, это - дать компилятору определение сгенерированных функций (из функций шаблона)
- выйти, включить дважды
- передать компилятору все штатные функции
С мотивацией ответа Моше: /questions/12898194/pochemu-shablonyi-mogut-byit-realizovanyi-tolko-v-zagolovochnom-fajle/12898207#12898207
Небольшой вклад с моей стороны с расширенным примером. Допустим, существует общий OperationSuccess, и он содержит ResponseSuccess, который имеет общий тип.
ResponseSuccess.h
template <class T>
class ResponseSuccess {
public:
ResponseSuccess(const ResponseStatus responseStatus, const T& data) :
m_responseStatus(responseStatus),
m_data(data) {}
~ResponseSuccess() = default;
// Basis requirement, have Copy/Move constructor/delete assignment operator
ResponseStatus getResponseStatus() const {
return m_responseStatus;
}
T getData() const {
return m_data;
};
private:
ResponseStatus m_responseStatus;
T m_data;
};
ОперацияSuccess.h
template <class T>
class OperationResponse {
public:
explicit OperationResponse(ResponseSuccess<T> responseSuccess) :
m_responseSuccess(std::move(responseSuccess)) {}
~OperationResponse() = default;
// Basis requirement, have Copy/Move constructor/delete assignment operator
ResponseSuccess<T> getResponseSuccess() const {
return m_responseSuccess;
}
private:
ResponseSuccess<T> m_responseSuccess;
// have a failure, in case required
};
Использование:
MyObject myObj(<ctor_args>);
ResponseSuccess<MyObject> responseSuccess(ResponseStatus::SUCCESS, myObj);
OperationResponse<MyObject> successOperationResponse(responseSuccess);
..
// Fetches the response -> successOperationResponse.getResponseSuccess();
Вы действительно можете определить свой шаблонный класс внутри файла.template, а не файла.cpp. Тот, кто говорит, что вы можете определить его только внутри заголовочного файла, ошибается. Это то, что работает вплоть до C++ 98.
Не забывайте, чтобы ваш компилятор рассматривал ваш файл.template как файл C++, чтобы сохранить смысл intelli.
Вот пример этого для класса динамического массива.
#ifndef dynarray_h
#define dynarray_h
#include <iostream>
template <class T>
class DynArray{
int capacity_;
int size_;
T* data;
public:
explicit DynArray(int size = 0, int capacity=2);
DynArray(const DynArray& d1);
~DynArray();
T& operator[]( const int index);
void operator=(const DynArray<T>& d1);
int size();
int capacity();
void clear();
void push_back(int n);
void pop_back();
T& at(const int n);
T& back();
T& front();
};
#include "dynarray.template" // this is how you get the header file
#endif
Теперь внутри вашего файла.template вы определяете свои функции так, как вы это обычно делаете.
template <class T>
DynArray<T>::DynArray(int size, int capacity){
if (capacity >= size){
this->size_ = size;
this->capacity_ = capacity;
data = new T[capacity];
}
// for (int i = 0; i < size; ++i) {
// data[i] = 0;
// }
}
template <class T>
DynArray<T>::DynArray(const DynArray& d1){
//clear();
//delete [] data;
std::cout << "copy" << std::endl;
this->size_ = d1.size_;
this->capacity_ = d1.capacity_;
data = new T[capacity()];
for(int i = 0; i < size(); ++i){
data[i] = d1.data[i];
}
}
template <class T>
DynArray<T>::~DynArray(){
delete [] data;
}
template <class T>
T& DynArray<T>::operator[]( const int index){
return at(index);
}
template <class T>
void DynArray<T>::operator=(const DynArray<T>& d1){
if (this->size() > 0) {
clear();
}
std::cout << "assign" << std::endl;
this->size_ = d1.size_;
this->capacity_ = d1.capacity_;
data = new T[capacity()];
for(int i = 0; i < size(); ++i){
data[i] = d1.data[i];
}
//delete [] d1.data;
}
template <class T>
int DynArray<T>::size(){
return size_;
}
template <class T>
int DynArray<T>::capacity(){
return capacity_;
}
template <class T>
void DynArray<T>::clear(){
for( int i = 0; i < size(); ++i){
data[i] = 0;
}
size_ = 0;
capacity_ = 2;
}
template <class T>
void DynArray<T>::push_back(int n){
if (size() >= capacity()) {
std::cout << "grow" << std::endl;
//redo the array
T* copy = new T[capacity_ + 40];
for (int i = 0; i < size(); ++i) {
copy[i] = data[i];
}
delete [] data;
data = new T[ capacity_ * 2];
for (int i = 0; i < capacity() * 2; ++i) {
data[i] = copy[i];
}
delete [] copy;
capacity_ *= 2;
}
data[size()] = n;
++size_;
}
template <class T>
void DynArray<T>::pop_back(){
data[size()-1] = 0;
--size_;
}
template <class T>
T& DynArray<T>::at(const int n){
if (n >= size()) {
throw std::runtime_error("invalid index");
}
return data[n];
}
template <class T>
T& DynArray<T>::back(){
if (size() == 0) {
throw std::runtime_error("vector is empty");
}
return data[size()-1];
}
template <class T>
T& DynArray<T>::front(){
if (size() == 0) {
throw std::runtime_error("vector is empty");
}
return data[0];
}
Еще одна причина, по которой в заголовочные файлы рекомендуется записывать как объявления, так и определения, - удобство чтения. Предположим, в Utility.h есть такая шаблонная функция:
template <class T>
T min(T const& one, T const& theOther);
И в Utility.cpp:
#include "Utility.h"
template <class T>
T min(T const& one, T const& other)
{
return one < other ? one : other;
}
Это требует, чтобы каждый класс T здесь реализовывал оператор less than (<). Он сгенерирует ошибку компилятора, когда вы сравните два экземпляра класса, которые не реализовали "<".
Поэтому, если вы разделите объявление и определение шаблона, вы не сможете только прочитать файл заголовка, чтобы увидеть все входы и выходы этого шаблона, чтобы использовать этот API в своих собственных классах, хотя компилятор скажет вам об этом случай, о котором оператор должен быть переопределен.