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




явное инстанцирование шаблонов c++ (13)

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

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

Почему это?

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


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

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

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


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

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


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


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

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

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


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

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

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

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

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


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

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


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


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

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


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

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;

    ...
}

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

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

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

template class vector<int>;

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

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


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

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


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

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

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





c++-faq