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

97

Я получаю ошибки при попытке скомпилировать класс шаблона C ++, который разделен между a .hppи .cppfile:

$ 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/…
Сериал
Явное создание экземпляров шаблона - это то, как вы можете сократить время компиляции шаблонов: stackoverflow.com/questions/2351148/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Ответы:

151

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

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

На самом деле следует понимать, что шаблонный класс - это вовсе не класс, а шаблон для класса, объявление и определение которого генерируется компилятором во время компиляции после получения информации о типе данных из аргумента. Пока не удается создать макет памяти, невозможно сгенерировать инструкции для определения метода. Помните, что первым аргументом метода класса является оператор this. Все методы класса преобразуются в отдельные методы с изменением имени и первым параметром в качестве объекта, с которым он работает. Аргумент this фактически сообщает о размере объекта, который в случае использования класса шаблона недоступен для компилятора, если пользователь не создает экземпляр объекта с аргументом допустимого типа. В этом случае, если вы поместите определения методов в отдельный файл cpp и попытаетесь его скомпилировать, сам объектный файл не будет сгенерирован с информацией о классе. Компиляция не завершится ошибкой, она сгенерирует объектный файл, но не создаст никакого кода для класса шаблона в объектном файле. Это причина того, что компоновщик не может найти символы в объектных файлах, и сборка не выполняется.

Какая же альтернатива скрытию важных деталей реализации? Как мы все знаем, основная цель отделения интерфейса от реализации - скрыть детали реализации в двоичной форме. Здесь вы должны разделить структуры данных и алгоритмы. Классы ваших шаблонов должны представлять только структуры данных, а не алгоритмы. Это позволяет скрыть более ценные детали реализации в отдельных библиотеках классов, не основанных на шаблонах, классы внутри которых будут работать с классами шаблонов или просто использовать их для хранения данных. Класс шаблона фактически будет содержать меньше кода для назначения, получения и установки данных. Остальную работу будут выполнять классы алгоритмов.

Я надеюсь, что это обсуждение будет полезным.

Шарджит Н.
источник
2
«надо понимать, что шаблонный класс - это вообще не класс» - разве не наоборот? Шаблон класса - это шаблон. «Класс шаблона» иногда используется вместо «создания экземпляра шаблона» и может быть фактическим классом.
Xupicor 08
Просто для справки, нельзя сказать, что обходных путей нет! Отделение структур данных от методов также является плохой идеей, поскольку этому противопоказана инкапсуляция. Здесь есть отличный обходной путь, который вы можете использовать в некоторых ситуациях (я верю больше всего): stackoverflow.com/questions/495021/…
Sether
@Xupicor, ты прав. Технически «шаблон класса» - это то, что вы пишете, чтобы вы могли создать экземпляр «класса шаблона» и соответствующего ему объекта. Однако я считаю, что в общей терминологии взаимозаменяемое использование обоих терминов не было бы таким уж неправильным, синтаксис для определения самого «Шаблона класса» начинается со слова «шаблон», а не «класс».
Шарджит Н.
@Seric, я не говорил, что обходных путей нет. Фактически, все, что доступно, - это только обходные пути, имитирующие разделение интерфейса и реализации в случае шаблонных классов. Ни один из этих обходных путей не работает без создания экземпляра определенного типизированного класса шаблона. В любом случае это сводит на нет всю универсальность использования шаблонов классов. Отделение структур данных от алгоритмов - это не то же самое, что отделение структур данных от методов. Классы структуры данных вполне могут иметь такие методы, как конструкторы, геттеры и сеттеры.
Шарджит Н.
Самое близкое, что я только что нашел для выполнения этой работы, - это использование пары файлов .h / .hpp и #include «filename.hpp» в конце файла .h, определяющего ваш класс шаблона. (точка с запятой под закрывающей скобкой для определения класса). Это, по крайней мере, структурно разделяет их по файлам и допускается, потому что в конце компилятор копирует / вставляет ваш .hpp-код поверх вашего #include "filename.hpp".
Artorias2718
90

Это является возможным, до тех пор , как вы знаете , что Instantiations вы будете нуждаться.

Добавьте следующий код в конец stack.cpp, и он заработает:

template class stack<int>;

Все нешаблонные методы стека будут созданы, и этап связывания будет работать нормально.

Бенуа
источник
7
На практике большинство людей используют для этого отдельный файл cpp - что-то вроде stackinstantiations.cpp.
Nemanja Trifunovic
@NemanjaTrifunovic, можете ли вы привести пример того, как будет выглядеть stackinstantiations.cpp?
qwerty9967
3
На самом деле есть и другие решения: codeproject.com/Articles/48575/…
sleepsort
@ Benoît Я получил сообщение об ошибке: ожидался неквалифицированный идентификатор перед ';' стек шаблона токена <int>; Ты знаешь почему? Спасибо!
camino
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 ++ export.

Садананд
источник
5
includeсоздание cppфайла - вообще ужасная идея. даже если у вас есть веская причина для этого, файлу - который на самом деле является просто прославленным заголовком - следует дать hppили другое расширение (например tpp), чтобы четко прояснить, что происходит, устранить путаницу вокруг makefileтаргетинга на реальные 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 разных файла, что может быть проще grok / grep или иногда требуется из-за циклических зависимостей IME - но затем заканчивается плохо, предполагая, что второй файл имеет неправильное расширение
underscore_d
6

Нет, это невозможно. Не обошлось и без exportключевого слова, которого на самом деле не существует.

Лучшее, что вы можете сделать, это поместить реализации ваших функций в файл «.tcc» или «.tpp» и # включить файл .tcc в конец файла .hpp. Однако это просто косметика; это все равно, что реализовать все в файлах заголовков. Это просто цена, которую вы платите за использование шаблонов.

Чарльз Сальвия
источник
3
Ваш ответ неверен. Вы можете сгенерировать код из класса шаблона в файле cpp, если знаете, какие аргументы шаблона использовать. См. Мой ответ для получения дополнительной информации.
Бенуа,
2
Верно, но это связано с серьезным ограничением необходимости обновлять файл .cpp и перекомпилировать каждый раз, когда вводится новый тип, который использует шаблон, что, вероятно, не то, что OP имел в виду.
Чарльз Сальвия,
3

Я считаю, что есть две основные причины для попытки разделить шаблонный код на заголовок и cpp:

Один для простой элегантности. Нам всем нравится писать код, который было бы неудобно читать, управлять и который впоследствии можно было бы повторно использовать.

Другое - сокращение времени компиляции.

В настоящее время (как всегда) я использую программное обеспечение для моделирования кодирования в сочетании с OpenCL, и нам нравится сохранять код, чтобы его можно было запускать с использованием типов float (cl_float) или double (cl_double) по мере необходимости, в зависимости от возможностей HW. Сейчас это делается с помощью #define REAL в начале кода, но это не очень элегантно. Изменение желаемой точности требует перекомпиляции приложения. Поскольку реальных типов времени выполнения не существует, нам пока придется жить с этим. К счастью, ядра OpenCL скомпилированы во время выполнения, и простой размер sizeof (REAL) позволяет нам соответствующим образом изменять время выполнения кода ядра.

Гораздо более серьезная проблема заключается в том, что, хотя приложение является модульным, при разработке вспомогательных классов (таких как те, которые предварительно вычисляют константы моделирования) также необходимо использовать шаблоны. Все эти классы появляются по крайней мере один раз в верхней части дерева зависимостей классов, так как последний шаблонный класс Simulation будет иметь экземпляр одного из этих фабричных классов, а это означает, что практически каждый раз, когда я вношу незначительные изменения в фабричный класс, весь программное обеспечение необходимо перестроить. Это очень раздражает, но я не могу найти лучшего решения.

Meteorhead
источник
2

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

лирик
источник
4
Если вы это делаете, вы захотите добавить #ifndef STACK_CPP (и друзей) в свой файл stack.cpp.
Стивен Ньюэлл
Обидите меня на это предложение. Я тоже не предпочитаю этот подход из соображений стиля.
люк
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) и ключевого слова export).

Мы использовали это в библиотеке math / geom, которая использовала шаблоны double / float, но имела довольно много кода. (В то время я искал это в Google, но сегодня у меня нет этого кода.)

Macke
источник
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
Вам не нужна фиктивная функция: используйте 'template stack <int>;' Это вызывает создание экземпляра шаблона в текущем модуле компиляции. Очень полезно, если вы определяете шаблон, но хотите только пару конкретных реализаций в общей библиотеке.
Мартин Йорк
@Martin: включая все функции-члены? Это круто. Вы должны добавить это предложение в ветку «Скрытые возможности C ++».
Марк Рэнсом,
@LokiAstari Я нашел статью об этом на случай, если кто-то захочет узнать больше: cplusplus.com/forum/articles/14272
Эндрю Ларссон
1

У вас должно быть все в файле hpp. Проблема в том, что классы фактически не создаются до тех пор, пока компилятор не увидит, что они нужны для какого-то ДРУГОГО файла cpp, поэтому у него должен быть весь код для компиляции шаблонного класса в это время.

Одна вещь, которую я стараюсь сделать, - это попытаться разделить мои шаблоны на общую, не шаблонную часть (которую можно разделить между cpp / hpp) и часть шаблона для конкретного типа, которая наследует класс без шаблонов.

Аарон
источник
1

Место, где вы, возможно, захотите сделать это, - это когда вы создаете комбинацию библиотеки и заголовка и скрываете реализацию от пользователя. Следовательно, предлагаемый подход заключается в использовании явного создания экземпляров, поскольку вы знаете, что ожидается от вашего программного обеспечения, и можете скрыть реализации.

Некоторая полезная информация находится здесь: 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

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

Шрирам Мурали
источник
Явный конкретный вопрос по
созданию
0

Поскольку шаблоны компилируются по мере необходимости, это вызывает ограничение для многофайловых проектов: реализация (определение) класса или функции шаблона должна находиться в том же файле, что и его объявление. Это означает, что мы не можем отделить интерфейс в отдельном файле заголовка и что мы должны включать интерфейс и реализацию в любой файл, который использует шаблоны.

ChadNC
источник
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», пока пользователь не укажет требуемый тип данных, затем он может быть сгенерирован для определенного типа данных, а затем скомпилирован

Aaron01
источник
-3

Я работаю с Visual Studio 2010, если вы хотите разделить свои файлы на .h и .cpp, включите заголовок cpp в конец файла .h

Ахмад
источник