После нескольких недель перерыва я пытаюсь расширить и расширить свои знания о шаблонах с помощью книги « Шаблоны - полное руководство » Дэвида Вандевурда и Николая М. Йосуттиса, и в данный момент я пытаюсь понять явное создание экземпляров шаблонов. .
На самом деле у меня нет проблем с механизмом как таковым, но я не могу представить себе ситуацию, в которой я хотел бы или хотел бы использовать эту функцию. Если кто-нибудь сможет мне это объяснить, я буду более чем благодарен.
Если вы определяете шаблонный класс, вы хотите работать только с парой явных типов.
Поместите объявление шаблона в файл заголовка, как обычный класс.
Поместите определение шаблона в исходный файл, как обычный класс.
Затем в конце исходного файла явно создайте экземпляр только той версии, которую вы хотите сделать доступной.
Глупый пример:
// 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(); }
источник
Явное создание экземпляров позволяет сократить время компиляции и размеры объектов.
Это основные преимущества, которые он может обеспечить. Они происходят из следующих двух эффектов, подробно описанных в следующих разделах:
Удалить определения из заголовков
Явное создание экземпляров позволяет оставлять определения в файле .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
: размер сгенерированного для них кодаМы можем увидеть это немного яснее:
который заканчивается на:
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 template
hpp для типов, экземпляры которых будут явно созданы.Как объяснено в: использование шаблона 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
метод будет работать, поскольку использование проектов будет просто ссылаться на ваш объектный файл, который будет содержать объект явного экземпляра шаблона.Однако для библиотек только заголовков, если вы хотите и то, и другое:
тогда вы можете попробовать одно из следующего:
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:что прямо указывает на то, что это
Dog
был один из экземпляров:0000000000004dac W AnimalTemplate<Dog>::noise[abi:cxx11]() 0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog) 0000000000004d82 W AnimalTemplate<Dog>::AnimalTemplate(Dog)
источник
Это зависит от модели компилятора - видимо есть модель Borland и модель CFront. И тогда это также зависит от вашего намерения - если вы пишете библиотеку, вы можете (как упоминалось выше) явно создать экземпляры нужных вам специализаций.
На странице GNU c ++ обсуждаются модели здесь https://gcc.gnu.org/onlinedocs/gcc-4.5.2/gcc/Template-Instantiation.html .
источник