Явное создание шаблона - когда он используется?

95

После нескольких недель перерыва я пытаюсь расширить и расширить свои знания о шаблонах с помощью книги « Шаблоны - полное руководство » Дэвида Вандевурда и Николая М. Йосуттиса, и в данный момент я пытаюсь понять явное создание экземпляров шаблонов. .

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

Мы ничего не можем сделать
источник

Ответы:

67

Непосредственно скопировано с https://docs.microsoft.com/en-us/cpp/cpp/explicit-instantiation :

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

(Например, libstdc ++ содержит явное создание экземпляра std::basic_string<char,char_traits<char>,allocator<char> > (что есть std::string), поэтому каждый раз, когда вы используете функции std::string, один и тот же код функции не нужно копировать в объекты. Компилятору нужно только ссылаться (связывать) их с libstdc ++.)

Kennytm
источник
8
Да, библиотеки CRT MSVC имеют явные экземпляры для всех классов потоков, локалей и строк, специализированные для char и wchar_t. Полученный .lib превышает 5 мегабайт.
Ханс Пассан,
4
Как компилятор узнает, что шаблон был явно создан в другом месте? Разве он не сгенерирует определение класса, потому что оно доступно?
@STing: Если шаблон создан, в таблице символов будет запись об этих функциях.
kennytm
@Kenny: Вы имеете в виду, если он уже создан в том же TU? Я бы предположил, что любой компилятор достаточно умен, чтобы не создавать экземпляры одной и той же специализации более одного раза в одном и том же TU. Я думал, что преимущество явного создания экземпляра (в отношении времени сборки / компоновки) заключается в том, что если специализация (явно) создается в одном TU, она не будет создана в других TU, в которых она используется. Нет?
4
@Kenny: Я знаю о опции GCC для предотвращения неявного создания экземпляров, но это не стандарт. Насколько мне известно, в VC ++ такой возможности нет. Явный инст. всегда рекламируется как улучшение времени компиляции / компоновки (даже Бьярном), но для того, чтобы он служил этой цели, компилятор должен каким-то образом знать, что не следует неявно создавать экземпляры шаблонов (например, с помощью флага GCC), или ему нельзя давать определение шаблона, только декларация. Это правильно? Я просто пытаюсь понять, почему можно использовать явное создание экземпляра (кроме ограничения конкретных типов).
86

Если вы определяете шаблонный класс, вы хотите работать только с парой явных типов.

Поместите объявление шаблона в файл заголовка, как обычный класс.

Поместите определение шаблона в исходный файл, как обычный класс.

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

Глупый пример:

// StringAdapter.h
template<typename T>
class StringAdapter
{
     public:
         StringAdapter(T* data);
         void doAdapterStuff();
     private:
         std::basic_string<T> m_data;
};
typedef StringAdapter<char>    StrAdapter;
typedef StringAdapter<wchar_t> WStrAdapter;

Источник:

// StringAdapter.cpp
#include "StringAdapter.h"

template<typename T>
StringAdapter<T>::StringAdapter(T* data)
    :m_data(data)
{}

template<typename T>
void StringAdapter<T>::doAdapterStuff()
{
    /* Manipulate a string */
}

// Explicitly instantiate only the classes you want to be defined.
// In this case I only want the template to work with characters but
// I want to support both char and wchar_t with the same code.
template class StringAdapter<char>;
template class StringAdapter<wchar_t>;

Основной

#include "StringAdapter.h"

// Note: Main can not see the definition of the template from here (just the declaration)
//       So it relies on the explicit instantiation to make sure it links.
int main()
{
  StrAdapter  x("hi There");
  x.doAdapterStuff();
}
Мартин Йорк
источник
1
Правильно ли сказать, что если компилятор имеет полное определение шаблона (включая определения функций) в данной единице перевода, он будет создавать экземпляр специализации шаблона, когда это необходимо (независимо от того, была ли эта специализация явно создана в другом TU)? То есть, чтобы воспользоваться преимуществами явного создания экземпляра при компиляции / компоновке, нужно только включить объявление шаблона, чтобы компилятор не мог его создать?
1
@ user123456: Возможно, зависит от компилятора. Но более чем вероятно, что это так в большинстве ситуаций.
Мартин Йорк
1
есть ли способ заставить компилятор использовать эту явно созданную версию для типов, которые вы предварительно указали, но затем, если вы попытаетесь создать экземпляр шаблона с «странным / неожиданным» типом, пусть он работает «как обычно», где он просто создает экземпляр шаблона по мере необходимости?
Дэвид Дориа
2
Что было бы хорошей проверкой / тестом, чтобы убедиться, что явные экземпляры действительно используются? Т.е. он работает, но я не совсем уверен, что это не просто создание экземпляров всех шаблонов по запросу.
Дэвид Дориа,
7
Большая часть приведенной выше болтовни в комментариях больше не соответствует действительности, так как c ++ 11: явное объявление экземпляра (шаблон extern) предотвращает неявное создание экземпляра: код, который в противном случае вызвал бы неявное создание экземпляра, должен использовать явное определение экземпляра, предоставленное где-то еще в программа (обычно в другом файле: это можно использовать для сокращения времени компиляции) en.cppreference.com/w/cpp/language/class_template
xaxxon
21

Явное создание экземпляров позволяет сократить время компиляции и размеры объектов.

Это основные преимущества, которые он может обеспечить. Они происходят из следующих двух эффектов, подробно описанных в следующих разделах:

  • удалить определения из заголовков, чтобы инструменты сборки не перестраивали включающие устройства
  • переопределение объекта

Удалить определения из заголовков

Явное создание экземпляров позволяет оставлять определения в файле .cpp.

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

Помещение определений в файлы .cpp имеет обратную сторону: внешние библиотеки не могут повторно использовать шаблон со своими собственными новыми классами, но «Удалить определения из включенных заголовков, но также предоставить шаблоны внешнего API» ниже показывает обходной путь.

См. Конкретные примеры ниже.

Преимущества переопределения объекта: понимание проблемы

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

Это означает много бесполезного использования диска и время компиляции.

Вот конкретный пример, в котором оба main.cppи notmain.cppнеявно определяют MyTemplate<int>из-за его использования в этих файлах.

main.cpp

#include <iostream>

#include "mytemplate.hpp"
#include "notmain.hpp"

int main() {
    std::cout << notmain() + MyTemplate<int>().f(1) << std::endl;
}

notmain.cpp

#include "mytemplate.hpp"
#include "notmain.hpp"

int notmain() { return MyTemplate<int>().f(1); }

mytemplate.hpp

#ifndef MYTEMPLATE_HPP
#define MYTEMPLATE_HPP

template<class T>
struct MyTemplate {
    T f(T t) { return t + 1; }
};

#endif

notmain.hpp

#ifndef NOTMAIN_HPP
#define NOTMAIN_HPP

int notmain();

#endif

GitHub вверх по течению .

Компилируйте и просматривайте символы с помощью nm:

g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o notmain.o notmain.cpp
g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o main.o main.cpp
g++    -Wall -Wextra -std=c++11 -pedantic-errors -o main.out notmain.o main.o
echo notmain.o
nm -C -S notmain.o | grep MyTemplate
echo main.o
nm -C -S main.o | grep MyTemplate

Выход:

notmain.o
0000000000000000 0000000000000017 W MyTemplate<int>::f(int)
main.o
0000000000000000 0000000000000017 W MyTemplate<int>::f(int)

Из man nm, мы видим, что это Wозначает слабый символ, который GCC выбрал, потому что это шаблонная функция. Слабый символ означает, что скомпилированный неявно сгенерированный код MyTemplate<int>был скомпилирован для обоих файлов.

Причина, по которой он не взрывается во время компоновки с несколькими определениями, заключается в том, что компоновщик принимает несколько слабых определений и просто выбирает одно из них, чтобы поместить в окончательный исполняемый файл.

Цифры в выходных данных означают:

  • 0000000000000000: адрес в разделе. Этот ноль объясняется тем, что шаблоны автоматически помещаются в отдельный раздел.
  • 0000000000000017: размер сгенерированного для них кода

Мы можем увидеть это немного яснее:

objdump -S main.o | c++filt

который заканчивается на:

Disassembly of section .text._ZN10MyTemplateIiE1fEi:

0000000000000000 <MyTemplate<int>::f(int)>:
   0:   f3 0f 1e fa             endbr64 
   4:   55                      push   %rbp
   5:   48 89 e5                mov    %rsp,%rbp
   8:   48 89 7d f8             mov    %rdi,-0x8(%rbp)
   c:   89 75 f4                mov    %esi,-0xc(%rbp)
   f:   8b 45 f4                mov    -0xc(%rbp),%eax
  12:   83 c0 01                add    $0x1,%eax
  15:   5d                      pop    %rbp
  16:   c3                      retq

и _ZN10MyTemplateIiE1fEiэто искореженное имя, от MyTemplate<int>::f(int)>которого c++filtрешили не распутывать.

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

Решения проблемы переопределения объекта

Этой проблемы можно избежать, используя явное создание экземпляра и либо:

  • сохраните определение на hpp и добавьте extern templatehpp для типов, экземпляры которых будут явно созданы.

    Как объяснено в: использование шаблона extern (C ++ 11) extern template предотвращает создание экземпляра полностью определенного шаблона модулями компиляции, за исключением нашего явного создания экземпляра. Таким образом, в конечных объектах будет определена только наша явная реализация:

    mytemplate.hpp

    #ifndef MYTEMPLATE_HPP
    #define MYTEMPLATE_HPP
    
    template<class T>
    struct MyTemplate {
        T f(T t) { return t + 1; }
    };
    
    extern template class MyTemplate<int>;
    
    #endif
    

    mytemplate.cpp

    #include "mytemplate.hpp"
    
    // Explicit instantiation required just for int.
    template class MyTemplate<int>;
    

    main.cpp

    #include <iostream>
    
    #include "mytemplate.hpp"
    #include "notmain.hpp"
    
    int main() {
        std::cout << notmain() + MyTemplate<int>().f(1) << std::endl;
    }
    

    notmain.cpp

    #include "mytemplate.hpp"
    #include "notmain.hpp"
    
    int notmain() { return MyTemplate<int>().f(1); }
    

    Обратная сторона:

    • если вы используете библиотеку только для заголовков, вы заставляете внешние проекты выполнять собственное явное создание экземпляров. Если вы не являетесь библиотекой только для заголовков, это решение, вероятно, будет лучшим.
    • если тип шаблона определен в вашем собственном проекте, а не во встроенном подобном int, кажется, что вы вынуждены добавить для него включение в заголовок, предварительного объявления недостаточно: extern template и неполные типы Это увеличивает зависимости заголовков немного.
  • перемещая определение в файл cpp, оставьте только объявление в hpp, т.е. измените исходный пример следующим образом:

    mytemplate.hpp

    #ifndef MYTEMPLATE_HPP
    #define MYTEMPLATE_HPP
    
    template<class T>
    struct MyTemplate {
        T f(T t);
    };
    
    #endif
    

    mytemplate.cpp

    #include "mytemplate.hpp"
    
    template<class T>
    T MyTemplate<T>::f(T t) { return t + 1; }
    
    // Explicit instantiation.
    template class MyTemplate<int>;
    

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

  • сохраните определение на hpp и добавьте extern templateдля каждого включающего устройства:

    mytemplate.cpp

    #include "mytemplate.hpp"
    
    // Explicit instantiation.
    template class MyTemplate<int>;
    

    main.cpp

    #include <iostream>
    
    #include "mytemplate.hpp"
    #include "notmain.hpp"
    
    // extern template declaration
    extern template class MyTemplate<int>;
    
    int main() {
        std::cout << notmain() + MyTemplate<int>().f(1) << std::endl;
    }
    

    notmain.cpp

    #include "mytemplate.hpp"
    #include "notmain.hpp"
    
    // extern template declaration
    extern template class MyTemplate<int>;
    
    int notmain() { return MyTemplate<int>().f(1); }
    

    Обратная сторона: все участники должны добавить в externсвои файлы CPP, что программисты, скорее всего, забудут сделать.

С любым из этих решений nmтеперь содержит:

notmain.o
                 U MyTemplate<int>::f(int)
main.o
                 U MyTemplate<int>::f(int)
mytemplate.o
0000000000000000 W MyTemplate<int>::f(int)

Итак , мы видим только mytemplate.oимеет компиляцию MyTemplate<int>по желанию, в то время как notmain.oи main.oне потому , что Uсредство не определено.

Удалите определения из включенных заголовков, но также предоставьте шаблоны внешнего API в библиотеке только для заголовков

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

Однако для библиотек только заголовков, если вы хотите и то, и другое:

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

тогда вы можете попробовать одно из следующего:

    • mytemplate.hpp: определение шаблона
    • mytemplate_interface.hpp: объявление шаблона соответствует только определениям из mytemplate_interface.hpp, без определений
    • mytemplate.cpp: включать mytemplate.hppи делать явные мгновенные сообщения
    • main.cppи везде в базе кода: включать mytemplate_interface.hpp, а неmytemplate.hpp
    • mytemplate.hpp: определение шаблона
    • mytemplate_implementation.hpp: включает mytemplate.hppи добавляет externк каждому классу, который будет создан
    • mytemplate.cpp: включать mytemplate.hppи делать явные мгновенные сообщения
    • main.cppи везде в базе кода: включать mytemplate_implementation.hpp, а неmytemplate.hpp

Или, возможно, еще лучше для нескольких заголовков: создайте папку intf/ implвнутри своей includes/папки и mytemplate.hppвсегда используйте ее в качестве имени.

mytemplate_interface.hppПодход выглядит следующим образом :

mytemplate.hpp

#ifndef MYTEMPLATE_HPP
#define MYTEMPLATE_HPP

#include "mytemplate_interface.hpp"

template<class T>
T MyTemplate<T>::f(T t) { return t + 1; }

#endif

mytemplate_interface.hpp

#ifndef MYTEMPLATE_INTERFACE_HPP
#define MYTEMPLATE_INTERFACE_HPP

template<class T>
struct MyTemplate {
    T f(T t);
};

#endif

mytemplate.cpp

#include "mytemplate.hpp"

// Explicit instantiation.
template class MyTemplate<int>;

main.cpp

#include <iostream>

#include "mytemplate_interface.hpp"

int main() {
    std::cout << MyTemplate<int>().f(1) << std::endl;
}

Скомпилируйте и запустите:

g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o mytemplate.o mytemplate.cpp
g++ -c -Wall -Wextra -std=c++11 -pedantic-errors -o main.o main.cpp
g++    -Wall -Wextra -std=c++11 -pedantic-errors -o main.out main.o mytemplate.o

Выход:

2

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

Модули C ++ 20

https://en.cppreference.com/w/cpp/language/modules

Я думаю, что эта функция обеспечит наилучшую настройку в будущем, когда она станет доступной, но я еще не проверял ее, потому что она еще не доступна в моем GCC 9.2.1.

Вам все равно придется делать явную инстанциацию, чтобы получить ускорение / экономию на диске, но, по крайней мере, у нас будет разумное решение для «Удалять определения из включенных заголовков, но также предоставлять шаблоны внешнего API», которое не требует копирования чего-либо примерно 100 раз.

Ожидаемое использование (без явного озарения, не уверен, какой будет точный синтаксис, см .: Как использовать явное создание экземпляра шаблона с модулями C ++ 20? ) Должно быть что-то вроде:

helloworld.cpp

export module helloworld;  // module declaration
import <iostream>;         // import declaration
 
template<class T>
export void hello(T t) {      // export declaration
    std::cout << t << std::end;
}

main.cpp

import helloworld;  // import declaration
 
int main() {
    hello(1);
    hello("world");
}

а затем компиляция, указанная на https://quuxplusone.github.io/blog/2019/11/07/modular-hello-world/

clang++ -std=c++2a -c helloworld.cpp -Xclang -emit-module-interface -o helloworld.pcm
clang++ -std=c++2a -c -o helloworld.o helloworld.cpp
clang++ -std=c++2a -fprebuilt-module-path=. -o main.out main.cpp helloworld.o

Итак, из этого мы видим, что clang может извлекать интерфейс шаблона + реализацию в магию helloworld.pcm, которая должна содержать некоторое промежуточное представление источника LLVM: Как шаблоны обрабатываются в модульной системе C ++? что по-прежнему допускает возможность спецификации шаблона.

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

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

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

# List all weak symbols with size only, no address.
find . -name '*.o' | xargs -I{} nm -C --size-sort --radix d '{}' |
  grep ' W ' > nm.log

# Sort by symbol size.
sort -k1 -n nm.log -o nm.sort.log

# Get a repetition count.
uniq -c nm.sort.log > nm.uniq.log

# Find the most repeated/largest objects.
sort -k1,2 -n nm.uniq.log -o nm.uniq.sort.log

# Find the objects that would give you the most gain after refactor.
# This gain is calculated as "(n_occurences - 1) * size" which is
# the size you would gain for keeping just a single instance.
# If you are going to refactor anything, you should start with the ones
# at the bottom of this list. 
awk '{gain = ($1 - 1) * $2; print gain, $0}' nm.uniq.sort.log |
  sort -k1 -n > nm.gains.log

# Total gain if you refactored everything.
awk 'START{sum=0}{sum += $1}END{print sum}' nm.gains.log

# Total size. The closer total gain above is to total size, the more
# you would gain from the refactor.
awk 'START{sum=0}{sum += $1}END{print sum}' nm.log

Мечта: кеш компилятора шаблонов

Я думаю, что окончательным решением было бы, если бы мы могли строить с:

g++ --template-cache myfile.o file1.cpp
g++ --template-cache myfile.o file2.cpp

а затем myfile.oавтоматически повторно использует ранее скомпилированные шаблоны в файлах.

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

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

Я обнаружил, что некоторые IDE, такие как Eclipse, не могут разрешить «список всех используемых экземпляров шаблонов».

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

Но в Eclipse 2020-03 я могу легко перечислить явно созданные экземпляры шаблонов, выполнив поиск Найти все использования (Ctrl + Alt + G) по имени класса, который указывает мне, например, из:

template <class T>
struct AnimalTemplate {
    T animal;
    AnimalTemplate(T animal) : animal(animal) {}
    std::string noise() {
        return animal.noise();
    }
};

кому:

template class AnimalTemplate<Dog>;

Вот демонстрация: https://github.com/cirosantilli/ide-test-projects/blob/e1c7c6634f2d5cdeafd2bdc79bcfbb2057cb04c4/cpp/animal_template.hpp#L15

Другой метод партизанской войны, который вы могли бы использовать вне среды IDE, - это запустить nm -Cпоследний исполняемый файл и ввести имя шаблона с помощью grep:

nm -C main.out | grep AnimalTemplate

что прямо указывает на то, что это Dogбыл один из экземпляров:

0000000000004dac W AnimalTemplate<Dog>::noise[abi:cxx11]()
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)
0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)
Чиро Сантилли 郝海东 冠状 病 六四 事件 法轮功
источник
1

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

На странице GNU c ++ обсуждаются модели здесь https://gcc.gnu.org/onlinedocs/gcc-4.5.2/gcc/Template-Instantiation.html .

DAmann
источник