7

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

24

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

.h файл

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

.cpp файл

template <typename T>
void foo::do(const T& t)
{
    // Выполняем некоторые действия с t
}

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

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

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

5 ответ(ов)

1

Для тех, кто на этой странице интересуется, какова правильная синтаксическая форма явной инстанциации шаблонов (как и я), особенно в VS2008, вот пример...

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

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

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

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

// Явная инстанциация шаблона
template class foo<int>;

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

0

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

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

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

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

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

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

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

#pragma once

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

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

// Защитный инкод, на случай если
#pragma once

#include "foo.hpp"

template <typename T>
void foo<T>::bar(const T& arg) {
    // Выполнить что-то с `arg`
}

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

// Рекомендуется добавить "анти-охрану", чтобы убедиться, что файл не включается в другие переводимые единицы
#if __INCLUDE_LEVEL__
  #error "Не включайте этот файл"
#endif

// Да, мы включаем .cpp файл
#include <lib/foo.cpp>
#include "MyType.hpp"

template class foo<MyType>;

Организуйте свои реализации по желанию:

  • Все реализации в одном файле
  • Несколько файлов реализации, по одному для каждого типа
  • Файл реализации для каждого набора типов

Почему??

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

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

foo.MyType.hpp - нуждается в информации о публичном интерфейсе foo<MyType>, но не в источниках .cpp

#pragma once

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

// Объявляем `temp`. Не нужно включать `foo.cpp`
extern foo<MyType> temp;

examples.cpp - может ссылаться на локальное объявление, но также не перекомпилирует foo<MyType>

#include "foo.MyType.hpp"

MyType instance;

// Определяем `temp`. Не нужно включать `foo.cpp`
foo<MyType> temp;

void example_1() {
    // Используем `temp`
    temp.bar(instance);
}

void example_2() {
    // Локальный экземпляр функции
    foo<MyType> temp2;

    // Используем шаблонную функцию библиотеки
    temp2.bar(instance);
}

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

#include <lib/foo.hpp>

// Вызывает ошибки компиляции на этапе линковки, поскольку мы никогда не имели явного инстанцирования:
// template class foo<int>;
// Линковщик GCC выдает ошибку: "неопределенная ссылка на `foo<int>::bar()'"
foo<int> nonExplicitlyInstantiatedTemplate;
void linkerError() {
    nonExplicitlyInstantiatedTemplate.bar();
}

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


Альтернативный подход из: https://stackoverflow.com/a/495056/4612476

0

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

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

0

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

0

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

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

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

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

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

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

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь