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

Я получаю ошибки, пытаясь скомпилировать класс шаблонов С++, который разделен между файлом .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 и сделать с ним.

Ответ 1

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

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

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

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

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

Ответ 2

Это возможно, если вы знаете, какие экземпляры вам понадобятся.

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

template class stack<int>;

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

Ответ 3

Вы можете сделать это таким образом

// 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, но используя ключевое слово экспорта С++.

Ответ 4

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

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

Ответ 5

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

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

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

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

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

Ответ 6

Иногда возможно, что большая часть реализации скрыта в файле cpp, если вы можете извлечь общую функциональность foo все параметры шаблона в класс без шаблона (возможно, тип-небезопасно). Тогда заголовок будет содержать вызовы перенаправления этого класса. Аналогичный подход используется при борьбе с проблемой "раздувания шаблона".

Ответ 7

Проблема в том, что шаблон не генерирует фактический класс, это просто шаблон, сообщающий компилятору, как создать класс. Вам нужно создать конкретный класс.

Простым и естественным способом является использование методов в файле заголовка. Но есть и другой способ.

В вашем .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

Если вы знаете, с какими типами будет использоваться ваш стек, вы можете создать их экземпляр в файле cpp и сохранить там все соответствующие коды.

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

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

Ответ 9

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

Ответ 10

Это довольно старый вопрос, но я думаю, что интересно посмотреть презентацию Артура О'Двайера на cppcon 2016. Хорошее объяснение, много предметов, которые должны быть рассмотрены, должны смотреть.

Ответ 11

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

Ответ 12

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

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

Ответ 13

Другая возможность - сделать что-то вроде:

#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

Мне не нравится это предложение в стиле, но оно может подойдет вам.

Ответ 14

Ключевое слово 'export' - это способ отделить реализацию шаблона от объявления шаблона. Это было введено в стандарте С++ без существующей реализации. В свое время только несколько компиляторов фактически реализовали его. Прочтите подробную информацию на Сообщите ИТ-статью об экспорте

Ответ 15

1) Помните, что основная причина для разделения файлов .h и .cpp заключается в том, чтобы скрыть реализацию класса как код скомпилированного Obj, который может быть связан с кодом пользователя, который включает в себя .h класса.

2) Классы без шаблонов имеют все переменные, конкретно и конкретно определенные в файлах .h и .cpp. Таким образом, компилятор будет иметь информацию о всех типах данных, используемых в классе, перед компиляцией/переводом  генерирование кода объекта/машины Классы шаблонов не имеют информации о конкретном типе данных до того, как пользователь класса создаст объект, передающий требуемый тип данных:

        TClass<int> myObj;

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

4) Поэтому .cpp НЕ МОЖЕТ компилироваться отдельно, не зная тип данных, специфичных для пользователей. Поэтому он должен оставаться исходным кодом внутри ".h", пока пользователь не укажет требуемый тип данных, тогда он может быть сгенерирован для определенного типа данных, а затем скомпилирован

Ответ 16

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