Использование внешнего шаблона (C++11)

Рисунок 1: шаблоны функций

TemplHeader.h

template<typename T>
void f();

TemplCpp.cpp

template<typename T>
void f(){
   //...
}    
//explicit instantation
template void f<T>();

main.cpp

#include "TemplHeader.h"
extern template void f<T>(); //is this correct?
int main() {
    f<char>();
    return 0;
}

Это правильный способ использования extern templateили я использую это ключевое слово только для шаблонов классов, как на рисунке 2?

Рисунок 2: шаблоны классов

TemplHeader.h

template<typename T>
class foo {
    T f();
};

TemplCpp.cpp

template<typename T>
void foo<T>::f() {
    //...
}
//explicit instantation
template class foo<int>;

main.cpp

#include "TemplHeader.h"
extern template class foo<int>();
int main() {
    foo<int> test;
    return 0;
}

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

Кроме того, рисунки 1 и 2 могут быть расширены до решения, в котором шаблоны находятся в одном заголовочном файле. В этом случае нам нужно использовать extern template Ключевое слово, чтобы избежать нескольких одинаковых экземпляров. Это только для классов или функций тоже?

4 ответа

Решение

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

Например:

// header.h

template<typename T>
void ReallyBigFunction()
{
    // Body
}

// source1.cpp

#include "header.h"
void something1()
{
    ReallyBigFunction<int>();
}

// source2.cpp

#include "header.h"
void something2()
{
    ReallyBigFunction<int>();
}

Это приведет к созданию следующих объектных файлов:

source1.o
    void something1()
    void ReallyBigFunction<int>()    // Compiled first time

source2.o
    void something2()
    void ReallyBigFunction<int>()    // Compiled second time

Если оба файла связаны друг с другом, один void ReallyBigFunction<int>() будет отброшен, что приведет к потере времени компиляции и размера объектного файла.

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

изменения source2.cpp чтобы:

// source2.cpp

#include "header.h"
extern template void ReallyBigFunction<int>();
void something2()
{
    ReallyBigFunction<int>();
}

Результатом будут следующие объектные файлы:

source1.o
    void something1()
    void ReallyBigFunction<int>() // compiled just one time

source2.o
    void something2()
    // No ReallyBigFunction<int> here because of the extern

Когда оба они будут связаны друг с другом, второй объектный файл будет просто использовать символ из первого объектного файла. Нет необходимости отбрасывать и не тратить время на компиляцию и размер файла объекта.

Это следует использовать только внутри проекта, например, когда вы используете vector<int> несколько раз, вы должны использовать extern во всех, кроме одного исходного файла.

Это также относится к классам и функциям как единым целым, и даже к функциям-членам шаблона.

Википедия имеет лучшее описание

В C++03 компилятор должен создавать экземпляр шаблона всякий раз, когда в модуле перевода встречается полностью указанный шаблон. Если шаблон создается с одними и теми же типами во многих единицах перевода, это может значительно увеличить время компиляции. В C++03 нет способа предотвратить это, поэтому в C++ 11 введены объявления шаблонов extern, аналогичные объявлениям extern данных.

C++03 имеет такой синтаксис, чтобы обязать компилятор создавать экземпляр шаблона:

  template class std::vector<MyClass>;

C++ 11 теперь обеспечивает этот синтаксис:

  extern template class std::vector<MyClass>;

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

Предупреждение:nonstandard extension used...

В Microsoft VC++ уже несколько лет используется нестандартная версия этой функции (на C++03). Об этом предупреждает компилятор, чтобы избежать проблем переносимости с кодом, который также необходимо компилировать на разных компиляторах.

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

extern template требуется только в том случае, если объявление шаблона завершено

На это намекали в других ответах, но я не думаю, что этому уделялось достаточно внимания.

Это означает, что в примерах OP extern template не имеет никакого эффекта, потому что определения шаблона в заголовках были неполными:

  • void f();: просто декларация, без тела
  • class foo: объявляет метод f() но не имеет определения

Поэтому я бы рекомендовал просто удалить extern template определение в этом конкретном случае: вам нужно только добавить их, если классы полностью определены.

Например:

TemplHeader.h

template<typename T>
void f();

TemplCpp.cpp

template<typename T>
void f(){}

// Explicit instantiation for char.
template void f<char>();

Main.cpp

#include "TemplHeader.h"

// Commented out from OP code, has no effect.
// extern template void f<T>(); //is this correct?

int main() {
    f<char>();
    return 0;
}

компилировать и просматривать символы с помощью nm:

g++ -std=c++11 -Wall -Wextra -pedantic -c -o TemplCpp.o TemplCpp.cpp
g++ -std=c++11 -Wall -Wextra -pedantic -c -o Main.o Main.cpp
g++ -std=c++11 -Wall -Wextra -pedantic -o Main.out Main.o TemplCpp.o
echo TemplCpp.o
nm -C TemplCpp.o | grep f
echo Main.o
nm -C Main.o | grep f

выход:

TemplCpp.o
0000000000000000 W void f<char>()
Main.o
                 U void f<char>()

а затем из man nm Мы видим, что U означает undefined, поэтому определение осталось только на TemplCpp по желанию.

Все это сводится к компромиссу между полными объявлениями заголовков:

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

Дальнейшие их примеры показаны по адресу: Явное создание шаблона - когда оно используется?

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

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

Протестировано в Ubuntu 18.04.

Известной проблемой с шаблонами является раздувание кода, которое является следствием генерации определения класса в каждом модуле, который вызывает специализацию шаблона класса. Чтобы предотвратить это, начиная с C++0x, можно использовать ключевое слово extern перед специализацией шаблона класса.

#include <MyClass> extern class CMyClass<int>;

Явное создание экземпляра класса шаблона должно происходить только в одном модуле перевода, предпочтительно с определением шаблона (MyClass.cpp).

template class CMyClass<int>;
template class CMyClass<float>;

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

Другие вопросы по тегам