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

1473

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

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

Почему это?

(Уточнение: файлы заголовков - не единственное портативное решение, но это наиболее удобное портативное решение.)

  • 48
    Вопрос неверный. Есть еще один портативный способ. Класс шаблона может быть явно создан - как было указано в других ответах.
  • 11
    Хотя верно, что размещение всех определений шаблонных функций в заголовочном файле, вероятно, является наиболее удобным способом их использования, все еще неясно, что делает «inline» в этой цитате. Для этого нет необходимости использовать встроенные функции. «Inline» не имеет к этому никакого отношения.
Показать ещё 1 комментарий
Теги:
templates
c++-faq

15 ответов

1222
Лучший ответ

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

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

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

  • 77
    На самом деле явное создание экземпляра должно быть в файле .cpp, который имеет доступ к определениям для всех функций-членов Foo, а не в заголовке.
  • 11
    «Компилятор должен иметь доступ к реализации методов, чтобы создавать их экземпляры с помощью аргумента шаблона (в данном случае int). Если бы эти реализации не были в заголовке, они не были бы доступны» Но почему реализация в файл .cpp не доступен для компилятора? Компилятор также может получить доступ к информации .cpp, как еще он может превратить их в файлы .obj? РЕДАКТИРОВАТЬ: ответ на этот вопрос в ссылке, приведенной в этом ответе ...
Показать ещё 26 комментариев
224

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

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

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

template class vector<int>;

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

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

  • 35
    Тьфу. Хороший ответ, но нет действительно чистого решения. Перечисление всех возможных типов для шаблона не похоже на то, каким должен быть шаблон.
  • 5
    Это может быть хорошо во многих случаях, но, как правило, нарушает назначение шаблона, что позволяет вам использовать класс с любым type не перечисляя их вручную.
Показать ещё 4 комментария
192

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

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

  • 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. Это добавит накладные расходы, такие как бокс, необходимо передать указатели на функции для распределителей и конструкторов и т.д. Намерение шаблонов С++ состоит в том, чтобы избежать необходимости писать почти идентичные 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).

  • 31
    выделенный цитатой шаблон буквально является шаблоном; шаблон класса - это не класс, это рецепт создания нового класса для каждого T, с которым мы сталкиваемся
  • 0
    Я хотел бы знать, возможно ли делать явные экземпляры из других источников, кроме заголовка класса или исходного файла? Например, сделать их в main.cpp?
Показать ещё 4 комментария
68

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

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

  • 1
    «экспорт» является стандартным, но его сложно реализовать, поэтому большинство команд компиляторов еще не сделали этого.
  • 5
    Экспорт не устраняет необходимость раскрытия исходного кода и не уменьшает зависимости компиляции, хотя требует огромных усилий со стороны сборщиков компиляторов. Поэтому сам Херб Саттер попросил сборщиков компиляторов «забыть об» экспорте. Поскольку время, необходимое для инвестиций, лучше потратить в другом месте ...
Показать ещё 5 комментариев
57

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

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

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

  • 3
    ... и пару лет спустя я наконец понял, что на самом деле дал бы нам export , а что нет ... и теперь я искренне согласен с людьми из EDG: это не принесло бы нам то, что большинство людей (я сам в ') 11 включенных) думаю, что будет, и стандарт C ++ лучше без него.
  • 1
    @DevSolar: эта статья политическая, повторяющаяся и плохо написанная. там не обычная проза стандартного уровня. Чрезвычайно длинный и скучный, говоря в основном 3 раза одни и те же вещи на десятках страниц. Но мне теперь сообщили, что экспорт не экспорт. Это хорошая информация!
Показать ещё 1 комментарий
31

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

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

  • 0
    Почему я не могу реализовать их в файле .cpp с ключевым словом "inline"?
  • 2
    Вы можете, и вам не нужно даже помещать «inline». Но вы сможете использовать их только в этом файле cpp и больше нигде.
Показать ещё 1 комментарий
27

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

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

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

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

13

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

template < typename ... >
class MyClass
{

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

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

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

  • 0
    Мне нравится этот подход, за исключением файла MyInstantiatedTemplate.h и добавления типа MyInstantiatedTemplate . Немного чище, если ты им не пользуешься, имхо. Оформите мой ответ на другой вопрос, показывающий это: stackoverflow.com/a/41292751/4612476
6

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

  • 1
    Этот ответ должен быть изменен гораздо больше. Я « независимо » обнаружил ваш тот же подход и специально искал кого-то, кто уже использовал его, так как мне любопытно, является ли это официальным паттерном и есть ли у него имя. Мой подход заключается в реализации class XBase везде, где мне нужно реализовать template class X , помещая зависимые от типа части в X а все остальные в XBase .
6

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

  • 5
    «заголовочные файлы НЕ компилируются» - это действительно странный способ описать это. Заголовочные файлы могут быть частью модуля перевода, как файл "c / cpp".
  • 1
    Фактически, это почти полная противоположность истине: заголовочные файлы очень часто компилируются много раз, тогда как исходный файл обычно компилируется один раз.
2

Способ иметь отдельную реализацию выглядит следующим образом.

//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 или других.

  • 1
    s / inner_foo / foo / g и включите foo.tpp в конце foo.h. На один файл меньше.
1

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


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;

    ...
}
1

Компилятор будет генерировать код для каждого экземпляра шаблона при использовании шаблона во время этапа компиляции. В процессе компиляции и компоновки файлы .cpp преобразуются в чистый объект или машинный код, который в них содержит ссылки или символы undefined, потому что файлы .h, которые включены в ваш файл main.cpp, не имеют реализации YET. Они готовы быть связаны с другим объектным файлом, который определяет реализацию для вашего шаблона, и, следовательно, у вас есть полный исполняемый файл a.out. Однако, поскольку шаблоны необходимо обработать на этапе компиляции, чтобы сгенерировать код для каждого экземпляра шаблона, который вы делаете в своей основной программе, ссылка не поможет, поскольку компиляция main.cpp в main.o, а затем компиляция вашего шаблона .cpp в template.o, а затем ссылка не будет достигать цели шаблонов, потому что я связываю различные экземпляры шаблонов с одной и той же реализацией шаблона! И шаблоны должны делать обратное, чтобы иметь одну реализацию, но допускать много доступных экземпляров с использованием одного класса.

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

0

Это потому, что стандарт C++ управляется комитетом.

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню