Явное создание экземпляров позволяет сократить время компиляции и размеры объектов.
Это основные преимущества, которые он может обеспечить. Они происходят из следующих двух эффектов, подробно описанных в следующих разделах:
- удалить определения из заголовков, чтобы инструменты сборки не перестраивали включающие устройства
- переопределение объекта
Удалить определения из заголовков
Явное создание экземпляров позволяет оставлять определения в файле .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 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"
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; }
template class MyTemplate<int>;
Оборотная сторона: внешние проекты не могут использовать ваш шаблон со своими типами. Также вы вынуждены явно создавать экземпляры всех типов. Но, возможно, это положительный момент, поскольку тогда программисты не забудут.
сохраните определение на hpp и добавьте extern template
для каждого включающего устройства:
mytemplate.cpp
#include "mytemplate.hpp"
template class MyTemplate<int>;
main.cpp
#include <iostream>
#include "mytemplate.hpp"
#include "notmain.hpp"
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 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"
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;
import <iostream>;
template<class T>
export void hello(T t) {
std::cout << t << std::end;
}
main.cpp
import helloworld;
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)