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

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

Я понятия не имел, что это возможно - интересный трюк! Знание этого могло бы значительно помочь некоторым недавним задачам - ура!

xan 22.09.2008 20:00

Меня поражает использование do в качестве идентификатора: p

Quentin 14.01.2015 13:00

я сделал что-то подобное с gcc, но все еще исследую

Nick 25.09.2015 19:01

Это не «взлом», это предварительное объявление. Этому есть место в стандарте языка; так что да, это разрешено в каждом стандартном компиляторе.

Ahmet Ipkin 07.03.2016 17:33

Что, если у вас есть десятки методов? Можете просто сделать template class foo<int>;template class foo<std::string>; в конце файла .cpp?

Ignorant 11.06.2018 17:22

связанные: stackoverflow.com/questions/495021/…

Ciro Santilli TRUMP BAN IS BAD 06.01.2020 17:55
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
575
6
383 806
13
Перейти к ответу Данный вопрос помечен как решенный

Ответы 13

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

Обновлено: исправлено на основе комментария.

Придирчиво к терминологии, это «явное воплощение».

Richard Corden 22.09.2008 20:19
Ответ принят как подходящий

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

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

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

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

ivotron 02.05.2011 01:46

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

Ident 17.08.2015 00:57

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

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

См. FAQ-lite об этом.

AFAIK, экспорт мертв, потому что они сталкиваются с все новыми и новыми проблемами, каждый раз они решают последнюю, что делает общее решение все более и более сложным. И ключевое слово «экспорт» в любом случае не позволит вам «экспортировать» из CPP (все равно из Х. Саттера). Поэтому я говорю: не задерживайте дыхание ...

paercebal 22.09.2008 20:25

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

Zan Lynx 12.03.2012 20:56

... и это ушел от стандарта из-за чрезмерного усложнения для минимального усиления.

DevSolar 26.08.2015 16:04

Этот код хорошо сформирован. Вам нужно только обратить внимание на то, что определение шаблона видно в момент создания экземпляра. Процитируем стандарт, § 14.7.2.4:

The definition of a non-exported function template, a non-exported member function template, or a non-exported member function or static data member of a class template shall be present in every translation unit in which it is explicitly instantiated.

Что означает не экспортированный?

Dan Nissenbaum 12.06.2014 23:29

@Dan Виден только внутри модуля компиляции, но не вне его. Если вы связываете несколько единиц компиляции вместе, экспортируемые символы могут использоваться в них (и должны иметь одно или, по крайней мере, в случае шаблонов, согласованные определения, иначе вы столкнетесь с UB).

Konrad Rudolph 12.06.2014 23:32

Спасибо. Я думал, что все функции (по умолчанию) видны вне модуля компиляции. Если у меня есть две единицы компиляции a.cpp (определяющие функцию a() {}) и b.cpp (определяющие функцию b() { a() }), то это будет успешно соединено. Если я прав, то приведенная выше цитата не подходит для типичного случая ... Я где-то ошибаюсь?

Dan Nissenbaum 12.06.2014 23:46

@Dan Тривиальный контрпример: функции inline

Konrad Rudolph 13.06.2014 00:02

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

Dan Nissenbaum 13.06.2014 01:06

Шаблоны функций @Dan неявно являются inline. Причина в том, что без стандартизированного C++ ABI трудно / невозможно определить эффект, который в противном случае имел бы место.

Konrad Rudolph 13.06.2014 01:41

осторожно, рекурсивные вопросы и ответы :)

osirisgothra 07.05.2015 02:39

@DanNissenbaum Я думаю, что не экспортированный относится к export ключевое слово.

Meowmere 18.02.2021 21:43

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

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

Что в этом неэффективного?

Cody Gray 25.06.2013 07:40

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

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

Dan Nissenbaum 12.06.2014 23:36

Пожалуйста, что такое ODR?

nonremovable 22.06.2018 14:45

Для других на этой странице, которым интересно, каков правильный синтаксис (как и я) для явной специализации шаблона (или, по крайней мере, в 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>;

Вы имеете в виду "для явного использования шаблона CLASS". В таком случае будет ли это охватывать все функции, которые есть в шаблонном классе?

0x26res 21.02.2013 17:57

@Arthur, похоже, нет, у меня некоторые методы шаблона остаются в заголовке, а большинство других методов в cpp работают нормально. Очень красивое решение.

user1633272 03.10.2019 18:29

В случае спрашивающего у них есть шаблон функции, а не шаблон класса.

user253751 06.03.2020 13:56

Итак, вы можете поместить несколько классов шаблонов foo <...> в конец определенного файла, верно? Итак, один файл для определений для int, например, Other для float, если есть какие-то различия, если нет различий, вы можете просто перенести класс шаблона foo <float> под int? Я правильно понимаю?

Petko Kamenov 09.03.2021 15:23

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

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

Непосредственный недостаток заключается в том, что это в основном то же самое, что и определение функций шаблона непосредственно в заголовке. После того, как вы запустите #include "file.inl", препроцессор вставит содержимое file.inl прямо в заголовок. По какой бы причине вы не хотели, чтобы реализация была указана в заголовке, это решение не решает эту проблему.

Cody Gray 25.06.2013 07:38

- и означает, что вы технически без надобности обременяете себя задачей написания всех подробных, умопомрачительных шаблонов, необходимых для автономных определений template. Я понимаю, почему люди хотят это делать - для достижения максимальной четкости с объявлениями / определениями, не являющимися шаблонами, чтобы объявление интерфейса выглядело аккуратно и т. д. - но это не всегда стоит хлопот. Это случай оценки компромиссов с обеих сторон и выбора наименее плохо. ... пока namespace class не станет вещью: O [пожалуйста, будь вещью]

underscore_d 02.08.2016 23:52

@ Андрей: Кажется, он застрял в трубе Комитета, хотя, кажется, я видел, как кто-то говорил, что это не было намеренно. Хотелось бы, чтобы это вошло в C++ 17. Может, в следующем десятилетии.

underscore_d 14.09.2016 13:05

@CodyGray: Технически это действительно то же самое для компилятора и, следовательно, не сокращает время компиляции. Тем не менее, я думаю, что это стоит упомянуть и практиковать в ряде проектов, которые я видел. Этот путь помогает отделить интерфейс от определения, что является хорошей практикой. В этом случае это не помогает с совместимостью ABI и т.п., но облегчает чтение и понимание интерфейса.

kiloalphaindia 07.05.2018 12:29

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

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

Следует ли компилировать другие версии шаблонного класса?

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

Альтернативный подход

Добавьте третий файл, который является файлом реализации / создания шаблона в ваших источниках.

lib/foo.hppв / из библиотеки

#pragma once

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

lib/foo.cppкомпиляция этого файла напрямую просто тратит время компиляции

// Include guard here, just in case
#pragma once

#include "foo.hpp"

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

foo.MyType.cppс использованием библиотеки, явное создание экземпляра шаблона foo<MyType>

// Consider adding "anti-guard" to make sure it's not included in other translation units
#if __INCLUDE_LEVEL__ != 0
  #error "Don't include this file"
#endif

// Yes, we include the .cpp file
#include <lib/foo.cpp>
#include "MyType.hpp"

template class foo<MyType>;

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

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

Примеры использования

foo.declarations.hppнеобходимо знать об общедоступном интерфейсе foo<MyType>, но не об источниках .cpp

#pragma once

#include <lib/foo.hpp>
#include "MyType.hpp"

// Declare `temp`. Doesn't need to include `foo.cpp`
extern foo<MyType> temp;

examples.cppможет ссылаться на локальную декларацию, но также не перекомпилирует foo<MyType>

#include "foo.declarations.hpp"

MyType instance;

// Define `temp`. Doesn't need to include `foo.cpp`
foo<MyType> temp;

void example_1() {
    // Use `temp`
    temp.bar(instance);
}

void example_2() {
    // Function local instance
    foo<MyType> temp2;

    // Use templated library function
    temp2.bar(instance);
}

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

#include <lib/foo.hpp>

// Causes compilation errors at link time since we never had the explicit instantiation:
// template class foo<int>;
// GCC linker gives an error: "undefined reference to `foo<int>::bar()'"

void linkerError()
{
    foo<int> nonExplicitlyInstantiatedTemplate;
}

Обратите внимание, что большинство компиляторов / линтеров / помощников по коду не распознают это как ошибку, поскольку в соответствии со стандартом C++ ошибки нет. Но когда вы собираетесь связать эту единицу трансляции с полным исполняемым файлом, компоновщик не найдет определенную версию foo<int>.

что это тебе дает? Вам все равно нужно отредактировать foo-impl.cpp, чтобы добавить новую специализацию.

MK. 22.03.2017 17:16

Разделение деталей реализации (также называемых определениями в foo.cpp), из которых фактически компилируются версии (в foo-impl.cpp) и деклараций (в foo.h). Мне не нравится, что большинство шаблонов C++ полностью определены в файлах заголовков. Это противоречит стандарту C / C++ пар c[pp]/h для каждого класса / пространства имен / любой группы, которую вы используете. Люди, кажется, до сих пор используют монолитные файлы заголовков просто потому, что эта альтернатива широко не используется и не известна.

Cameron Tacklind 29.03.2017 23:06

@MK. Сначала я помещал явные экземпляры шаблона в конец определения в исходном файле, пока мне не потребовались дальнейшие экземпляры в другом месте (например, модульные тесты с использованием макета в качестве шаблонного типа). Это разделение позволяет мне добавлять больше экземпляров извне. Кроме того, он все еще работает, когда я сохраняю оригинал как пару h/cpp, хотя мне пришлось окружить исходный список экземпляров защитой включения, но я все еще мог скомпилировать foo.cpp как обычно. Я все еще новичок в C++, и мне было бы интересно узнать, есть ли у этого смешанного использования какие-либо дополнительные оговорки.

Thirdwater 28.12.2018 10:02

Я считаю, что лучше разделять foo.cpp и foo-impl.cpp. Не используйте #include "foo.cpp" в файле foo-impl.cpp; вместо этого добавьте объявление extern template class foo<int>; в foo.cpp, чтобы компилятор не создавал экземпляр шаблона при компиляции foo.cpp. Убедитесь, что система сборки создает оба файла .cpp и передает оба объектных файла компоновщику. У этого есть несколько преимуществ: a) в foo.cpp ясно, что нет создания экземпляра; б) изменения в foo.cpp не требуют перекомпиляции foo-impl.cpp.

Shmuel Levine 29.05.2019 17:17

Это очень хороший подход к проблеме определений шаблонов, который берет лучшее из обоих миров - реализацию заголовка и создание экземпляров для часто используемых типов. Единственное изменение, которое я бы внес в эту настройку, - это переименовать foo.cpp в foo_impl.h, а foo-impl.cpp - в foo.cpp. Я бы также добавил typedef для экземпляров от foo.cpp до foo.h, аналогично using foo_int = foo<int>;. Хитрость заключается в том, чтобы предоставить пользователям два интерфейса заголовков на выбор. Когда пользователю требуется предопределенная реализация, он включает foo.h, когда пользователю нужно что-то не в порядке, он включает foo_impl.h.

Wormer 30.10.2019 23:04

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

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

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

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

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

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

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

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

Отличный ответ

OS2 27.12.2020 05:06

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

.час

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

Ваш ответ такой же, как и вопрос, и он не работает!

Mahmut EFE 24.01.2021 05:53

Другие вопросы по теме