Явна інстанціація шаблону - коли вона використовується?


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 ++.)


8
Так, бібліотеки CRT MSVC мають явні екземпляри для всіх класів потоків, мов та рядків, спеціалізованих для char та wchar_t. Отриманий файл .lib перевищує 5 мегабайт.
Ганс Пасант

4
Звідки компілятору відомо, що шаблон був явно створений в іншому місці? Чи не буде це просто генерувати визначення класу, оскільки воно доступне?

@STing: Якщо шаблон створений за допомогою екземпляра, ці функції будуть введені в таблицю символів.
kennytm

@Kenny: Ви маєте на увазі, якщо це вже зроблено в тому самому TU? Я б припустив, що будь-який компілятор є достатньо розумним, щоб не створити одну і ту ж спеціалізацію більше одного разу в одному і тому ж TU. Я вважав, що перевага явного створення екземпляра (щодо часу побудови / зв’язування) полягає в тому, що якщо спеціалізація (явно) інстанціюється в одному ТУ, вона не буде інстанційована в інших ТУ, в яких вона використовується. Немає?

4
@Kenny: Я знаю про опцію GCC для запобігання неявної інстанціації, але це не стандарт. Наскільки я знаю, VC ++ не має такої можливості. Явний inst. завжди рекламується як покращення часу компіляції / посилання (навіть Бьярном), але для того, щоб він служив цій меті, компілятор повинен якось знати, щоб не неявно створювати екземпляри шаблонів (наприклад, через прапор 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 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 і неповні типи Це збільшує залежності заголовка трохи.
  • переміщуючи визначення у файл 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автоматично повторно використовувати раніше скомпільовані шаблони між файлами.

Це означало б додаткові зусилля для програмістів, крім передачі цього додаткового параметра 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)

1

Це залежить від моделі компілятора - очевидно, існує модель Борланда та модель CFront. І тоді це також залежить від вашого наміру - якщо ви пишете бібліотеку, ви можете (як зазначено вище) явно створити екземпляр бажаних спеціалізацій.

Сторінка GNU c ++ обговорює моделі тут https://gcc.gnu.org/onlinedocs/gcc-4.5.2/gcc/Template-Instantiation.html .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.