Збереження визначень функцій шаблону C ++ у файлі .CPP


526

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

.h файл

class foo
{
public:
    template <typename T>
    void do(const T& t);
};

.cpp файл

template <typename T>
void foo::do(const T& t)
{
    // Do something with t
}

template void foo::do<int>(const int&);
template void foo::do<std::string>(const std::string&);

Зауважте останні два рядки - функція шаблону foo :: do використовується лише з ints та std :: рядками, тому ці визначення означають, що додаток буде посилатися.

Моє запитання - це неприємний злом чи це працюватиме з іншими компіляторами / лінкерами? На даний момент я використовую лише цей код з VS2008, але буду бажати порту в інші середовища.


22
Я не уявляв, що це можливо - цікава хитрість! Це могло б допомогти деяким останнім завданням, знаючи це - ура!
xan

69
Що мене заважає - це використання doв якості ідентифікатора: p
Квентін

Я робив щось подібне до gcc, але все ще досліджую
Нік,

16
Це не "хак", це декларація вперед. Це має місце в стандарті мови; так що так, це дозволено в кожному стандартному компіляторі відповідності.
Ахмет Іпкін

1
Що робити, якщо у вас є десятки методів? Чи можете ви просто зробити template class foo<int>;template class foo<std::string>;в кінці файлу .cpp?
Невідомий

Відповіді:


231

Описану вами проблему можна вирішити, визначивши шаблон у заголовку або за допомогою описаного вище підходу.

Рекомендую прочитати наступні пункти з C ++ FAQ Lite :

Вони детально розглядають ці (та інші) шаблонні проблеми.


39
Тільки для доповнення відповіді, посилається посилання відповідає на питання позитивно, тобто можна зробити те, що запропонував Роб, і щоб код був переносним.
ivotron

160
Чи можете ви просто розмістити відповідні частини у самій відповіді? Чому таке посилання навіть дозволено на SO. Я не маю поняття, що шукати в цьому посиланні, оскільки воно сильно змінилося з тих пір.
Ідентифікатор

124

Для інших на цій сторінці цікаво, що таке правильний синтаксис (як і я) для явної спеціалізації шаблону (або принаймні у VS2008), його наступне ...

У вашому .h файлі ...

template<typename T>
class foo
{
public:
    void bar(const T &t);
};

І у вашому .cpp-файлі

template <class T>
void foo<T>::bar(const T &t)
{ }

// Explicit template instantiation
template class foo<int>;

15
Ви маєте на увазі "для явної спеціалізації шаблону CLASS". Чи буде в цьому випадку покривати кожну функцію, яку має шаблоновий клас?
Артур

@Arthur, здається, ні, у мене є деякі шаблонні методи, що знаходяться в заголовку, а більшість інших методів у cpp, добре працює. Дуже приємне рішення.
користувач1633272

У випадку запитувача вони мають шаблон функції, а не шаблон класу.
користувач253751

23

Цей код добре сформований. Вам залишається лише звернути увагу, що визначення шаблону видно в точці інстанції. Для цитування стандарту, § 14.7.2.4:

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


2
Що означає неекспорт ?
Дан Ніссенбаум

1
@Dan Видимий лише всередині свого блоку компіляції, а не поза ним. Якщо ви з’єднаєте кілька одиниць компіляції разом, експортовані символи можуть використовуватися через них (і повинні мати єдине або, принаймні, у випадку шаблонів, послідовних визначень, інакше ви натрапите на UB).
Конрад Рудольф

Дякую. Я подумав, що всі функції (за замовчуванням) видимі поза блоком компіляції. Якщо у мене є два одиниці компіляції a.cpp(визначають функцію a() {}) та b.cpp(визначають функцію b() { a() }), то це буде успішно зв’язаним. Якщо я маю рацію, то, здається, наведена цитата не стосується типового випадку ... чи я десь помиляюся?
Дан Ніссенбаум

@Dan Trivial Counterxample: inlinefunction
Konrad Rudolph

1
@Dan Шаблони функцій неявно inline. Причина полягає в тому, що без стандартизованого C ++ ABI важко / неможливо визначити ефект, який це може мати в іншому випадку.
Конрад Рудольф

15

Це має працювати добре скрізь, коли шаблони підтримуються. Явне описування шаблону є частиною стандарту C ++.


13

Ваш приклад правильний, але не дуже портативний. Існує також трохи більш чистий синтаксис, який можна використовувати (на що вказує @ namespace-sid).

Припустимо, шаблоновий клас є частиною бібліотеки, якою слід ділитися. Чи повинні бути складені інші версії шаблонного класу? Чи повинен підтримувач бібліотеки передбачати всі можливі шаблонні використання класу?

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

файл foo.h

// Standard header file guards omitted

template <typename T>
class foo
{
public:
    void bar(const T& t);
};

файл foo.cpp

// Always include your headers
#include "foo.h"

template <typename T>
void foo::bar(const T& t)
{
    // Do something with t
}

файл foo-impl.cpp

// Yes, we include the .cpp file
#include "foo.cpp"
template class foo<int>;

Одне застереження полягає в тому, що вам потрібно сказати компілятору компілювати foo-impl.cppзамість того foo.cpp, що компіляція останнього не робить нічого.

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

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

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


що це купує у вас? Ще потрібно відредагувати foo-impl.cpp, щоб додати нову спеціалізацію.
МК.

Відокремлення деталей про реалізацію (також визначень у foo.cpp), з яких версії фактично складені (в foo-impl.cpp) та декларації (в foo.h). Мені не подобається, що більшість шаблонів C ++ повністю визначені у файлах заголовків. Це суперечить стандарту C / C ++ пар c[pp]/hдля кожного класу / простору імен / будь-якої групи, яку ви використовуєте. Люди, здається, все ще використовують монолітні файли заголовків просто тому, що ця альтернатива широко не використовується і не відома.
Cameron Tacklind

1
@MK. Я клав явні екземпляри шаблону в кінці визначення спочатку у вихідний файл, поки мені не потрібні подальші інстанції в іншому місці (наприклад, тестові одиниці, що використовують макет як тип шаблону). Цей поділ дозволяє мені додати більше екземплярів зовні. Крім того, він все ще працює, коли я зберігаю оригінал як h/cppпару, хоча мені довелося оточити оригінальний список екземплярів у включенні гвардії, але я все-таки могла скласти foo.cppяк звичайне. Я все ще досить новий в C ++, і мені цікаво дізнатися, чи є в цьому змішаному використанні додаткові застереження.
Третьої води

3
Я вважаю, що краще роз'єднати foo.cppі foo-impl.cpp. Не #include "foo.cpp"у foo-impl.cppфайлі; натомість додайте декларацію, extern template class foo<int>;щоб foo.cppзапобігти інстанціюванню шаблона при компіляції foo.cpp. Переконайтесь, що система збірки будує обидва .cppфайли та передає обидва об'єктні файли в лінкер. Це має багато переваг: а) зрозуміло, foo.cppщо немає жодної інстанції; б) зміни foo.cpp не вимагають перекомпіляції foo-impl.cpp.
Шмуель Левін

3
Це дуже хороший підхід до проблеми визначень шаблонів, яка найкраще використовує обидва світи - реалізацію заголовка та інстанціювання для часто використовуваних типів. Єдина зміна, яку я вніс би в цю установку, - це перейменування foo.cppв foo_impl.hі foo-impl.cppпросто foo.cpp. Я також хотів би додати для конкретизації визначень типів від foo.cppдо foo.h, також using foo_int = foo<int>;. Трюк полягає у наданні користувачам двох інтерфейсів заголовків на вибір. Коли користувачеві потрібна заздалегідь визначена інстанція, він включає foo.h, коли користувачеві потрібно щось не в порядку, він включає foo_impl.h.
Вормер

5

Це, безумовно, не злий хакер, але пам’ятайте про те, що вам доведеться це зробити (явна спеціалізація шаблонів) для кожного класу / типу, який ви хочете використовувати із заданим шаблоном. У випадку СТОЛЬКО типів, що вимагають ідентифікацію шаблону, у вашому .cpp-файлі може бути багато рядків. Щоб вирішити цю проблему, ви можете мати TemplateClassInst.cpp у кожному використаному вами проекті, щоб ви мали більш високий контроль над типами, які будуть інстанційними. Очевидно, що це рішення не буде ідеальним (він же срібна куля), оскільки ви, в кінцевому підсумку, зможете зламати ODR :)


Ви впевнені, що це порушить ODR? Якщо рядки екземплярів у TemplateClassInst.cpp посилаються на ідентичний вихідний файл (що містить визначення функції шаблону), чи не гарантовано це не порушує ODR, оскільки всі визначення однакові (навіть якщо повторюються)?
Dan Nissenbaum

Будь ласка, що таке ОРР?
незнімний

4

В останньому стандарті є ключове слово ( export), яке допоможе полегшити цю проблему, але воно не реалізоване в жодному мені компіляторі, окрім Comeau.

Про це дивіться FAQ-Lite .


2
AFAIK, експорт мертвий, оскільки вони стикаються з новими і новими проблемами, щоразу вирішуючи останні, роблячи загальне рішення все більш складним. І ключове слово "експорт" не дозволить вам "експортувати" з CPP у будь-якому випадку (все-таки з Х. Саттера). Тому я кажу: Не затримуй дихання ...
paercebal

2
Для реалізації експорту для компілятора все ще потрібно повне визначення шаблону. Все, що ви отримуєте, - це отримання їх у своєрідному складеному вигляді. Але насправді в цьому немає сенсу.
Зан Лінкс

2
... і це пішло від стандартного, через надмірне ускладнення для мінімального виграшу.
DevSolar

4

Це стандартний спосіб визначення функцій шаблону. Я думаю, що для визначення шаблонів я читаю три методи. Або, мабуть, 4. Кожен із плюсами і мінусами.

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

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

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

  4. Останній метод, який був розміщеним способом, - це визначення шаблонів у вихідному файлі, як і число 3; але замість того, щоб включати вихідний файл, ми попередньо інстанціюємо шаблони до тих, які нам знадобляться. У мене немає проблем з цим методом, і він корисний іноді. У нас є один великий код, він не може отримати користі від вставки, тому просто покладіть його у файл CPP. І якщо ми знаємо загальні моменти і можемо їх заздалегідь визначити. Це рятує нас від написання однієї і тієї самої речі 5, 10 разів. Цей метод має перевагу збереження нашого коду. Але я не рекомендую додавати крихітні, регулярно використовувані функції у файли CPP. Оскільки це знизить продуктивність вашої бібліотеки.

Зауважте, мені невідомі наслідки роздутого файлу obj.


3

Так, це стандартний спосіб зробити явну інстанціалізацію спеціалізації . Як ви заявили, ви не можете інстанціювати цей шаблон іншими типами.

Редагувати: виправлено на основі коментаря.


Бути прискіпливим до термінології - це "явна інформація".
Річард Корден

2

Візьмемо один приклад, скажімо, з якої причини ви хочете мати шаблон шаблону:

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

template <>
void DemoT<int>::test()
{
    printf("int test (int)\n");
}


template <>
void DemoT<bool>::test()
{
    printf("int test (bool)\n");
}

Якщо ви компілюєте цей код із Visual Studio - він працює нестандартно. gcc призведе до помилки лінкера (якщо один і той самий файл заголовка використовується з декількох .cpp-файлів):

error : multiple definition of `DemoT<int>::test()'; your.o: .../test_template.h:16: first defined here

Можна перенести реалізацію у файл .cpp, але тоді вам потрібно оголосити такий клас, як -

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

template <>
void DemoT<int>::test();

template <>
void DemoT<bool>::test();

// Instantiate parametrized template classes, implementation resides on .cpp side.
template class DemoT<bool>;
template class DemoT<int>;

І тоді .cpp буде виглядати приблизно так:

//test_template.cpp:
#include "test_template.h"

template <>
void DemoT<int>::test()
{
    printf("int test (int)\n");
}


template <>
void DemoT<bool>::test()
{
    printf("int test (bool)\n");
}

Без двох останніх рядків у файлі заголовка - gcc буде добре працювати, але Visual studio видасть помилку:

 error LNK2019: unresolved external symbol "public: void __cdecl DemoT<int>::test(void)" (?test@?$DemoT@H@@QEAAXXZ) referenced in function

Синтаксис шаблону класу необов’язковий у випадку, якщо ви хочете розкрити функцію за допомогою експорту .dll, але це застосовно лише до платформи Windows - тому test_template.h може виглядати так:

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

#ifdef _WIN32
    #define DLL_EXPORT __declspec(dllexport) 
#else
    #define DLL_EXPORT
#endif

template <>
void DLL_EXPORT DemoT<int>::test();

template <>
void DLL_EXPORT DemoT<bool>::test();

з .cpp-файлом з попереднього прикладу.

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


1

Час для оновлення! Створіть вбудований (.inl чи, можливо, будь-який інший) файл та просто скопіюйте у нього всі свої визначення. Обов’язково додайте шаблон над кожною функцією ( template <typename T, ...>). Тепер замість того, щоб включати заголовок у вбудований файл, ви зробите навпаки. Додайте вбудований файл після оголошення вашого класу (#include "file.inl" ).

Я не знаю, чому ніхто про це не згадував. Я не бачу негайних недоліків.


25
Безпосередні недоліки - це принципово те саме, що просто визначати функції шаблону безпосередньо в заголовку. Як тільки ви #include "file.inl", препроцесор збирається вставити вміст file.inlбезпосередньо в заголовок. З якої б причини ви не хотіли уникнути впровадження в заголовку, це рішення не вирішує цю проблему.
Коді Грей

5
- і означає, що ви, технічно невиправдано, обтяжуєте себе завданням написати всю багатослівну, вигинну з розуму котельну табличку, необхідну для позалінійних templateвизначень. Я розумію, чому люди хочуть це зробити - домогтися максимального паритету з нешаблонними деклараціями / визначеннями, щоб декларація інтерфейсу виглядала охайною тощо. Це випадок оцінки компромісів з обох сторін та вибору найменш поганого . ... поки не namespace classстане річчю: O [ будь ласка, річ ]
underscore_d

2
@Andrew Здається, що він застряг у комітетах комітету, хоча, я думаю, я бачив, як хтось сказав, що це не було навмисно. Я б хотів, щоб це перетворилося на C ++ 17. Можливо, наступне десятиліття.
підкреслити_14

@CodyGray: Технічно це компілятор дійсно те саме, і тому не скорочує час компіляції. Я все ж думаю, що це варто згадати і практикувати у ряді проектів, які я бачив. Зниження цієї дороги допомагає відокремити Інтерфейс від визначення, що є хорошою практикою. У цьому випадку це не допомагає сумісності з ABI тощо, але полегшує читання та розуміння інтерфейсу.
kiloalphaindia

0

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

Використовуючи разом із явним інстанціюванням класу, бібліотека перевірки концепції Boost (BCCL) може допомогти вам генерувати код функції шаблону у файлах cpp.


8
Що в цьому неефективне?
Коді Грей

0

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

.h

class Model
{
    template <class T>
    void build(T* b, uint32_t number);
};

.cpp

#include "Model.h"
template <class T>
void Model::build(T* b, uint32_t number)
{
    //implementation
}

void TemporaryFunction()
{
    Model m;
    m.build<B1>(new B1(),1);
    m.build<B2>(new B2(), 1);
    m.build<B3>(new B3(), 1);
}

це дозволяє уникнути помилок у зв’язках і взагалі не потрібно викликати тимчасову функцію

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