Я получаю ошибки при попытке скомпилировать класс шаблона C ++, который разделен между a .hpp
и .cpp
file:
$ 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
и покончить с этим.
Ответы:
Невозможно записать реализацию класса шаблона в отдельный файл cpp и скомпилировать. Все способы сделать это, если кто-то утверждает, являются обходными путями для имитации использования отдельного файла cpp, но практически, если вы намереваетесь написать библиотеку классов шаблонов и распространять ее с файлами заголовков и lib, чтобы скрыть реализацию, это просто невозможно .
Чтобы узнать почему, давайте посмотрим на процесс компиляции. Заголовочные файлы никогда не компилируются. Они только предварительно обработаны. Затем предварительно обработанный код объединяется с файлом cpp, который фактически компилируется. Теперь, если компилятор должен сгенерировать соответствующий макет памяти для объекта, ему необходимо знать тип данных класса шаблона.
На самом деле следует понимать, что шаблонный класс - это вовсе не класс, а шаблон для класса, объявление и определение которого генерируется компилятором во время компиляции после получения информации о типе данных из аргумента. Пока не удается создать макет памяти, невозможно сгенерировать инструкции для определения метода. Помните, что первым аргументом метода класса является оператор this. Все методы класса преобразуются в отдельные методы с изменением имени и первым параметром в качестве объекта, с которым он работает. Аргумент this фактически сообщает о размере объекта, который в случае использования класса шаблона недоступен для компилятора, если пользователь не создает экземпляр объекта с аргументом допустимого типа. В этом случае, если вы поместите определения методов в отдельный файл cpp и попытаетесь его скомпилировать, сам объектный файл не будет сгенерирован с информацией о классе. Компиляция не завершится ошибкой, она сгенерирует объектный файл, но не создаст никакого кода для класса шаблона в объектном файле. Это причина того, что компоновщик не может найти символы в объектных файлах, и сборка не выполняется.
Какая же альтернатива скрытию важных деталей реализации? Как мы все знаем, основная цель отделения интерфейса от реализации - скрыть детали реализации в двоичной форме. Здесь вы должны разделить структуры данных и алгоритмы. Классы ваших шаблонов должны представлять только структуры данных, а не алгоритмы. Это позволяет скрыть более ценные детали реализации в отдельных библиотеках классов, не основанных на шаблонах, классы внутри которых будут работать с классами шаблонов или просто использовать их для хранения данных. Класс шаблона фактически будет содержать меньше кода для назначения, получения и установки данных. Остальную работу будут выполнять классы алгоритмов.
Я надеюсь, что это обсуждение будет полезным.
источник
Это является возможным, до тех пор , как вы знаете , что Instantiations вы будете нуждаться.
Добавьте следующий код в конец stack.cpp, и он заработает:
Все нешаблонные методы стека будут созданы, и этап связывания будет работать нормально.
источник
template class stack<int>;
.Вы можете сделать это таким образом
Это обсуждалось в Daniweb
Также в FAQ, но с использованием ключевого слова C ++ export.
источник
include
созданиеcpp
файла - вообще ужасная идея. даже если у вас есть веская причина для этого, файлу - который на самом деле является просто прославленным заголовком - следует датьhpp
или другое расширение (напримерtpp
), чтобы четко прояснить, что происходит, устранить путаницу вокругmakefile
таргетинга на реальныеcpp
файлы и т. д..cpp
файла - ужасная идея?cpp
(илиcc
, илиc
, или что-то еще) указывает, что файл является частью реализации, что результирующая единица трансляции (вывод препроцессора) компилируется отдельно и что содержимое файла компилируется только один раз. это не означает, что файл является повторно используемой частью интерфейса, которую можно произвольно включить куда-либо.#include
создание реальногоcpp
файла быстро заполнит ваш экран множеством ошибок определения, и это правильно. в данном случае, поскольку на то есть причина#include
,cpp
был просто неправильный выбор расширения..cpp
расширение для такого использования. Но использовать другое слово.tpp
вполне нормально, которое будет служить той же цели, но использовать другое расширение для более простого / быстрого понимания?cpp
/cc
/ и т.д. следует избегать, но рекомендуется использовать что-то иное, чемhpp
- напримерtpp
,tcc
и т.д. - чтобы вы могли повторно использовать остальную часть имени файла и указать, чтоtpp
файл, хотя он действует как заголовок, содержит внешнюю реализацию объявлений шаблона в соответствующемhpp
. Итак, этот пост начинается с хорошей предпосылки - разделения деклараций и определений на 2 разных файла, что может быть проще grok / grep или иногда требуется из-за циклических зависимостей IME - но затем заканчивается плохо, предполагая, что второй файл имеет неправильное расширениеНет, это невозможно. Не обошлось и без
export
ключевого слова, которого на самом деле не существует.Лучшее, что вы можете сделать, это поместить реализации ваших функций в файл «.tcc» или «.tpp» и # включить файл .tcc в конец файла .hpp. Однако это просто косметика; это все равно, что реализовать все в файлах заголовков. Это просто цена, которую вы платите за использование шаблонов.
источник
Я считаю, что есть две основные причины для попытки разделить шаблонный код на заголовок и cpp:
Один для простой элегантности. Нам всем нравится писать код, который было бы неудобно читать, управлять и который впоследствии можно было бы повторно использовать.
Другое - сокращение времени компиляции.
В настоящее время (как всегда) я использую программное обеспечение для моделирования кодирования в сочетании с OpenCL, и нам нравится сохранять код, чтобы его можно было запускать с использованием типов float (cl_float) или double (cl_double) по мере необходимости, в зависимости от возможностей HW. Сейчас это делается с помощью #define REAL в начале кода, но это не очень элегантно. Изменение желаемой точности требует перекомпиляции приложения. Поскольку реальных типов времени выполнения не существует, нам пока придется жить с этим. К счастью, ядра OpenCL скомпилированы во время выполнения, и простой размер sizeof (REAL) позволяет нам соответствующим образом изменять время выполнения кода ядра.
Гораздо более серьезная проблема заключается в том, что, хотя приложение является модульным, при разработке вспомогательных классов (таких как те, которые предварительно вычисляют константы моделирования) также необходимо использовать шаблоны. Все эти классы появляются по крайней мере один раз в верхней части дерева зависимостей классов, так как последний шаблонный класс Simulation будет иметь экземпляр одного из этих фабричных классов, а это означает, что практически каждый раз, когда я вношу незначительные изменения в фабричный класс, весь программное обеспечение необходимо перестроить. Это очень раздражает, но я не могу найти лучшего решения.
источник
Только если вы
#include "stack.cpp
в концеstack.hpp
. Я бы рекомендовал этот подход только в том случае, если реализация относительно велика, и если вы переименуете файл .cpp в другое расширение, чтобы отличить его от обычного кода.источник
cpp
(cc
или что-то еще), потому что это резко контрастирует с его реальной ролью. Вместо этого ему следует дать другое расширение, которое указывает, что это (A) заголовок и (B) заголовок, который должен быть включен внизу другого заголовка. Я используюtpp
для этого, которые сподручно также может стоятьt
эмp
поздно имp
lementation (из-линии определения). Подробнее об этом я рассказал здесь: stackoverflow.com/questions/1724036/…Иногда можно скрыть большую часть реализации в файле cpp, если вы можете извлечь общие функции для всех параметров шаблона в класс, не являющийся шаблоном (возможно, небезопасный по типу). Тогда заголовок будет содержать вызовы перенаправления в этот класс. Аналогичный подход используется и при борьбе с проблемой "раздувания шаблона".
источник
Если вы знаете, с какими типами будет использоваться ваш стек, вы можете явно создать их экземпляры в файле cpp и сохранить там весь соответствующий код.
Их также можно экспортировать в библиотеки DLL (!), Но получить правильный синтаксис довольно сложно (специфичные для MS комбинации __declspec (dllexport) и ключевого слова export).
Мы использовали это в библиотеке math / geom, которая использовала шаблоны double / float, но имела довольно много кода. (В то время я искал это в Google, но сегодня у меня нет этого кода.)
источник
Проблема в том, что шаблон не генерирует реальный класс, это просто шаблон, сообщающий компилятору, как создать класс. Вам нужно создать конкретный класс.
Самый простой и естественный способ - поместить методы в файл заголовка. Но есть другой способ.
Если в вашем файле .cpp есть ссылка на каждый экземпляр шаблона и метод, который вам нужен, компилятор сгенерирует их там для использования в вашем проекте.
новый stack.cpp:
источник
У вас должно быть все в файле hpp. Проблема в том, что классы фактически не создаются до тех пор, пока компилятор не увидит, что они нужны для какого-то ДРУГОГО файла cpp, поэтому у него должен быть весь код для компиляции шаблонного класса в это время.
Одна вещь, которую я стараюсь сделать, - это попытаться разделить мои шаблоны на общую, не шаблонную часть (которую можно разделить между cpp / hpp) и часть шаблона для конкретного типа, которая наследует класс без шаблонов.
источник
Место, где вы, возможно, захотите сделать это, - это когда вы создаете комбинацию библиотеки и заголовка и скрываете реализацию от пользователя. Следовательно, предлагаемый подход заключается в использовании явного создания экземпляров, поскольку вы знаете, что ожидается от вашего программного обеспечения, и можете скрыть реализации.
Некоторая полезная информация находится здесь: https://docs.microsoft.com/en-us/cpp/cpp/explicit-instantiation?view=vs-2019
Для вашего же примера: Stack.hpp
stack.cpp
main.cpp
Вывод:
Однако мне не совсем нравится этот подход, потому что он позволяет приложению стрелять себе в ногу, передавая неверные типы данных шаблонному классу. Например, в функции main вы можете передать другие типы, которые могут быть неявно преобразованы в int, например s.Push (1.2); и это, на мой взгляд, просто плохо.
источник
Поскольку шаблоны компилируются по мере необходимости, это вызывает ограничение для многофайловых проектов: реализация (определение) класса или функции шаблона должна находиться в том же файле, что и его объявление. Это означает, что мы не можем отделить интерфейс в отдельном файле заголовка и что мы должны включать интерфейс и реализацию в любой файл, который использует шаблоны.
источник
Другая возможность - сделать что-то вроде:
Мне не нравится это предложение как вопрос стиля, но оно может вам подойти.
источник
cpp
чтобы не путать с реальными исходными файлами. Общие предложения включаютtpp
иtcc
.Ключевое слово 'export' - это способ отделить реализацию шаблона от объявления шаблона. Это было введено в стандарт C ++ без существующей реализации. Со временем только пара компиляторов фактически реализовали это. Подробную информацию читайте в статье «Информ ИТ» об экспорте
источник
1) Помните, что основная причина разделения файлов .h и .cpp заключается в том, чтобы скрыть реализацию класса как отдельно скомпилированный код Obj, который может быть связан с кодом пользователя, который включает .h класса.
2) В нешаблонных классах все переменные конкретно и конкретно определены в файлах .h и .cpp. Таким образом, компилятор будет иметь необходимую информацию обо всех типах данных, используемых в классе, перед компиляцией / трансляцией генерация объектного / машинного кода. Классы шаблонов не имеют информации о конкретном типе данных до того, как пользователь класса создаст экземпляр объекта, передающего требуемые данные. тип:
3) Только после этого экземпляра компилятор генерирует конкретную версию класса шаблона, соответствующую переданному типу (ам) данных.
4) Следовательно, .cpp НЕ МОЖЕТ быть скомпилирован отдельно без знания конкретного типа данных пользователя. Таким образом, он должен оставаться как исходный код в «.h», пока пользователь не укажет требуемый тип данных, затем он может быть сгенерирован для определенного типа данных, а затем скомпилирован
источник
Я работаю с Visual Studio 2010, если вы хотите разделить свои файлы на .h и .cpp, включите заголовок cpp в конец файла .h
источник