Явна інстанціація дозволяє зменшити час компіляції та розміри об'єктів
Це основні вигоди, які він може забезпечити. Вони походять від наступних двох ефектів, докладно описаних у розділах нижче:
- видалити визначення з заголовків, щоб запобігти відновленню інструментів збірки
- перевизначення об’єкта
Видаліть визначення із заголовків
Явна інстанціація дозволяє залишати визначення у файлі .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 і неповні типи Це збільшує залежності заголовка трохи.
переміщуючи визначення у файл 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
автоматично повторно використовувати раніше скомпільовані шаблони між файлами.
Це означало б додаткові зусилля для програмістів, крім передачі цього додаткового параметра CLI вашій системі складання.
Вторинний бонус явного створення інстанції шаблону: допомога 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)