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

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

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

Почему это?

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

Ответ 1

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

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

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

Ответ 2

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

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

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

template class vector<int>;

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

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

Ответ 3

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

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

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

Ответ 4

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

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

Ответ 5

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

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

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

Ответ 6

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

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

Ответ 7

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

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

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

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

Ответ 8

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

template < typename ... >
class MyClass
{

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

Ответ 9

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

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

Ответ 10

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

Ответ 11

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

Ответ 12

Компилятор будет генерировать код для каждого экземпляра шаблона при использовании шаблона на этапе компиляции. В процессе компиляции и компоновки файлы .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 T заменяется на этапе компиляции, а не на этапе компоновки, поэтому, если я попытаюсь скомпилировать шаблон без замены T в качестве конкретного типа значения, поэтому он не будет работать, поскольку определение шаблонов во время компиляции процесс, и, кстати, метапрограммирование все об использовании этого определения.

Ответ 13

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

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

Ответ 14

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


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;

    ...
}

Ответ 15

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

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

Ответ 16

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

Ответ 17

На самом деле вы можете определить свой класс шаблона внутри файла .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];
    }