Почему шаблоны могут быть реализованы только в файле заголовка?

цитата стандартная библиотека C++: учебник и руководство:

единственный переносимый способ использования шаблонов на данный момент-реализовать их в файлах заголовков с помощью встроенных функций.

Почему это?

(уточнение: заголовочные файлы не только портативное решение. Но они являются наиболее удобным портативным решением.)

14 ответов


это не необходимо поместить реализацию в заголовочный файл, см. альтернативное решение в конце этого ответа.

во всяком случае, причина сбоя кода заключается в том, что при создании экземпляра шаблона компилятор создает новый класс с заданным аргументом шаблона. Например:

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). Если бы эти реализации не были в заголовке, они не были бы доступны, и поэтому компилятор не смог бы создать экземпляр шаблона.

общим решением для этого является запись объявления шаблона в файле заголовка, а затем реализовать класс в файле реализации (например. ТЭЦ), и включить этот файл реализации в конце заголовка.

// 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 по этому вопросу.


много правильных ответов здесь, но я хотел добавить (для полноты картины):

Если вы, в нижней части файла cpp реализации, делаете явный экземпляр всех типов, с которыми будет использоваться шаблон, компоновщик сможет найти их, как обычно.

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

template class vector<int>;

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

приведенный выше пример довольно бесполезен, так как вектор полностью определен в заголовках, за исключением случаев, когда общий файл include (предварительно скомпилированный заголовок?) использует extern template class vector<int> чтобы он не создавал его во всех другое (1000?) файлы, использующие вектор.


это из-за требования к отдельной компиляции и потому, что шаблоны являются полиморфизмом в стиле экземпляра.

давайте немного приблизимся к конкретному для объяснения. Скажем у меня есть следующие файлы:

  • foo.ч
    • объявляет интерфейс class MyClass<T>
  • foo.СРР
    • определяет реализацию class MyClass<T>
  • бар.СРР
    • использует MyClass<int>

отдельная компиляция означает, что я должен быть в состоянии скомпилировать foo.cpp независимо от бар.cpp. Компилятор выполняет всю тяжелую работу по анализу, оптимизации и генерации кода на каждом блоке компиляции совершенно независимо; нам не нужно делать анализ всей программы. Это только компоновщик, который должен обрабатывать все программируйте сразу, и работа компоновщика существенно облегчится.

бар.cpp даже не нужно существовать, когда я компилирую foo.cpp, но я все равно смогу связать foo.o у меня уже было вместе с бар.o я только что произвел, без необходимости перекомпилировать foo.cpp. foo.cpp может быть даже скомпилирован в динамическую библиотеку, распространенную где-то еще без foo.cpp, и связанный с кодом они пишут годы после того, как я написал foo.cpp.

"полиморфизм в стиле экземпляра" означает, что шаблон MyClass<T> на самом деле не является универсальным классом, который может быть скомпилирован в код, который может работать для любого значения T. Это добавило бы накладные расходы, такие как бокс, необходимость передачи указателей функций распределителям и конструкторам и т. д. Цель шаблонов C++ - избежать необходимости писать почти идентичные class MyClass_int, class MyClass_float, и т. д., Но все равно иметь возможность получить скомпилированный код, который в основном выглядит так, как будто мы had пишется каждая версия отдельно. Таким образом, шаблон буквально шаблон; шаблон класса -не класс, это рецепт для создания нового класса для каждого T мы сталкиваемся. Шаблон нельзя скомпилировать в код, можно скомпилировать только результат создания экземпляра шаблона.

когда foo.cpp компилируется, компилятор не вижу бар.cpp знал, что это. Он может видеть шаблон MyClass<T>, но он не может создавать код для этого (это шаблон, а не класс). И когда бар.cpp компилируется, компилятор может видеть, что ему нужно создать MyClass<int>, но он не может видеть шаблон MyClass<T> (только его интерфейс в foo.h), поэтому он не может создать ее.

если foo.cpp использует MyClass<int>, затем код для этого будет сгенерирован в то время как компиляция foo.cpp, когда бар.o связан с foo.o они могут быть подключены и будут работать. Мы можем использовать этот факт, чтобы позволить реализовать конечный набор экземпляров шаблона в a .cpp файл, написав один шаблон. Но для бар.cpp использовать шаблон как шаблон и инстанцировать его на любые типы, которые ему нравятся; он может использовать только существующие версии шаблонного класса, которые автор foo.cpp решил поставить.

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

  • Базом.СРР
    • объявляет и реализует class BazPrivate, и использует MyClass<BazPrivate>

нет никакой возможности, что это может сработать, если мы либо

  1. перекомпилировать foo.cpp каждый раз, когда мы меняем любой другой файл в программе, в случае, если он добавил новый роман экземпляр MyClass<T>
  2. требуют Базом.cpp содержит (возможно, через заголовок включает) полный шаблон MyClass<T>, чтобы компилятор мог генерировать MyClass<BazPrivate> при компиляции Базом.cpp.

никто не любит (1), ведь всей программы-анализ системы компиляции забрать навсегда для компиляции , и потому, что это делает невозможным распространение скомпилированных библиотек, но без исходного кода. Вместо этого мы имеем (2).


шаблоны должны быть инстанцировать компилятором перед фактической компиляцией их в объектный код. Этот экземпляр может быть достигнут только в том случае, если известны аргументы шаблона. Теперь представьте себе сценарий, в котором функция шаблона объявлена в a.h, определена в a.cpp и в b.cpp. Когда a.cpp компилируется, не обязательно известно, что предстоящая компиляция b.cpp потребуется экземпляр шаблона, не говоря уже о том, какой конкретный экземпляр будет вот так. Для большего количества заголовочных и исходных файлов ситуация может быстро усложниться.

можно утверждать, что компиляторы могут быть сделаны умнее, чтобы "смотреть вперед" для всех видов использования шаблона, но я уверен, что не будет сложно создать рекурсивные или иным образом сложные сценарии. AFAIK, компиляторы так не выглядят. Как отметил Антон, некоторые компиляторы поддерживают явные экспортные объявления экземпляров шаблонов, но не все компиляторы поддерживают его (пока?).


на самом деле, версии стандарта C++ до C++11 определили ключевое слово "export", которое б сделать возможным просто объявить шаблоны в файле заголовка и реализовать их в другом месте.

к сожалению, ни один из популярных компиляторов реализовали это ключевое слово. Единственный, о котором я знаю, - это интерфейс, написанный Edison Design Group, который используется компилятором Comeau C++. Все остальные настаивали на том, чтобы вы писали шаблоны в заголовочных файлах, нуждаясь в определении кода для правильного создания экземпляра (как уже указывали другие).

в результате комитет по стандарту ISO C++ решил удалить export особенность шаблонов, начинающихся с C++11.


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

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


шаблоны должны использоваться в заголовках, поскольку компилятору необходимо создать экземпляры различных версий кода в зависимости от параметров, заданных/выведенных для параметров шаблона. Помните, что шаблон не представляет код напрямую, а шаблон для нескольких версий этого кода. Когда вы компилируете функцию без шаблона в .cpp file, вы компилируете конкретную функцию / класс. Это не относится к шаблонам, которые могут быть созданы с различными типами, а именно, конкретный код должен испускаться при замене параметров шаблона конкретными типами.

была функция с export ключевое слово, которое должно было использоваться для отдельной компиляции. The export функция устарела в C++11 и, AFAIK, только один компилятор реализовал его. Вы не должны использовать export. Отдельная компиляция невозможна в C++ или C++11 но, может быть, в C++17, если концепции делают это, мы могли бы иметь какой-то способ разделения сборник.

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

отдельная проблема компиляции для шаблонов Я думаю, что это также проблема, которая возникает с переходом на модули, который в настоящее время прорабатывается.


это означает, что наиболее переносимым способом определения реализаций метода классов шаблонов является определение их внутри определения класса шаблона.

template < typename ... >
class MyClass
{

    int myMethod()
    {
       // Not just declaration. Add method implementation here
    }
};

хотя есть много хороших объяснений выше, мне не хватает практического способа разделить шаблоны на заголовок и тело.
Моя главная забота-избежать перекомпиляции всех пользователей шаблона, когда я изменяю его определение.
Наличие всех экземпляров шаблона в теле шаблона не является жизнеспособным решением для меня, так как Автор шаблона может не знать все, если его использование и пользователь шаблона не имеют права изменять его.
Я принял следующий подход, который работает также для старых компиляторов (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;
}

таким образом, необходимо будет перекомпилировать только экземпляры шаблонов, а не всех пользователей шаблонов (и зависимостей).


Это точно правильно, потому что компилятор должен знать, какой тип это для выделения. Итак, классы шаблонов, функции, перечисления и т. д.. должен быть реализован также в заголовочном файле, если он должен быть общедоступным или частью библиотеки (статической или динамической), потому что заголовочные файлы не компилируются в отличие от файлов c/cpp, которые есть. Если компилятор не знает, что тип не может его скомпилировать. В .Net это возможно, потому что все объекты являются производными от класса Object. Это не так .Сеть.


Если проблема заключается в дополнительном времени компиляции и двоичном размере раздувания, производимом компиляцией .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; и фу.h будет иметь только одну строку, включая foo.ГРЭС.

во время компиляции содержимое foo.h копируются в foo.tpp, а затем весь файл копируется в foo.h после чего он компилируется. Таким образом, нет никаких ограничений, и имя непротиворечиво, в обмен на один дополнительный файл.

I сделайте это, потому что статические анализаторы для разрыва кода, когда он не видит прямые объявления класса в *.ГРЭС. Это раздражает при написании кода в любой среде IDE или с помощью YouCompleteMe или других.


компилятор будет генерировать код для каждого экземпляра шаблона при использовании шаблона на этапе компиляции. В процессе компиляции и связывания .cpp-файлы преобразуются в чистый объектный или машинный код, который в них содержит ссылки или неопределенные символы .H-файлы, которые включены в основной.cpp еще не реализованы. Они готовы быть связаны с другим объектным файлом, который определяет реализацию для вашего шаблона, и поэтому у вас есть полный a.выход выполнимый. Однако, поскольку шаблоны должны обрабатываться на этапе компиляции, чтобы генерировать код для каждого экземпляра шаблона, который вы делаете в своей основной программе, связывание не поможет, потому что компиляция main.cpp в main.o а затем компиляция шаблона .cpp в шаблон.o и затем связывание не достигнет цели шаблонов, потому что я связываю разные экземпляры шаблонов с одной и той же реализацией шаблона! И шаблоны должны делать противоположное i.e иметь один реализация, но допускает множество доступных экземпляров с помощью одного класса.

смысл typename T get заменен на этапе компиляции, а не на этапе связывания, поэтому, если я попытаюсь скомпилировать шаблон без T заменяется как конкретный тип значения, поэтому он не будет работать, потому что это определение шаблонов, это процесс времени компиляции, и мета-Программирование btw связано с использованием этого определения.


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


моя_очередь.ГЭС:

template <class T> 
class QueueA {
    int size;
    ...
public:
    template <class T> T dequeue() {
       // implementation here
    }

    bool isEmpty();

    ...
}    

моя_очередь.cpp:

// implementation of regular methods goes like this:
template <class T> bool QueueA<T>::isEmpty() {
    return this->size == 0;
}


main()
{
    QueueA<char> Q;

    ...
}