Разделение шаблонных классов C ++ на файлы .hpp / .cpp - возможно ли это? - PullRequest
85 голосов
/ 12 ноября 2009

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

Ответы [ 15 ]

137 голосов
/ 27 января 2010

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

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

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

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

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

81 голосов
/ 12 ноября 2009

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

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

template class stack<int>;

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

8 голосов
/ 03 апреля 2012

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

// 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 ++.

6 голосов
/ 12 ноября 2009

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

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

3 голосов
/ 09 ноября 2012

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

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

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

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

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

2 голосов
/ 12 ноября 2009

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

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

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

2 голосов
/ 12 ноября 2009

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

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

Если в вашем файле .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
}
2 голосов
/ 12 ноября 2009

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

1 голос
/ 12 ноября 2009

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

1 голос
/ 12 ноября 2009

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...