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

1780

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

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

Почему это?

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

MainID
источник
13
Хотя верно, что размещение всех определений шаблонных функций в заголовочном файле, вероятно, является наиболее удобным способом их использования, все еще неясно, что «встроенный» делает в этой цитате. Для этого нет необходимости использовать встроенные функции. «Inline» не имеет к этому никакого отношения.
AnT
7
Книга устарела.
Gerardw
1
Шаблон не похож на функцию, которая может быть скомпилирована в байт-код. Это просто шаблон для генерации такой функции. Если вы поместите шаблон самостоятельно в файл * .cpp, компилировать нечего. Более того, явное создание экземпляра на самом деле является не шаблоном, а отправной точкой для создания функции из шаблона, которая заканчивается в файле * .obj.
dgrat
5
Неужели я единственный, кто чувствует, что концепция шаблона в C ++ искалечена из-за этого? ...
DragonGamer

Ответы:

1559

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

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

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

Люк Турэй
источник
96
На самом деле явное создание экземпляра должно быть в файле .cpp, который имеет доступ к определениям для всех функций-членов Foo, а не в заголовке.
Mankarse
11
«Компилятор должен иметь доступ к реализации методов, чтобы создавать их экземпляры с помощью аргумента шаблона (в данном случае int). Если бы эти реализации не были в заголовке, они не были бы доступны» Но почему реализация в файл .cpp не доступен для компилятора? Компилятор также может получить доступ к информации .cpp, как еще он может превратить их в файлы .obj? РЕДАКТИРОВАТЬ: ответ на этот вопрос находится в ссылке, приведенной в этом ответе ...
xcrypt
31
Я не думаю, что это объясняет вопрос ясно, что ключевая вещь, очевидно, связана с компиляцией UNIT, которая не упоминается в этом посте
zinking
6
@Gabson: структуры и классы эквивалентны, за исключением того, что модификатор доступа по умолчанию для классов является «закрытым», в то время как он является открытым для структур. Есть несколько других крошечных различий, которые вы можете узнать, взглянув на этот вопрос .
Люк Турай
3
Я добавил предложение в самом начале этого ответа, чтобы прояснить, что вопрос основан на ложной предпосылке. Если кто-то спросит: «Почему X - правда?» когда на самом деле X не соответствует действительности, мы должны быстро отвергнуть это предположение.
Аарон МакДейд
251

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

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

  • 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) вместо этого.

Бен
источник
50
выделенный цитатой шаблон буквально является шаблоном; шаблон класса - это не класс, это рецепт создания нового класса для каждого T, с которым мы сталкиваемся
v.oddou
Я хотел бы знать, возможно ли делать явные экземпляры откуда-то, кроме заголовка класса или исходного файла? Например, сделать их в main.cpp?
gromit190
1
@Birger Вы должны иметь возможность сделать это из любого файла, который имеет доступ к полной реализации шаблона (либо потому, что он находится в том же файле, либо через заголовок).
Бен
11
@ajeh Это не риторика. Вопрос в том, «почему вы должны реализовывать шаблоны в заголовке?», Поэтому я объяснил технический выбор языка C ++, который приводит к этому требованию. До того, как я написал свой ответ, другие уже предоставили обходные пути, которые не являются полными решениями, потому что не может быть полного решения. Я чувствовал, что эти ответы будут дополнены более полным обсуждением вопроса «почему».
Бен
1
представьте себе так, ребята ... если бы вы не использовали шаблоны (чтобы эффективно кодировать то, что вам нужно), вы бы в любом случае предлагали только несколько версий этого класса. так что у вас есть 3 варианта. 1). не используйте шаблоны. (как и все другие классы / функции, никого не волнует, что другие не могут изменять типы) 2). использовать шаблоны и документировать, какие типы они могут использовать. 3). дать им весь бонус реализации (источник) 4). дайте им весь источник на случай, если они захотят сделать шаблон из другого вашего класса;)
Лужа
250

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

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

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

template class vector<int>;

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

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

MaHuJa
источник
51
Тьфу. Хороший ответ, но нет действительно чистого решения. Перечисление всех возможных типов для шаблона не похоже на то, каким должен быть шаблон.
Jiminion
6
Это может быть хорошо во многих случаях, но, как правило, нарушает назначение шаблона, который предназначен для того, чтобы позволить вам использовать класс с любым typeбез их ручного перечисления.
Томаш Зато - Восстановить Монику
7
vectorЭто не хороший пример, потому что контейнер по своей природе нацелен на «все» типы. Но очень часто случается, что вы создаете шаблоны, которые предназначены только для определенного набора типов, например числовых типов: int8_t, int16_t, int32_t, uint8_t, uint16_t и т. Д. В этом случае все же имеет смысл использовать шаблон , но явное создание их экземпляров для всего набора типов также возможно и, на мой взгляд, рекомендуется.
UncleZeiv
Используется после определения шаблона «и все функции-члены определены». Спасибо !
Витт Вольт
1
Я чувствую, что что-то упустил ... Я поместил явное создание экземпляров для двух типов в .cppфайл класса, и два экземпляра ссылаются из других .cppфайлов, и я все еще получаю ошибку компоновки, что члены не найдены.
oarfish
81

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

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

Дэвид Ханак
источник
1
«экспорт» является стандартным, но его сложно реализовать, поэтому большинство команд компиляторов еще не сделали этого.
Вава
5
Экспорт не устраняет необходимость раскрытия исходного кода и не уменьшает зависимости компиляции, хотя требует огромных усилий со стороны сборщиков компиляторов. Поэтому сам Херб Саттер попросил сборщиков компиляторов «забыть об» экспорте. Поскольку время, необходимое для инвестиций, лучше потратить в другом месте ...
Питер
2
Поэтому я не думаю, что экспорт еще не реализован. Это, вероятно, никогда не будет сделано кем-то еще, кроме EDG, после того, как другие увидят, сколько времени это заняло, и как мало было получено
Питер
3
Если это вас интересует, газета называется «Почему мы не можем позволить себе экспорт», она указана в его блоге ( gotw.ca/publications ), но там нет pdf (хотя быстрый Google должен его включить)
Питер
1
Хорошо, спасибо за хороший пример и объяснение. Но вот мой вопрос: почему компилятор не может понять, где вызывается шаблон, и сначала скомпилировать эти файлы перед компиляцией файла определения? Я могу предположить, что это может быть сделано в простом случае ... Ответ, что взаимозависимости испортят порядок довольно быстро?
Влад
63

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

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

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

DevSolar
источник
6
... и пару лет спустя я наконец понял, что на exportсамом деле дало бы нам, а что нет ... и теперь я искренне согласен с людьми из EDG: это не принесло бы нам то, что большинство людей (я сам в '11 включил) думаю, что будет, и стандарт C ++ лучше без него.
DevSolar
4
@DevSolar: эта статья политическая, повторяющаяся и плохо написанная. там не обычная проза стандартного уровня. Чрезвычайно длинный и скучный, говоря, в основном, 3 раза одни и те же вещи на десятках страниц. Но теперь мне сообщили, что экспорт - это не экспорт. Это хорошая информация!
v.oddou
1
@ v.oddou: Хороший разработчик и хороший технический писатель - два отдельных набора навыков. Некоторые могут сделать оба, многие не могут. ;-)
DevSolar
@ v.oddou Бумага не просто плохо написана, это дезинформация. Также это вращение реальности: то, что на самом деле является чрезвычайно сильным аргументом в пользу экспорта, смешано таким образом, что это звучит так, как будто они против экспорта: «обнаружение многочисленных дыр в стандарте ODR при наличии экспорта. Перед экспортом нарушения ODR не должны были диагностироваться компилятором. Теперь это необходимо, потому что вам нужно объединить внутренние структуры данных из разных единиц перевода, и вы не можете объединить их, если они на самом деле представляют разные вещи, поэтому вам нужно выполнить проверку ».
любопытный парень
« Теперь нужно добавить, в каком блоке перевода он находился, когда это произошло ». Когда вы вынуждены использовать аргументы, которые неубедительны, у вас нет никаких аргументов. Конечно, вы будете упоминать имена файлов в своих ошибках, в чем же дело? То, что кто-то влюбляется в это BS, ошеломляет. « Даже экспертам, таким как Джеймс Канзе, трудно принять тот факт, что экспорт действительно таков. » ЧТО? !!!!
любопытный парень
34

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

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

Антон Гоголев
источник
Почему я не могу реализовать их в файле .cpp с ключевым словом "inline"?
MainID
2
Вы можете, и вам не нужно даже помещать «inline». Но вы сможете использовать их только в этом файле cpp и больше нигде.
Вава
10
Это почти самый точный ответ, за исключением того, что «это означает, что эти компиляторы не позволят им быть определенными в файлах без заголовка, таких как файлы .cpp», явно ложно.
Гонки легкости на орбите
28

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

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

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

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

Герман Диаго
источник
15

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

template < typename ... >
class MyClass
{

    int myMethod()
    {
       // Not just declaration. Add method implementation here
    }
};
Benoît
источник
15

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

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

lafrecciablu
источник
1
Мне нравится этот подход за исключением MyInstantiatedTemplate.hфайла и добавленного MyInstantiatedTemplateтипа. Немного чище, если ты им не пользуешься, имхо. Зацените мой ответ на другой вопрос, показывающий это: stackoverflow.com/a/41292751/4612476
Кэмерон Таклинд
Это занимает лучшее из двух миров. Я хотел бы, чтобы этот ответ был оценен выше! Также см. Ссылку выше для более чистой реализации той же идеи.
Вормер
8

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


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;

    ...
}
Nikos
источник
2
Для настоящего мужчины ??? Если это правда, тогда ваш ответ должен быть проверен как верный. Зачем кому-то нужны все эти хакерские вещи вудо, если вы можете просто определить методы, не являющиеся членами шаблона, в .cpp?
Михаил IV
Ну, это не работает. По крайней мере, в MSVC 2019, получение неразрешенного внешнего символа для функции-члена класса шаблона.
Михаил IV
У меня нет MSVC 2019 для тестирования. Это разрешено стандартом C ++. Теперь MSVC печально известен тем, что не всегда придерживается правил. Если вы этого еще не сделали, попробуйте Настройки проекта -> C / C ++ -> Язык -> Режим соответствия -> Да (разрешено-).
Никос
1
Этот точный пример работает, но тогда вы не можете звонить isEmptyиз других переводчиков, кроме myQueue.cpp...
ММ
7

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

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

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

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

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

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

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

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

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

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

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

Моше Рабаев
источник
4

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

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

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

Я предлагаю посмотреть на эту страницу gcc, на которой обсуждаются компромиссы между "cfront" и "borland" моделью для создания шаблонов.

https://gcc.gnu.org/onlinedocs/gcc-4.6.4/gcc/Template-Instantiation.html

Модель "borland" соответствует тому, что предлагает автор, предоставляя полное определение шаблона и составляя объекты несколько раз.

Он содержит четкие рекомендации по использованию ручного и автоматического создания шаблона. Например, опция «-repo» может использоваться для сбора шаблонов, которые необходимо создать. Другой вариант - отключить автоматическое создание экземпляров шаблона с помощью «-fno-implicit-templates», чтобы принудительно создать экземпляр шаблона вручную.

Исходя из своего опыта, я полагаюсь на то, что стандартные библиотеки C ++ и шаблоны Boost создаются для каждого модуля компиляции (с использованием библиотеки шаблонов). Для моих больших шаблонных классов я выполняю ручную инстанцирование шаблонов один раз для нужных мне типов.

Это мой подход, потому что я предоставляю рабочую программу, а не библиотеку шаблонов для использования в других программах. Автор книги, Josuttis, много работает над библиотеками шаблонов.

Если бы я действительно беспокоился о скорости, я полагаю, что я бы изучил использование предварительно скомпилированных заголовков https://gcc.gnu.org/onlinedocs/gcc/Precompiled-Headers.html.

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

Juan
источник
-2

Еще одна причина, по которой в заголовочные файлы рекомендуется записывать как объявления, так и определения, - удобство чтения. Предположим, в 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 в своих собственных классах, хотя компилятор скажет вам об этом случай, о котором оператор должен быть переопределен.

ClarHandsome
источник
-7

Вы можете фактически определить свой шаблонный класс внутри файла .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];
    }
Ni Nisan Nijackle
источник
2
Большинство людей определяют заголовочный файл как что-либо, что распространяет определения в исходные файлы. Таким образом, вы, возможно, решили использовать расширение файла «.template», но вы написали файл заголовка.
Томми