Хранение определений функций шаблона C ++ в файле .CPP

526

У меня есть некоторый шаблон кода, который я бы предпочел сохранить в файле CPP вместо встроенного в заголовке. Я знаю, что это можно сделать, если вы знаете, какие типы шаблонов будут использоваться. Например:

.h файл

class foo
{
public:
    template <typename T>
    void do(const T& t);
};

файл .cpp

template <typename T>
void foo::do(const T& t)
{
    // Do something with t
}

template void foo::do<int>(const int&);
template void foo::do<std::string>(const std::string&);

Обратите внимание на две последние строки - шаблонная функция foo :: do используется только с ints и std :: strings, поэтому эти определения означают, что приложение будет ссылаться.

Мой вопрос - это противный взлом или это будет работать с другими компиляторами / компоновщиками? В настоящее время я использую только этот код с VS2008, но хочу портировать на другие среды.

обкрадывать
источник
22
Я понятия не имел, что это возможно - интересный трюк! Это помогло бы некоторым недавним задачам, знающим это - ура!
Xan
69
Меня сбивает с толку использование doидентификатора: p
Quentin
я сделал что-то похожее с gcc, но все еще исследую
Ник
16
Это не «взлом», это прямая декларация. Это имеет место в стандарте языка; так что да, это разрешено в каждом стандартном компиляторе.
Ахмет Ипкин
1
Что делать, если у вас есть десятки методов? Можете ли вы просто сделать template class foo<int>;template class foo<std::string>;в конце файла .cpp?
Невежественный

Ответы:

231

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

Я рекомендую прочитать следующие пункты из C ++ FAQ Lite :

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

Аарон Н. Таббс
источник
39
Просто чтобы дополнить ответ, ссылка, на которую есть ссылка, отвечает на вопрос положительно, то есть можно сделать то, что предложил Роб, и иметь код для переносимости.
ivotron
161
Вы можете просто разместить соответствующие части в самом ответе? Почему такая ссылка даже разрешена на SO. Я понятия не имею, что искать в этой ссылке, так как она сильно изменилась с тех пор.
Идентич
124

Для других на этой странице, задающихся вопросом, каков правильный синтаксис (как я сделал) для явной специализации шаблона (или по крайней мере в VS2008), его следующее ...

В вашем .h файле ...

template<typename T>
class foo
{
public:
    void bar(const T &t);
};

И в вашем .cpp файле

template <class T>
void foo<T>::bar(const T &t)
{ }

// Explicit template instantiation
template class foo<int>;
пространство имен sid
источник
15
Вы имеете в виду «для явной специализации шаблона CLASS». В таком случае это будет охватывать все функции, которые есть в шаблонном классе?
Артур
@ Артур, кажется, нет, у меня есть некоторые методы шаблона остаются в заголовке и большинство других методов в cpp, отлично работает. Очень хорошее решение.
user1633272
В случае спрашивающего, у них есть шаблон функции, а не шаблон класса.
user253751
23

Этот код правильно сформирован. Нужно только обратить внимание, что определение шаблона видно в момент его создания. Чтобы процитировать стандарт, § 14.7.2.4:

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

Конрад Рудольф
источник
2
Что означает не экспортируемый ?
Дан Ниссенбаум
1
@Dan Видимый только внутри модуля компиляции, а не снаружи. Если вы связываете несколько модулей компиляции вместе, экспортированные символы могут использоваться между ними (и должны иметь одно или, по крайней мере, в случае шаблонов, согласованные определения, в противном случае вы столкнетесь с UB).
Конрад Рудольф
Спасибо. Я думал, что все функции (по умолчанию) видны за пределами модуля компиляции. Если у меня есть две единицы компиляции a.cpp(определяющие функцию a() {}) и b.cpp(определяющие функцию b() { a() }), то это будет успешно соединено. Если я прав, то приведенная выше цитата может не подходить для типичного случая ... я что-то не так делаю?
Дан Ниссенбаум
@Dan Тривиальный контрпример: inlineфункции
Конрад Рудольф
1
Шаблоны функций @Dan неявно inline. Причина в том, что без стандартизированного C ++ ABI трудно / невозможно определить, какой эффект это могло бы оказать в противном случае.
Конрад Рудольф
15

Это должно работать нормально везде, где поддерживаются шаблоны. Явная реализация шаблона является частью стандарта C ++.

Лунная тень
источник
13

Ваш пример правильный, но не очень переносимый. Существует также немного более чистый синтаксис, который можно использовать (как указано @ namespace-sid).

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

Альтернативный подход - это небольшая вариация того, что у вас есть: добавьте третий файл, который является файлом реализации / создания шаблона.

файл foo.h

// Standard header file guards omitted

template <typename T>
class foo
{
public:
    void bar(const T& t);
};

файл foo.cpp

// Always include your headers
#include "foo.h"

template <typename T>
void foo::bar(const T& t)
{
    // Do something with t
}

файл foo-impl.cpp

// Yes, we include the .cpp file
#include "foo.cpp"
template class foo<int>;

Один нюанс в том , что вы должны сообщить компилятору для компиляции foo-impl.cppвместо того , foo.cppкак составление последнего ничего не делает.

Конечно, вы можете иметь несколько реализаций в третьем файле или иметь несколько файлов реализации для каждого типа, который вы хотите использовать.

Это обеспечивает большую гибкость при совместном использовании шаблонного класса для других целей.

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

Кэмерон Таклинд
источник
что это тебе покупает? Вам все еще нужно отредактировать foo-impl.cpp, чтобы добавить новую специализацию.
МК.
Разделение деталей реализации (или определений в foo.cpp), из которых версии фактически скомпилированы (в foo-impl.cpp) и объявлений (в foo.h). Мне не нравится, что большинство шаблонов C ++ полностью определены в заголовочных файлах. Это противоречит стандарту пар C / C ++ c[pp]/hдля каждого класса / пространства имен / независимо от того, какую группу вы используете. Люди, похоже, все еще используют монолитные заголовочные файлы просто потому, что эта альтернатива широко не используется и не известна.
Кэмерон Таклинд
1
@MK. Сначала я помещал явные экземпляры шаблона в конце определения в исходном файле, пока мне не понадобились дополнительные экземпляры в другом месте (например, модульные тесты с использованием макета в качестве шаблонного типа). Это разделение позволяет мне добавить больше экземпляров извне. Кроме того, он все еще работает, когда я сохраняю оригинал в h/cppпаре, хотя мне нужно было окружить исходный список экземпляров включенной защитой, но я все еще мог скомпилировать foo.cppкак обычно. Я все еще довольно новичок в C ++, и мне было бы интересно узнать, есть ли у этого смешанного использования какие-либо дополнительные предостережения.
Thirdwater
3
Я думаю, что предпочтительнее отделить foo.cppи foo-impl.cpp. Нет #include "foo.cpp"в foo-impl.cppфайле; вместо этого добавьте объявление, extern template class foo<int>;чтобы foo.cppпредотвратить создание экземпляра шаблона компилятором при компиляции foo.cpp. Убедитесь, что система сборки создает оба .cppфайла и передает оба объектных файла компоновщику. Это имеет несколько преимуществ: а) ясно, foo.cppчто нет инстанцирования; б) изменения в foo.cpp не требуют перекомпиляции foo-impl.cpp.
Шмуэль Левайн
3
Это очень хороший подход к проблеме определений шаблонов, которая берет лучшее из обоих миров - реализация заголовка и создание экземпляров для часто используемых типов. Единственное изменение, которое я внесу в эту настройку, - это переименование foo.cppв foo_impl.hи foo-impl.cppтолько foo.cpp. Я также добавил бы typedefs для экземпляров из foo.cppto foo.h, аналогично using foo_int = foo<int>;. Хитрость заключается в том, чтобы предоставить пользователям два интерфейса заголовков на выбор. Когда пользователю нужно предварительно определенное создание, он включает foo.h, когда пользователю нужно что-то не в порядке, он включает foo_impl.h.
Вормер
5

Это определенно не противный хак, но имейте в виду, что вам придется делать это (явная специализация шаблона) для каждого класса / типа, который вы хотите использовать с данным шаблоном. В случае МНОГИХ типов, запрашивающих создание экземпляра шаблона, в вашем файле .cpp может быть МНОГО строк. Чтобы устранить эту проблему, вы можете иметь TemplateClassInst.cpp в каждом используемом вами проекте, чтобы вы могли лучше контролировать, какие типы будут создаваться. Очевидно, что это решение не будет идеальным (так называемая серебряная пуля), так как в итоге вы можете сломать ODR :).

Красный XIII
источник
Вы уверены, что это сломает ODR? Если строки экземпляра в TemplateClassInst.cpp ссылаются на идентичный исходный файл (содержащий определения функции шаблона), разве это не гарантирует, что не нарушит ODR, поскольку все определения идентичны (даже если повторяются)?
Дан Ниссенбаум
Пожалуйста, что такое ODR?
несменяемый
4

В последнем стандарте есть ключевое слово ( export), которое поможет решить эту проблему, но оно не реализовано ни в одном из известных мне компиляторов, кроме Comeau.

Смотрите FAQ-Lite об этом.

Бен Коллинз
источник
2
AFAIK, экспорт мертв, потому что они сталкиваются с новыми и новыми проблемами, каждый раз, когда они решают последнюю, делая общее решение все более и более сложным. И ключевое слово «export» не позволит вам «экспортировать» из CPP в любом случае (все равно из H. Sutter's в любом случае). Поэтому я говорю: не задерживай дыхание ...
paercebal
2
Для реализации экспорта компилятору все еще требуется полное определение шаблона. Все, что вы получаете, это иметь своего рода скомпилированную форму. Но на самом деле в этом нет никакого смысла.
Zan Lynx
2
... и это ушло от стандарта из-за чрезмерного усложнения для минимального усиления.
DevSolar
4

Это стандартный способ определения функций шаблона. Я думаю, что есть три метода для определения шаблонов. Или, вероятно, 4. Каждый со своими плюсами и минусами.

  1. Определите в определении класса. Мне это совсем не нравится, потому что я думаю, что определения классов предназначены только для справки и должны быть легко читаемыми. Однако определить шаблоны в классе гораздо сложнее, чем снаружи. И не все объявления шаблонов находятся на одном уровне сложности. Этот метод также делает шаблон настоящим шаблоном.

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

  3. Включите header.h и creation.CPP в ваш main.CPP. Я думаю, вот как это сделано. Вам не придется готовить какие-либо предварительные экземпляры, это будет вести себя как настоящий шаблон. Проблема с этим в том, что это не естественно. Обычно мы не включаем и не ожидаем включения исходных файлов. Я предполагаю, что поскольку вы включили исходный файл, функции шаблона могут быть встроены.

  4. Этот последний метод, который был опубликован, определяет шаблоны в исходном файле, как и номер 3; но вместо того, чтобы включать исходный файл, мы предварительно создаем шаблоны для тех, которые нам понадобятся. У меня нет проблем с этим методом, и иногда он бывает полезен. У нас есть один большой код, он не может извлечь выгоду из встроенного кода, поэтому просто поместите его в файл CPP. И если мы знаем общие экземпляры и можем их предопределить. Это спасает нас от написания в основном одной и той же вещи 5, 10 раз. Этот метод имеет преимущество сохранения нашего кода в проприетарном виде. Но я не рекомендую помещать крошечные, регулярно используемые функции в файлы CPP. Как это снизит производительность вашей библиотеки.

Обратите внимание, я не знаю о последствиях раздутого файла obj.

Кассио Ренан
источник
3

Да, это стандартный способ явной реализации специализации . Как вы заявили, вы не можете создать экземпляр этого шаблона с другими типами.

Изменить: исправлено на основе комментария.

Лу Франко
источник
Быть разборчивым в терминологии - это «явная реализация».
Ричард Корден
2

Давайте рассмотрим один пример, скажем, по какой-то причине вы хотите иметь шаблонный класс:

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

template <>
void DemoT<int>::test()
{
    printf("int test (int)\n");
}


template <>
void DemoT<bool>::test()
{
    printf("int test (bool)\n");
}

Если вы компилируете этот код с помощью Visual Studio - он работает «из коробки». gcc выдаст ошибку компоновщика (если один и тот же заголовочный файл используется из нескольких файлов .cpp):

error : multiple definition of `DemoT<int>::test()'; your.o: .../test_template.h:16: first defined here

Можно перенести реализацию в файл .cpp, но тогда вам нужно объявить класс следующим образом:

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

template <>
void DemoT<int>::test();

template <>
void DemoT<bool>::test();

// Instantiate parametrized template classes, implementation resides on .cpp side.
template class DemoT<bool>;
template class DemoT<int>;

И тогда .cpp будет выглядеть так:

//test_template.cpp:
#include "test_template.h"

template <>
void DemoT<int>::test()
{
    printf("int test (int)\n");
}


template <>
void DemoT<bool>::test()
{
    printf("int test (bool)\n");
}

Без двух последних строк в заголовочном файле - gcc будет работать нормально, но Visual Studio выдаст ошибку:

 error LNK2019: unresolved external symbol "public: void __cdecl DemoT<int>::test(void)" (?test@?$DemoT@H@@QEAAXXZ) referenced in function

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

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

#ifdef _WIN32
    #define DLL_EXPORT __declspec(dllexport) 
#else
    #define DLL_EXPORT
#endif

template <>
void DLL_EXPORT DemoT<int>::test();

template <>
void DLL_EXPORT DemoT<bool>::test();

с .cpp файлом из предыдущего примера.

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

TarmoPikaro
источник
1

Время для обновления! Создайте встроенный (.inl или, возможно, любой другой) файл и просто скопируйте в него все свои определения. Обязательно добавьте шаблон над каждой функцией ( template <typename T, ...>). Теперь вместо включения заголовочного файла во встроенный файл вы делаете обратное. Включите встроенный файл после объявления вашего класса ( #include "file.inl").

Я действительно не знаю, почему никто не упомянул это. Я не вижу непосредственных недостатков.

Didii
источник
25
Непосредственный недостаток заключается в том, что он в основном такой же, как просто определение шаблонных функций непосредственно в заголовке. Как только вы #include "file.inl", препроцессор собирается вставить содержимое file.inlнепосредственно в заголовок. Какой бы ни была причина, по которой вы хотели избежать реализации в заголовке, это решение не решает эту проблему.
Коди Грей
5
- и означает, что вы, технически неоправданно, обременяете себя задачей написания всего многословного, сногсшибательного шаблона, необходимого для внеплановых templateопределений. Я понимаю, почему люди хотят это сделать - добиться наибольшего паритета с объявлениями / определениями, не относящимися к шаблонам, чтобы интерфейсные объявления выглядели аккуратно и т. Д., - но это не всегда стоит хлопот. Это случай оценки компромиссов с обеих сторон и выбора наименее плохих . ... пока не namespace classстанет вещью: O [ пожалуйста, будь вещью ]
underscore_d
2
@ Андрей Кажется, он застрял в трубах Комитета, хотя я думаю, что видел, как кто-то говорил, что это не было преднамеренным. Я хотел бы, чтобы это вошло в C ++ 17. Может быть, в следующем десятилетии.
underscore_d
@CodyGray: Технически, это действительно то же самое для компилятора, и поэтому оно не сокращает время компиляции. Тем не менее, я думаю, что это стоит упомянуть и практиковать в ряде проектов, которые я видел. Переход по этому пути помогает отделить интерфейс от определения, что является хорошей практикой. В этом случае это не помогает с совместимостью с ABI и т.п., но облегчает чтение и понимание интерфейса.
kiloalphaindia
0

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

При использовании вместе с явной реализацией классов, библиотека Boost Concept Check Library (BCCL) может помочь вам сгенерировать код функции шаблона в файлах cpp.

Benoît
источник
8
Что в этом неэффективного?
Коди Грей
0

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

.час

class Model
{
    template <class T>
    void build(T* b, uint32_t number);
};

.cpp

#include "Model.h"
template <class T>
void Model::build(T* b, uint32_t number)
{
    //implementation
}

void TemporaryFunction()
{
    Model m;
    m.build<B1>(new B1(),1);
    m.build<B2>(new B2(), 1);
    m.build<B3>(new B3(), 1);
}

это позволяет избежать ошибок компоновщика и вообще не вызывать TemporaryFunction

KronuZ
источник