Почему шаблоны могут быть реализованы только в заголовочном файле? - PullRequest
1579 голосов
/ 30 января 2009

Цитата из Стандартная библиотека C ++: учебное пособие и справочник :

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

Почему это?

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

Ответы [ 16 ]

1379 голосов
/ 30 января 2009

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

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

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

227 голосов
/ 13 августа 2009

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

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

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

template class vector<int>;

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

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

210 голосов
/ 11 мая 2013

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

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

  • 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>

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

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

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

73 голосов
/ 30 января 2009

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

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

58 голосов
/ 30 января 2009

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

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

В результате комитет по стандартизации ISO C ++ решил удалить функцию export шаблонов с C ++ 11.

34 голосов
/ 30 января 2009

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

Существует ключевое слово export , которое должно решить эту проблему, но оно далеко от того, чтобы быть переносимым.

27 голосов
/ 12 мая 2013

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

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

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

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

15 голосов
/ 30 января 2009

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

template < typename ... >
class MyClass
{

    int myMethod()
    {
       // Not just declaration. Add method implementation here
    }
};
12 голосов
/ 12 мая 2016

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

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

6 голосов
/ 27 июля 2016

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

...