Чому шаблони можуть бути реалізовані лише у файлі заголовка?


1776

Цитата зі стандартної бібліотеки C ++: навчальний посібник та посібник :

Наразі єдиний портативний спосіб використання шаблонів - це реалізувати їх у файлах заголовків, використовуючи вбудовані функції.

Чому це?

(Уточнення: файли заголовків - не єдине портативне рішення. Але вони є найбільш зручним портативним рішенням.)


13
Хоча це правда, що розміщення всіх визначень функцій шаблону у файлі заголовка - це, мабуть, найзручніший спосіб їх використання, все ще не зрозуміло, що "вбудовано" в цю цитату. Для цього не потрібно використовувати вбудовані функції. "Inline" абсолютно нічого спільного з цим не має.
ANT

7
Книга застаріла.
gerardw

1
Шаблон не є функцією, яку можна скласти в байт-код. Це просто шаблон для генерації такої функції. Якщо ви самостійно помістили шаблон у файл * .cpp, компілювати нічого немає. Більше того, експлікаційна інстанція насправді не є шаблоном, а відправною точкою для того, щоб зробити функцію із шаблону, який потрапляє у файл * .obj.
дграт

5
Чи я єдиний, хто вважає, що концепція шаблону скалічена в C ++ через це? ...
DragonGamer

Відповіді:


1557

Caveat: Не потрібно ставити реалізацію у файл заголовка, дивіться альтернативне рішення в кінці цієї відповіді.

У будь-якому випадку, причина вашого коду не працює в тому, що при інстанціюванні шаблона компілятор створює новий клас із заданим аргументом шаблону. Наприклад:

template<typename T>
struct Foo
{
    T bar;
    void doSomething(T param) {/* do stuff using T */}
};

// somewhere in a .cpp
Foo<int> f; 

Читаючи цей рядок, компілятор створить новий клас (назвемо його FooInt), який еквівалентний наступному:

struct FooInt
{
    int bar;
    void doSomething(int param) {/* do stuff using int */}
}

Отже, компілятору необхідно мати доступ до реалізації методів, щоб інстанціювати їх аргументом шаблону (в даному випадку int). Якби ці реалізації не були в заголовку, вони не були б доступні, і тому компілятор не міг би інстанціювати шаблон.

Поширеним рішенням цього є написання декларації шаблону у файлі заголовка, потім реалізація класу у файлі реалізації (наприклад, .tpp) та включення цього файлу реалізації в кінці заголовка.

Foo.h

template <typename T>
struct Foo
{
    void doSomething(T param);
};

#include "Foo.tpp"

Foo.tpp

template <typename T>
void Foo<T>::doSomething(T param)
{
    //implementation
}

Таким чином, реалізація все ще відокремлена від декларації, але є доступною для компілятора.

Альтернативне рішення

Ще одне рішення - тримати виконання розділеним та чітко створювати всі екземпляри шаблону:

Foo.h

// no implementation
template <typename T> struct Foo { ... };

Foo.cpp

// implementation of Foo's methods

// explicit instantiations
template class Foo<int>;
template class Foo<float>;
// You will only be able to use Foo with int or float

Якщо моє пояснення недостатньо зрозуміле, ви можете ознайомитись із C ++ Super-FAQ на цю тему .


96
Насправді явна інстанція повинна бути у .cpp-файлі, який має доступ до визначень для всіх функцій-членів Foo, а не в заголовку.
Манкарсе

11
"компілятору потрібно мати доступ до реалізації методів, інстанціювати їх аргументом шаблону (в даному випадку int). Якби ці реалізації не були в заголовку, вони не були б доступні". Але чому це реалізація в .cpp файл недоступний компілятору? Компілятор також може отримати доступ до інформації .cpp, як ще вони перетворили б їх у .obj-файли? EDIT: відповідь на це питання знаходиться за посиланням, наведеним у цій відповіді ...
xcrypt

31
Я не думаю, що це пояснює питання, що чітко, ключова річ, очевидно, пов'язана з компіляцією UNIT, про яку не згадується в цьому дописі
zinking

6
@Gabson: структури та класи еквівалентні за винятком того, що модифікатор доступу для класів для класів є "приватним", тоді як він є відкритим для структур. Є деякі інші крихітні відмінності, про які можна дізнатися, переглянувши це питання .
Люк Турайль

3
Я додав речення на самому початку цієї відповіді, щоб уточнити, що питання ґрунтується на помилковій передумові. Якщо хтось запитує "Чому X правдивий?" якщо насправді X не відповідає дійсності, ми повинні швидко відкинути це припущення.
Аарон Макдейд

250

Тут досить правильних відповідей, але я хотів додати це (для повноти):

Якщо ви внизу файлу cpp реалізації реалізуєте явні екземпляри всіх типів, з якими буде використовуватися шаблон, лінкер зможе знайти їх як завжди.

Редагування: додавання прикладу явного опису шаблону. Використовується після того, як шаблон визначений, і всі функції учасника були визначені.

template class vector<int>;

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

Наведений вище приклад є досить марним, оскільки вектор повністю визначений у заголовках, за винятком випадків, коли загальний файл, що включає файл (попередньо складений заголовок?), Використовує extern template class vector<int>так, щоб запобігти його екземпляру у всіх інших (1000?) Файлах, які використовують вектор.


51
Тьфу. Гарна відповідь, але немає справжнього чистого рішення. Перерахування всіх можливих типів шаблону, схоже, не відповідає тому, яким повинен бути шаблон.
Jiminion

6
Це може бути корисним у багатьох випадках, але, як правило, порушує мету шаблону, який покликаний вам використовувати клас з будь-яким, typeне перераховуючи їх вручну.
Томаш Зато - Відновіть Моніку

7
vectorне є хорошим прикладом, тому що контейнер в основному орієнтований на "всі" типи. Але дуже часто буває, що ви створюєте шаблони, призначені лише для певного набору типів, наприклад числових типів: int8_t, int16_t, int32_t, uint8_t, uint16_t тощо. У цьому випадку все-таки є сенс використовувати шаблон , але явне інстанціювання їх для всього набору типів теж можливо і, на мою думку, рекомендується.
UncleZeiv

Використовується після того, як шаблон визначений, "і всі функції учасника були визначені". Дякую !
Вітт Вольт

1
Я відчуваю, що мені щось не вистачає ... Я поміщаю явну інстанцію для двох типів у .cppфайл класу, а ці два екземпляри посилаються з інших .cppфайлів, і я все одно отримую помилку посилання, що члени не знайдені.
морська риба

250

Це пояснюється вимогою окремої компіляції та тим, що шаблони є поліморфізмом у стилі інстанції.

Давайте трохи ближче до конкретного для пояснення. Скажіть, у мене є такі файли:

  • foo.h
    • оголошує інтерфейс class MyClass<T>
  • foo.cpp
    • визначає реалізацію class MyClass<T>
  • bar.cpp
    • використовує MyClass<int>

Окрема компіляція означає, що я повинен мати можливість збирати foo.cpp незалежно від bar.cpp . Компілятор виконує всю важку роботу з аналізу, оптимізації та генерації коду на кожному блоці компіляції повністю незалежно; нам не потрібно робити цілопрограмний аналіз. Тільки лінкер повинен одразу обробляти всю програму, а робота лінкера значно простіша.

bar.cpp навіть не потрібно існувати, коли я компілюю foo.cpp , але я все одно повинен мати можливість пов'язувати foo.o, який я вже мав разом з bar.o, який я тільки що створив, не потребуючи перекомпіляції foo .cpp . foo.cpp можна було навіть скласти в динамічну бібліотеку, поширювати деінде без foo.cpp , і пов'язувати з кодом, який вони пишуть через роки після того, як я написав foo.cpp .

"Поліморфізм у стилі інстантації" означає, що шаблон MyClass<T>насправді не є загальним класом, який можна скласти до коду, який може працювати для будь-якого значення T. Це було б додати накладні витрати , такі як бокс, необхідність передати покажчики на функції для розподільників і конструкторами і т.д. Намір шаблонів C ++, щоб уникнути необхідності писати майже ідентичні class MyClass_int, class MyClass_floatі т.д., але по - , як і раніше бути в змозі закінчити з скомпільованого коду , який головним чином , як якщо б ми були написані кожну версію окремо. Отже, шаблон - це буквально шаблон; шаблон класу - це не клас, це рецепт створення нового класу для кожного, з яким Tми стикаємось. Шаблон не може бути скомпільований у код, може бути зібраний лише результат екземпляра шаблону.

Отже, коли компілюється foo.cpp , компілятор не може бачити bar.cpp, щоб знати, що MyClass<int>потрібно. Він може бачити шаблон MyClass<T>, але він не може видати код для цього (це шаблон, а не клас). І коли компілюється bar.cpp , компілятор може побачити, що йому потрібно створити MyClass<int>, але він не може бачити шаблон MyClass<T>(лише його інтерфейс у foo.h ), тому він не може його створити.

Якщо foo.cpp сам використовує MyClass<int>, то код для цього буде генеруватися під час компіляції foo.cpp , тому коли bar.o пов'язаний з foo.o, вони можуть бути підключені і працюватимуть. Ми можемо використовувати цей факт, щоб дозволити реалізацію скінченного набору шаблонів шаблонів у файлі .cpp, записавши один шаблон. Але немає можливості для bar.cpp використовувати шаблон як шаблон і інстанціювати його на будь-яких типах, які він любить; він може використовувати лише попередньо існуючі версії шаблонного класу, які автор foo.cpp думав надати.

Ви можете подумати, що під час компіляції шаблону компілятор повинен "генерувати всі версії", при цьому ті, які ніколи не використовуються, відфільтровуються під час посилання. Окрім величезних накладних витрат та надзвичайних труднощів, такий підхід зіткнеться, оскільки такі "модифікатори типу", як покажчики та масиви, дозволяють навіть просто вбудованим типам створювати нескінченну кількість типів, що відбувається, коли я зараз розширюю свою програму додавши:

  • baz.cpp
    • декларує та реалізує class BazPrivateта використовуєMyClass<BazPrivate>

Неможливо, щоб це могло працювати, якщо ми не обидва

  1. Доводиться перекомпілювати foo.cpp кожного разу, коли ми змінюємо будь-який інший файл програми , якщо він додав нову інстанціюMyClass<T>
  2. Потрібно, щоб baz.cpp містив (можливо, через заголовок включає) повний шаблон MyClass<T>, щоб компілятор міг генерувати MyClass<BazPrivate>під час компіляції baz.cpp .

Нікому не подобається (1), тому що системи компіляції цілопрограмного аналізу беруть назавжди для компіляції і тому, що це неможливо поширювати складені бібліотеки без вихідного коду. Отже, маємо натомість (2).


50
підкреслена цитата шаблону - це буквально шаблон; шаблон класу - це не клас, це рецепт створення нового класу для кожного Т, з яким ми стикаємось
v.oddou

Мені хотілося б знати, чи можна робити явні інстанції з іншого місця, крім заголовка класу чи вихідного файла? Наприклад, робити їх у main.cpp?
gromit190

1
@Birger Ви повинні бути в змозі зробити це з будь-якого файлу, який має доступ до повної реалізації шаблону (або тому, що він знаходиться в одному файлі або через заголовок).
Бен

11
@ajeh Це не риторика. Питання полягає в тому, "чому вам потрібно реалізувати шаблони в заголовку?", Тому я пояснив технічний вибір, який робить мова C ++, що призводить до цієї вимоги. До того, як я написав свою відповідь, інші вже надали обхідні шляхи, які не є повноцінними рішеннями, оскільки повноцінного рішення не може бути. Я вважав, що ці відповіді будуть доповнені більш повною дискусією "чому" кута питання.
Бен

1
уявіть це таким чином, люди ... якби ви не використовували шаблони (щоб ефективно кодувати те, що вам потрібно), ви все одно пропонували б лише кілька версій цього класу. тож у вас є 3 варіанти. 1). не використовуйте шаблони. (як і всі інші класи / функції, нікого не хвилює, що інші не можуть змінювати типи) 2). використовувати шаблони та документувати, які типи вони можуть використовувати. 3). дайте їм весь бонус за реалізацію (джерело) 4). дайте їм усе джерело на випадок, якщо вони хочуть зробити шаблон з іншого вашого класу;)
Калюжа

81

Компілятор повинен інстанціювати шаблони, перш ніж їх фактично скомпілювати в об'єктний код. Цього обґрунтування можна досягти лише за умови відомих аргументів шаблону. Тепер уявіть сценарій, коли функція шаблону оголошена a.h, визначена в a.cppі використовується в b.cpp. Коли a.cppкомпілюється, не обов’язково відомо, що для майбутньої компіляції b.cppбуде потрібен екземпляр шаблону, не кажучи вже про те, який конкретний екземпляр це був би. Що стосується більше заголовків та вихідних файлів, ситуація може швидко ускладнитися.

Можна стверджувати, що компілятори можуть бути розумнішими, щоб "заглянути вперед" для всіх цілей використання шаблону, але я впевнений, що створити рекурсивний або інший складний сценарій не буде складно. AFAIK, компілятори не виглядають на очах. Як зазначав Антон, деякі компілятори підтримують явні декларації про експорт шаблонів шаблонів, але не всі компілятори підтримують це (поки?).


1
"експорт" є стандартним, але його просто важко здійснити, тому більшість команд-компіляторів ще не зробили цього.
vava

5
експорт не усуває необхідності розкриття джерел, а також не зменшує залежність компіляції, в той час як він вимагає великих зусиль з боку розробників-компіляторів. Тож Герб Саттер попросив будівельників-компіляторів "забути" про експорт. Оскільки потрібні інвестиції в час, краще б витратити в іншому місці ...
Пітер

2
Тому я не думаю, що експорт ще не реалізований. Це, мабуть, ніколи не зробить хтось інший, ніж EDG, після того, як інші побачили, як це триває, і як мало отримано
Пітер

3
Якщо це вас цікавить, документ називається "Чому ми не можемо дозволити собі експорт", він вказаний у його блозі ( gotw.ca/publications ), але там немає жодного PDF-файлу (швидкий google повинен його підняти)
Пітер

1
Добре, дякую за гарний приклад та пояснення. Ось моє питання, хоча: чому компілятор не може з'ясувати, куди викликається шаблон, і скомпілювати ці файли спочатку, перш ніж компілювати файл визначення? Я можу уявити, що це можна зробити в простому випадку ... Чи є відповідь, що взаємозалежності зіпсують порядок досить швидко?
Влад

62

Насправді до C ++ 11 стандарт визначав exportключове слово, яке дозволить оголошувати шаблони у файлі заголовка та реалізовувати їх в іншому місці.

Жоден із популярних компіляторів не реалізував це ключове слово. Єдиний, про який я знаю, - це фронтенд, написаний Edison Design Group, який використовується компілятором Comeau C ++. Усі інші вимагали, щоб ви писали шаблони у файли заголовків, оскільки компілятору потрібне визначення шаблону для належної інстанції (як уже вказували інші).

Як результат, комітет стандартів ISO C ++ вирішив видалити exportособливості шаблонів із C ++ 11.


6
... і через пару років я нарешті зрозумів, що exportнасправді дав би нам, а що ні ... і тепер я від усієї думки погоджуюся з людьми EDG: Це не принесло б нам того, що більшість людей (я в 11 році включено) думаю, що це було б, а стандарт C ++ краще без нього.
DevSolar

4
@DevSolar: цей документ є політичним, повторюваним та погано написаним. це не звичайна стандартна проза рівня. Невпинно довгий і нудний, кажучи в основному 3 рази одних і тих же речей на десятках сторінок. Але мені зараз відомо, що експорт - це не експорт. Це хороший інтел!
v.oddou

1
@ v.oddou: Хороший розробник і хороший технічний автор - два окремі набори навичок. Деякі можуть зробити і те, і інше. ;-)
DevSolar

@ v.oddou Папір не просто погано написана, це дезінформація. Крім того, це віджимання реальності: насправді надзвичайно сильні аргументи щодо експорту змішуються таким чином, щоб це звучало так, ніби вони проти експорту: “виявлення численних дір, пов'язаних з ODR, у стандарті за наявності експорту. Перед експортом компілятор не повинен був діагностувати порушення ODR. Тепер це необхідно, оскільки вам потрібно поєднувати внутрішні структури даних з різних одиниць перекладу, і ви не можете їх поєднувати, якщо вони насправді представляють різні речі, тому вам потрібно зробити перевірку. "
curiousguy

" тепер повинен додати, в якій одиниці перекладу він був, коли це сталося " Да. Коли ви змушені використовувати кульгаві аргументи, у вас немає аргументів. Звичайно, ви збираєтесь згадати імена файлів у своїх помилках, у чому справа? Що хто-небудь потрапляє на те, що BS, це болісно. " Навіть фахівцям, як Джеймс Канзе, важко визнати, що експорт насправді такий ". ЩО? !!!!
curiousguy

34

Хоча стандартний C ++ не вимагає такої вимоги, деякі компілятори вимагають, щоб усі шаблони функцій та класів були доступні для кожного блоку перекладу, який вони використовуються. Фактично для цих компіляторів тіла функцій шаблону повинні бути доступними у файлі заголовка. Повторюся: це означає, що ці компілятори не дозволяють визначати їх у файлах, що не містять заголовків, таких як .cpp-файли

Існує експортне ключове слово, яке повинно пом'якшити цю проблему, але воно ніде не може бути портативним.


Чому я не можу реалізувати їх у .cpp-файлі з ключовим словом "вбудований"?
MainID

2
Ви можете, і вам не доведеться рівномірно ставити "інлійн". Але ви зможете використовувати їх просто у тому файлі cpp і ніде більше.
vava

10
Це майже найточніша відповідь, за винятком того, "що означає, що ці компілятори не дозволять визначати їх у файлах без заголовків, таких як .cpp-файли", явно неправдиві.
Гонки легкості на орбіті

28

Шаблони повинні бути використані в заголовках, оскільки компілятору необхідно створити різні версії коду, залежно від параметрів, заданих / виведених для параметрів шаблону. Пам’ятайте, що шаблон не представляє код безпосередньо, а шаблон для кількох версій цього коду. Коли ви компілюєте не шаблонну функцію у .cppфайлі, ви збираєте конкретну функцію / клас. Це не стосується шаблонів, які можна інстанціювати різними типами, а саме конкретний код повинен видаватися при заміні параметрів шаблону конкретними типами.

Була функція з exportключовим словом, яке повинно було використовуватися для окремої компіляції. Ця exportфункція застаріла в, C++11а AFAIK лише один компілятор реалізував її. Ви не повинні використовувати це export. Окремий збір неможливий уC++ або C++11але , можливо, C++17якщо поняття зробити його, ми могли б мати якийсь - то спосіб роздільної компіляції.

Для досягнення окремої компіляції необхідно проводити окрему перевірку корпусу шаблону. Здається, що з рішеннями можливо рішення. Погляньте на це документ, нещодавно представлений на засіданні комітетів зі стандартів. Я думаю, що це не єдина вимога, оскільки вам все одно потрібно інстанціювати код шаблону в коді користувача.

Окрема проблема компіляції шаблонів. Я думаю, це також проблема, що виникає при переході на модулі, над якими зараз працює.


15

Це означає, що найбільш портативним способом визначення реалізацій методів класів шаблонів є визначення їх усередині визначення класу шаблонів.

template < typename ... >
class MyClass
{

    int myMethod()
    {
       // Not just declaration. Add method implementation here
    }
};

15

Незважаючи на те, що вище є багато хороших пояснень, мені не вистачає практичного способу розділення шаблонів на заголовок та тіло.
Моя головна проблема - уникати перекомпіляції всіх користувачів шаблону, коли я змінюю його визначення.
Наявність усіх екземплярів шаблону в тілі шаблону не є життєздатним рішенням для мене, оскільки автор шаблону може не знати всіх, якщо його використання та користувач шаблону можуть не мати права його змінювати.
Я взяв такий підхід, який працює і для старих компіляторів (gcc 4.3.4, ACC A.03.13).

Для кожного використання шаблону є typedef у власному файлі заголовка (генерується з моделі UML). Її тіло містить інстанцію (яка опиняється в бібліотеці, яка пов'язана в кінці).
Кожен користувач шаблону включає цей заголовок і використовує typedef.

Схематичний приклад:

MyTemplate.h:

#ifndef MyTemplate_h
#define MyTemplate_h 1

template <class T>
class MyTemplate
{
public:
  MyTemplate(const T& rt);
  void dump();
  T t;
};

#endif

MyTemplate.cpp:

#include "MyTemplate.h"
#include <iostream>

template <class T>
MyTemplate<T>::MyTemplate(const T& rt)
: t(rt)
{
}

template <class T>
void MyTemplate<T>::dump()
{
  cerr << t << endl;
}

MyInstantiatedTemplate.h:

#ifndef MyInstantiatedTemplate_h
#define MyInstantiatedTemplate_h 1
#include "MyTemplate.h"

typedef MyTemplate< int > MyInstantiatedTemplate;

#endif

MyInstantiatedTemplate.cpp:

#include "MyTemplate.cpp"

template class MyTemplate< int >;

main.cpp:

#include "MyInstantiatedTemplate.h"

int main()
{
  MyInstantiatedTemplate m(100);
  m.dump();
  return 0;
}

Таким чином, потрібно буде перекомпілювати лише дані шаблонів, а не всі користувачі шаблону (та залежності).


1
Мені подобається такий підхід за винятком MyInstantiatedTemplate.hфайлу та доданого MyInstantiatedTemplateтипу. Це трохи чистіше, якщо ви не користуєтесь цим, imho. Оформити замовлення моя відповідь на інше питання , показуючи це: stackoverflow.com/a/41292751/4612476
Cameron Теклінда

Це займає найкраще з двох світів. Я хочу, щоб ця відповідь була оцінена вище! Також дивіться посилання вище для дещо чіткішого втілення тієї ж ідеї.
Вормер

8

Просто додати тут щось примітне. Можна визначити методи шаблонованого класу просто добре у файлі реалізації, коли вони не є шаблонами функцій.


myQueue.hpp:

template <class T> 
class QueueA {
    int size;
    ...
public:
    template <class T> T dequeue() {
       // implementation here
    }

    bool isEmpty();

    ...
}    

myQueue.cpp:

// implementation of regular methods goes like this:
template <class T> bool QueueA<T>::isEmpty() {
    return this->size == 0;
}


main()
{
    QueueA<char> Q;

    ...
}

2
Для справжнього чоловіка ??? Якщо це правда, то вашу відповідь слід перевірити як правильну. Чому комусь потрібні всі ці хиткі речі з вуду, якщо ви можете просто визначити методи, які не є членами шаблону в .cpp?
Михайло IV

Добре, що не працює. Принаймні, у MSVC 2019, отримання невирішеного зовнішнього символу для функції члена шаблону класу.
Михайло IV

У мене немає MSVC 2019 для тестування. Це дозволено стандартом C ++. Тепер MSVC відомий тим, що не завжди дотримується правил. Якщо ви ще цього не зробите, спробуйте Налаштування проекту -> C / C ++ -> Мова -> Режим відповідності -> Так (дозвільно-).
Нікос

1
Цей точний приклад працює, але тоді ви не можете зателефонувати isEmptyз будь-якого іншого підрозділу перекладу, окрім myQueue.cpp...
MM

7

Якщо проблема викликає додатковий час компіляції та розряд бінарного розміру, що утворюється шляхом компілювання .h як частини всіх .cpp-модулів, що використовують його, у багатьох випадках ви можете зробити так, щоб клас шаблону спустився з базового класу, який не шаблонований для не залежні від типу частини інтерфейсу, і цей базовий клас може мати його реалізацію у файлі .cpp.


2
Цю відповідь слід модифікувати ще більше. Я " незалежно " виявив ваш той самий підхід і спеціально шукав когось іншого, щоб вже його використали, оскільки мені цікаво, чи це офіційний зразок і чи отримав це ім'я. Мій підхід полягає в тому, щоб реалізувати class XBaseтам, де мені потрібно реалізувати template class X, розміщуючи залежні від типу деталі Xта все інше XBase.
Фабіо А.

6

Це абсолютно правильно, оскільки компілятор повинен знати, який тип він призначений для розподілу. Отже, шаблонні класи, функції, перерахунки тощо повинні бути також реалізовані у файлі заголовка, якщо він повинен бути опублікованим або частиною бібліотеки (статичної або динамічної), оскільки файли заголовків НЕ компілюються на відміну від файлів c / cpp, які є. Якщо компілятор не знає, тип не може його скомпілювати. У .Net це може, тому що всі об'єкти походять від класу Object. Це не так.


5
"файли заголовків НЕ компілюються" - це дійсно дивний спосіб його опису. Файли заголовків можуть бути частиною блоку перекладу, як і файл "c / cpp".
Flexo

2
Насправді це майже протилежна істина, яка полягає в тому, що файли заголовків дуже часто збираються багато разів, тоді як вихідний файл зазвичай збирається один раз.
xaxxon

6

Компілятор буде генерувати код для кожної інстанції шаблону, коли ви використовуєте шаблон під час кроку компіляції. У процесі компіляції та зв’язування файли .cpp перетворюються на чистий об'єкт або машинний код, який у них містить посилання або невизначені символи, оскільки файли .h, які включені у ваш main.cpp, не мають реалізації. Вони готові пов’язатись з іншим об’єктним файлом, який визначає реалізацію для вашого шаблону, і таким чином у вас є повний a.out виконуваний файл.

Однак, оскільки шаблони потрібно обробити на етапі компіляції, щоб генерувати код для кожного встановленого вами шаблону шаблону, тому просто компілювати шаблон окремо від його заголовкового файлу не буде працювати, оскільки вони завжди йдуть з рук, з тієї самої причини. що кожне опитування шаблонів - це абсолютно новий клас буквально. У звичайному класі ви можете розділити .h та .cpp, тому що .h - це крок цього класу, а .cpp - це необроблена реалізація, тому будь-які файли реалізації можна збирати та зв’язувати регулярно, однак за допомогою шаблонів .h - креслення того, як клас повинен виглядати не так, як повинен виглядати об’єкт, що означає шаблон .cpp-файл не є необробленою регулярною реалізацією класу, це просто креслення для класу, тому будь-яка реалізація файлу шаблону .h може '

Тому шаблони ніколи не складаються окремо, а збираються лише там, де у вас є конкретні дані в якомусь іншому вихідному файлі. Однак конкретна інстанція повинна знати реалізацію файлу шаблону, тому що просто модифікуючиtypename Tвикористання конкретного типу у файлі .h не збирається виконати роботу, тому що що .cpp є для того, щоб посилатись, я не можу його знайти пізніше, тому що шаблони запам'ятовування є абстрактними і їх неможливо скласти, тому я змушений щоб дати реалізацію прямо зараз, щоб я знав, що потрібно компілювати та посилати, а тепер, коли я маю реалізацію, вона пов'язується з доданим вихідним файлом. В основному, в момент, коли я створюю шаблон, мені потрібно створити цілий новий клас, і я не можу це зробити, якщо не знаю, як повинен виглядати цей клас при використанні наданого типу, якщо я не повідомляю про це компілятора реалізація шаблону, тож тепер компілятор може замінити Tмій тип та створити конкретний клас, готовий до компіляції та зв’язування.

Підсумовуючи, шаблони - це креслення для того, як повинні виглядати класи, класи - креслення для того, як повинен виглядати об’єкт. Я не можу компілювати шаблони окремо від їх конкретної інстанції, оскільки компілятор збирає лише конкретні типи, інакше кажучи, шаблони принаймні на C ++ - це чиста абстракція мови. Ми повинні скасувати абстрактні шаблони, так би мовити, надаючи їм конкретний тип для вирішення, щоб наша абстракція шаблонів могла трансформуватися у звичайний файл класу, і, в свою чергу, він може бути складений нормально. Розмежування шаблону .h-файлу та шаблону .cpp-файла безглуздо. Це безглуздо, тому що розділення .cpp і .h є лише там, де .cpp може бути складено окремо і пов'язано окремо, з шаблонами, оскільки ми не можемо їх скласти окремо, оскільки шаблони - це абстракція,

Значення typename Tзамінюється get під час кроку компіляції, а не кроком посилання, тому якщо я спробую скомпілювати шаблон, не Tбудучи заміненим як тип конкретного значення, що абсолютно безглуздо для компілятора, і в результаті об'єктний код не може бути створений, оскільки він не знайте, що Tтаке.

Технічно можливо створити якийсь функціонал, який збереже файл template.cpp і вимкне типи, коли він знайде їх в інших джерелах, я думаю, що в стандарті є ключове слово, exportяке дозволить вам розмістити шаблони в окремі файл cpp, але не дуже багато компіляторів реально реалізують це.

Лише бічна примітка, коли виробляються спеціалізації для шаблонного класу, ви можете відокремити заголовок від реалізації, оскільки спеціалізація за визначенням означає, що я спеціалізуюся на конкретному типі, який можна складати та зв’язувати окремо.


4

Спосіб окремої реалізації полягає в наступному.

//inner_foo.h

template <typename T>
struct Foo
{
    void doSomething(T param);
};


//foo.tpp
#include "inner_foo.h"
template <typename T>
void Foo<T>::doSomething(T param)
{
    //implementation
}


//foo.h
#include <foo.tpp>

//main.cpp
#include <foo.h>

inner_foo має прямі декларації. foo.tpp має реалізацію і включає Internal_foo.h; і foo.h матиме лише один рядок, щоб включити foo.tpp.

Під час компіляції вміст foo.h копіюється у foo.tpp, а потім весь файл копіюється у foo.h, після чого він компілюється. Таким чином, обмежень немає, і найменування є послідовним, в обмін на один додатковий файл.

Я роблю це тому, що статичні аналізатори для коду ламаються, коли він не бачить прямих декларацій класу в * .tpp. Це дратує при написанні коду в будь-якому IDE або використанні YouCompleteMe або інших.


2
s / inner_foo / foo / g і включають foo.tpp в кінці foo.h. На один файл менше.

1

Я пропоную переглянути цю сторінку gcc, яка обговорює компроміси між моделлю "cfront" і "borland" для шаблонів шаблонів.

https://gcc.gnu.org/onlinedocs/gcc-4.6.4/gcc/Template-Instantiation.html

Модель "borland" відповідає тому, що пропонує автор, надаючи повне визначення шаблону та збираючи речі кілька разів.

Він містить чіткі рекомендації щодо використання вручну та автоматичного опису шаблону. Наприклад, параметр "-repo" може використовуватися для збору шаблонів, які потрібно інстанціювати. Або інший варіант - відключити автоматичні екземпляри шаблонів, використовуючи "-fno-implicit-templates", щоб примусити інстанціювати шаблони вручну.

На моєму досвіді я покладаюсь на те, що шаблони стандартної бібліотеки C і Boost (С ++) і інстанціюються для кожного компіляційного блоку (використовуючи бібліотеку шаблонів). Для моїх великих класів шаблонів я одноразово роблю вручну шаблони шаблонів для потрібних мені типів.

Це мій підхід, оскільки я надаю робочу програму, а не бібліотеку шаблонів для використання в інших програмах. Автор книги Йосуттіс багато працює над бібліотеками шаблонів.

Якби я справді хвилювався з приводу швидкості, я вважаю, що буду досліджувати за допомогою попередньо складених заголовків https://gcc.gnu.org/onlinedocs/gcc/Precompiled-Headers.html

яка набуває підтримки у багатьох компіляторів. Однак я думаю, що попередньо складені заголовки складно матимуть файли заголовків шаблонів.


-2

Ще одна причина того, що добре писати як декларації, так і визначення у файли заголовків, - це читабельність. Припустимо, у Utility.h є така функція шаблону:

template <class T>
T min(T const& one, T const& theOther);

І в Utility.cpp:

#include "Utility.h"
template <class T>
T min(T const& one, T const& other)
{
    return one < other ? one : other;
}

Для цього потрібно, щоб кожен клас T тут реалізував менше оператора (<). Це призведе до помилки компілятора, коли ви порівнюєте два екземпляри класу, які не реалізували "<".

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


-7

Ви можете фактично визначити свій клас шаблонів у файлі .template, а не у файлі .cpp. Хто б не сказав, що ви можете визначити його лише у файлі заголовка, це неправильно. Це те, що працює на шляху до c ++ 98.

Не забудьте, щоб ваш компілятор ставився до файлу .template як до файлу c ++, щоб зберегти інтелігентний сенс.

Ось приклад цього для класу динамічного масиву.

#ifndef dynarray_h
#define dynarray_h

#include <iostream>

template <class T>
class DynArray{
    int capacity_;
    int size_;
    T* data;
public:
    explicit DynArray(int size = 0, int capacity=2);
    DynArray(const DynArray& d1);
    ~DynArray();
    T& operator[]( const int index);
    void operator=(const DynArray<T>& d1);
    int size();

    int capacity();
    void clear();

    void push_back(int n);

    void pop_back();
    T& at(const int n);
    T& back();
    T& front();
};

#include "dynarray.template" // this is how you get the header file

#endif

Тепер всередині вас .template файл ви визначаєте свої функції саме так, як зазвичай.

template <class T>
DynArray<T>::DynArray(int size, int capacity){
    if (capacity >= size){
        this->size_ = size;
        this->capacity_ = capacity;
        data = new T[capacity];
    }
    //    for (int i = 0; i < size; ++i) {
    //        data[i] = 0;
    //    }
}

template <class T>
DynArray<T>::DynArray(const DynArray& d1){
    //clear();
    //delete [] data;
    std::cout << "copy" << std::endl;
    this->size_ = d1.size_;
    this->capacity_ = d1.capacity_;
    data = new T[capacity()];
    for(int i = 0; i < size(); ++i){
        data[i] = d1.data[i];
    }
}

template <class T>
DynArray<T>::~DynArray(){
    delete [] data;
}

template <class T>
T& DynArray<T>::operator[]( const int index){
    return at(index);
}

template <class T>
void DynArray<T>::operator=(const DynArray<T>& d1){
    if (this->size() > 0) {
        clear();
    }
    std::cout << "assign" << std::endl;
    this->size_ = d1.size_;
    this->capacity_ = d1.capacity_;
    data = new T[capacity()];
    for(int i = 0; i < size(); ++i){
        data[i] = d1.data[i];
    }

    //delete [] d1.data;
}

template <class T>
int DynArray<T>::size(){
    return size_;
}

template <class T>
int DynArray<T>::capacity(){
    return capacity_;
}

template <class T>
void DynArray<T>::clear(){
    for( int i = 0; i < size(); ++i){
        data[i] = 0;
    }
    size_ = 0;
    capacity_ = 2;
}

template <class T>
void DynArray<T>::push_back(int n){
    if (size() >= capacity()) {
        std::cout << "grow" << std::endl;
        //redo the array
        T* copy = new T[capacity_ + 40];
        for (int i = 0; i < size(); ++i) {
            copy[i] = data[i];
        }

        delete [] data;
        data = new T[ capacity_ * 2];
        for (int i = 0; i < capacity() * 2; ++i) {
            data[i] = copy[i];
        }
        delete [] copy;
        capacity_ *= 2;
    }
    data[size()] = n;
    ++size_;
}

template <class T>
void DynArray<T>::pop_back(){
    data[size()-1] = 0;
    --size_;
}

template <class T>
T& DynArray<T>::at(const int n){
    if (n >= size()) {
        throw std::runtime_error("invalid index");
    }
    return data[n];
}

template <class T>
T& DynArray<T>::back(){
    if (size() == 0) {
        throw std::runtime_error("vector is empty");
    }
    return data[size()-1];
}

template <class T>
T& DynArray<T>::front(){
    if (size() == 0) {
        throw std::runtime_error("vector is empty");
    }
    return data[0];
    }

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