Розбиття шаблонованих класів C ++ на файли .hpp / .cpp - чи можливо?


96

Я отримую помилки при спробі скомпілювати клас шаблону C ++, який розділений між a .hppта .cppфайлом:

$ g++ -c -o main.o main.cpp  
$ g++ -c -o stack.o stack.cpp   
$ g++ -o main main.o stack.o  
main.o: In function `main':  
main.cpp:(.text+0xe): undefined reference to 'stack<int>::stack()'  
main.cpp:(.text+0x1c): undefined reference to 'stack<int>::~stack()'  
collect2: ld returned 1 exit status  
make: *** [program] Error 1  

Ось мій код:

stack.hpp :

#ifndef _STACK_HPP
#define _STACK_HPP

template <typename Type>
class stack {
    public:
            stack();
            ~stack();
};
#endif

stack.cpp :

#include <iostream>
#include "stack.hpp"

template <typename Type> stack<Type>::stack() {
        std::cerr << "Hello, stack " << this << "!" << std::endl;
}

template <typename Type> stack<Type>::~stack() {
        std::cerr << "Goodbye, stack " << this << "." << std::endl;
}

main.cpp :

#include "stack.hpp"

int main() {
    stack<int> s;

    return 0;
}

ldце, звичайно, правильно: символи відсутні stack.o.

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

Чи є єдиним розумним рішенням перемістити все, що знаходиться у .cppфайлі, у .hppфайл, і просто включити все, а не зв’язувати як самостійний об’єктний файл? Це здається жахливо потворним! У такому випадку я міг би також повернутися до свого попереднього стану та перейменувати stack.cppйого stack.hppта закінчити з ним.


Є два чудові способи вирішення того, коли ви хочете по-справжньому зберегти свій код прихованим (у двійковому файлі) або зберегти його чистим. Це потрібно для зменшення загальності, хоча в першій ситуації. Це пояснюється тут: stackoverflow.com/questions/495021/…
Sheric

Відповіді:


151

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

Щоб знати чому, давайте розглянемо процес складання. Файли заголовків ніколи не компілюються. Вони лише попередньо оброблені. Попередньо оброблений код клабується з файлом cpp, який фактично скомпільовано. Тепер, якщо компілятор повинен сформувати відповідний макет пам’яті для об’єкта, йому потрібно знати тип даних класу шаблону.

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

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

Сподіваюсь, ця дискусія буде корисною.


2
"треба розуміти, що шаблон-клас - це зовсім не клас" - чи не було навпаки? Шаблон класу - це шаблон. "Шаблонний клас" іноді використовується замість "створення шаблону", і він буде фактичним класом.
Xupicor

Тільки для довідки, неправильно стверджувати, що обхідних шляхів немає! Відокремлення структур даних від методів також є поганою ідеєю, оскільки їй протиставляється інкапсуляція. Існує чудове обхідне рішення, яке ви можете використовувати в деяких ситуаціях (я вважаю, що більшість) тут: stackoverflow.com/questions/495021/…
Sheric

@Xupicor, ти маєш рацію. Технічно "Шаблон класу" - це те, що ви пишете, щоб ви могли створити екземпляр "Шаблону класу" та відповідного йому об'єкта. Однак я вважаю, що у загальній термінології використання обох термінів як взаємозамінних не було б не так вже й неправильним, синтаксис визначення самого «Шаблону класу» починається зі слова «шаблон», а не «класу».
Шарджіт Н.

@Sheric, я не говорив, що обхідних шляхів немає. Насправді все, що доступно, - це лише обхідні шляхи, що імітують поділ інтерфейсу та реалізації у випадку класів шаблонів. Жодне з цих обхідних шляхів не працює без створення конкретного набраного класу шаблону. Це в будь-якому випадку розчиняє всю суть загальності використання шаблонів класів. Відокремлення структур даних від алгоритмів - це не те саме, що відокремлення структур даних від методів. Класи структури даних можуть цілком мати такі методи, як конструктори, геттери та сетери.
Шарджіт Н.

Найближче, що я щойно знайшов, щоб зробити цю роботу, це використання пари файлів .h / .hpp та #include "filename.hpp" в кінці файлу .h, що визначає ваш клас шаблону. (нижче вашої заключної дужки для визначення класу з крапкою з комою). Це принаймні структурно відокремлює їх по файлу, і це дозволено, оскільки врешті-решт компілятор копіює / вставляє ваш .hpp-код над вашим #include "filename.hpp".
Artorias2718

90

Це є можливим, до тих пір , як ви знаєте , що Instantiations ви будете потребувати.

Додайте наступний код в кінці stack.cpp, і він буде працювати:

template class stack<int>;

Всі нешаблонні методи стека будуть створені за допомогою екземпляра, і крок зв’язування буде працювати нормально.


7
На практиці більшість людей використовують для цього окремий файл cpp - щось на зразок stackinstantiations.cpp.
Неманья Трифунович

@NemanjaTrifunovic, чи можете ви навести приклад того, як би виглядав stackinstantiations.cpp?
qwerty9967

3
Насправді є й інші рішення: codeproject.com/Articles/48575/…
Slepsort 18.03.13

@ Benoît Я отримав помилку про помилку: очікувався некваліфікований ідентифікатор до ';' стек шаблону маркера <int>; Ти знаєш чому? Дякую!
Каміно

3
Власне, правильним є синтаксис template class stack<int>;.
Пол Бальтеску

8

Ви можете зробити це таким чином

// xyz.h
#ifndef _XYZ_
#define _XYZ_

template <typename XYZTYPE>
class XYZ {
  //Class members declaration
};

#include "xyz.cpp"
#endif

//xyz.cpp
#ifdef _XYZ_
//Class definition goes here

#endif

Про це йшлося в Daniweb

Також у FAQ, але з використанням ключового слова експорту C ++.


5
includeвведення cppфайлу, як правило, жахлива ідея. навіть якщо у вас є поважна причина для цього, файлу - який насправді є просто прославленим заголовком - слід надати hppабо інше розширення (наприклад tpp), щоб дуже чітко зрозуміти, що відбувається, усунути плутанину навколо makefiles, націлених на фактичні cpp файли тощо.
underscore_d

@underscore_d Не могли б ви пояснити, чому включення .cppфайлу - це жахлива ідея?
Аббас,

1
@Abbas, оскільки розширення cpp(або cc, або c, або що завгодно) вказує на те, що файл є частиною реалізації, що отриманий блок перекладу (вихід препроцесора) можна компілювати окремо і що вміст файлу компілюється лише один раз. це не означає, що файл є частиною інтерфейсу, яку можна багаторазово використовувати і довільно включити в будь-яке місце. #includeстворення реального cpp файлу швидко заповнить ваш екран багатьма помилками у визначенні, і це правильно. у цьому випадку, оскільки на це є причина #include, cppбув просто неправильний вибір розширення.
underscore_d

@underscore_d Отже, в основному неправильно просто використовувати .cppрозширення для такого використання. Але використовувати інше висловлювання .tpp- цілком добре, що служило б тій самій меті, але використовувало б інше розширення для легшого / швидшого розуміння?
Аббас,

1
@Abbas Так, cpp/ cc/ і т.д. слід уникати, але це хороша ідея використовувати що - то інше , ніж hpp- наприклад tpp, tccі так далі - так що ви можете використовувати решту файлу і вказати , що tppфайл, хоча він діє як заголовок, утримує позалінійну реалізацію оголошень шаблонів у відповідному hpp. Отже, ця публікація починається з хорошої передумови - розділення оголошень та визначень на 2 різні файли, які легше переглядати / grep або іноді потрібні через кругові залежності IME - але потім закінчується погано, припускаючи, що 2-й файл має неправильне розширення
underscore_d

6

Ні, це неможливо. Не обійшлося і без exportключового слова, яке насправді насправді не існує.

Найкраще, що ви можете зробити, - це помістити реалізації своїх функцій у файл ".tcc" або ".tpp" та #include файл .tcc в кінці вашого .hpp-файлу. Однак це просто косметика; це все одно, як реалізувати все у файлах заголовків. Це просто ціна, яку ви платите за використання шаблонів.


3
Ваша відповідь неправильна. Ви можете генерувати код із класу шаблону у файлі cpp, якщо ви знаєте, які аргументи шаблону використовувати. Дивіться мою відповідь для отримання додаткової інформації.
Бенуа

2
Правда, але це пов’язано з серйозним обмеженням необхідності оновлювати файл .cpp та перекомпілювати кожен раз, коли вводиться новий тип, який використовує шаблон, що, мабуть, не те, про що мав на увазі OP.
Чарльз Сальвія

3

Я вважаю, що є дві основні причини для спроби розділити шаблонний код на заголовок та cpp:

Один - для простої елегантності. Ми всі любимо писати код, який не можна читати, керувати ним і який можна використовувати повторно пізніше.

Іншим є скорочення часу компіляції.

В даний час я (як завжди) кодую програмне забезпечення для моделювання спільно з OpenCL, і ми любимо зберігати код, щоб його можна було запускати, використовуючи типи float (cl_float) або подвійні (cl_double), залежно від можливостей HW. Зараз це робиться за допомогою #define REAL на початку коду, але це не дуже елегантно. Зміна бажаної точності вимагає перекомпіляції програми. Оскільки реальних типів часу виконання не існує, нам поки що доводиться жити з цим. На щастя, ядра OpenCL компілюються під час виконання, і простий sizeof (REAL) дозволяє нам відповідно змінити час виконання коду ядра.

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


2

Тільки якщо ви #include "stack.cppв кінці stack.hpp. Я б рекомендував такий підхід лише в тому випадку, якщо реалізація відносно велика, і якщо ви перейменуєте файл .cpp на інше розширення, щоб відрізнити його від звичайного коду.


4
Якщо ви робите це, ви захочете додати #ifndef STACK_CPP (та друзів) до свого файлу stack.cpp.
Стівен Ньюелл

Побийте мене на цю пропозицію. Я теж не віддаю перевагу такому підходу з міркувань стилю.
luke

2
Так, у такому випадку другому файлу точно не слід надавати розширення cpp( ccабо будь-яке інше), оскільки це суттєвий контраст із його справжньою роллю. Натомість йому слід надати інше розширення, яке вказує, що це (A) заголовок і (B) заголовок, який слід включити в нижній частині іншого заголовка. Я використовую tppдля цього, які зручніше також може стояти tем pпізно їм plementation (через лінії визначення). Докладніше про це я доклав тут: stackoverflow.com/questions/1724036/…
underscore_d

2

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


+1 - навіть незважаючи на те, що в більшості випадків це працює не дуже добре (принаймні, не так часто, як я бажаю)
peterchen

2

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

Також можна експортувати їх через бібліотеки DLL (!), Але досить складно правильно встановити синтаксис (специфічні для MS комбінації __declspec (dllexport) та ключове слово експорту).

Ми використовували це в математичній / географічній бібліотеці, яка шаблонувала подвійне / плаваюче, але мала досить багато коду. (У той час я гуглив, не маю цього коду сьогодні.)


2

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

Простий і природний спосіб - це помістити методи у файл заголовка. Але є й інший шлях.

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

новий stack.cpp:

#include <iostream>
#include "stack.hpp"
template <typename Type> stack<Type>::stack() {
        std::cerr << "Hello, stack " << this << "!" << std::endl;
}
template <typename Type> stack<Type>::~stack() {
        std::cerr << "Goodbye, stack " << this << "." << std::endl;
}
static void DummyFunc() {
    static stack<int> stack_int;  // generates the constructor and destructor code
    // ... any other method invocations need to go here to produce the method code
}

8
Вам не потрібна фіктивна функція: Використовуйте 'стек шаблонів <int>;' Це примушує інстанцію шаблону до поточного блоку компіляції. Дуже корисно, якщо ви визначаєте шаблон, але хочете лише кілька конкретних реалізацій у спільній бібліотеці.
Мартін Йорк,

@Martin: включаючи всі функції-члени? Це фантастика. Ви повинні додати цю пропозицію до потоку "приховані можливості C ++".
Марк Ренсом,

@LokiAstari Я знайшов статтю про це на випадок, якщо хтось хоче дізнатись більше: cplusplus.com/forum/articles/14272
Ендрю Ларссон

1

Вам потрібно мати все у файлі hpp. Проблема полягає в тому, що класи фактично не створюються, поки компілятор не побачить, що вони потрібні якомусь ІНШОМУ файлу cpp - тому він повинен мати весь доступний код для компіляції шаблонованого класу на той момент.

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


0

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


0

Інша можливість - зробити щось на зразок:

#ifndef _STACK_HPP
#define _STACK_HPP

template <typename Type>
class stack {
    public:
            stack();
            ~stack();
};

#include "stack.cpp"  // Note the include.  The inclusion
                      // of stack.h in stack.cpp must be 
                      // removed to avoid a circular include.

#endif

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


1
Включений прославлений 2-й заголовок повинен мати принаймні розширення, крім того, cppщоб уникнути плутанини з фактичними вихідними файлами. Загальні пропозиції включають tppі tcc.
underscore_d

0

Ключове слово 'export' - це спосіб відокремити реалізацію шаблону від оголошення шаблону. Це було введено в стандарт C ++ без існуючої реалізації. З часом лише декілька компіляторів насправді його реалізували. Прочитайте детальнішу інформацію в статті Інформування ІТ щодо експорту


1
Це майже відповідь лише на посилання, і це посилання мертве.
underscore_d

0

1) Запам’ятайте, що основною причиною розділення файлів .h та .cpp є приховування реалізації класу як окремо скомпільованого коду Obj, який можна пов’язати з кодом користувача, що включав .h класу.

2) Нешаблонні класи мають усі змінні конкретно і конкретно визначені у файлах .h та .cpp. Отже, компілятор матиме необхідну інформацію про всі типи даних, що використовуються в класі, перед компіляцією / перекладом  генерація об’єкта / машинного коду Класи шаблонів не мають інформації про конкретний тип даних, перш ніж користувач класу створить екземпляр об’єкта, що передає необхідні дані тип:

        TClass<int> myObj;

3) Тільки після цього екземпляра компілятор генерує конкретну версію класу шаблону для відповідності переданим типам даних.

4) Отже, .cpp НЕ МОЖЕ складатись окремо, не знаючи конкретного типу даних користувачів. Отже, він повинен залишатися вихідним кодом в межах “.h”, доки користувач не вкаже необхідний тип даних, тоді його можна сформувати до певного типу даних, а потім скомпілювати


0

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

Деякі корисні відомості можна знайти тут: https://docs.microsoft.com/en-us/cpp/cpp/explicit-instantiation?view=vs-2019

Для того ж вашого прикладу: Stack.hpp

template <class T>
class Stack {

public:
    Stack();
    ~Stack();
    void Push(T val);
    T Pop();
private:
    T val;
};


template class Stack<int>;

stack.cpp

#include <iostream>
#include "Stack.hpp"
using namespace std;

template<class T>
void Stack<T>::Push(T val) {
    cout << "Pushing Value " << endl;
    this->val = val;
}

template<class T>
T Stack<T>::Pop() {
    cout << "Popping Value " << endl;
    return this->val;
}

template <class T> Stack<T>::Stack() {
    cout << "Construct Stack " << this << endl;
}

template <class T> Stack<T>::~Stack() {
    cout << "Destruct Stack " << this << endl;
}

main.cpp

#include <iostream>
using namespace std;

#include "Stack.hpp"

int main() {
    Stack<int> s;
    s.Push(10);
    cout << s.Pop() << endl;
    return 0;
}

Вихід:

> Construct Stack 000000AAC012F8B4
> Pushing Value
> Popping Value
> 10
> Destruct Stack 000000AAC012F8B4

Однак мені такий підхід не зовсім подобається, оскільки це дозволяє додатку вистрілити собі в ногу, передаючи неправильні типи даних до шаблонованого класу. Наприклад, у головній функції ви можете передавати інші типи, які можна неявно перетворити на int, наприклад s.Push (1.2); і це просто погано, на мій погляд.


-3

Я працюю з Visual Studio 2010, якщо ви хочете розділити свої файли на .h та .cpp, включіть заголовок cpp в кінці .h файлу

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