Как я могу сжато написать множество явных экземпляров шаблонов функций?

Я пишу библиотеку C++, содержащую множество шаблонов функций, которые я хочу явно создать и экспортировать для нескольких параметров типа. В моем конкретном случае у меня есть много шаблонов числовых функций, которые я хочу отдельно создать и скомпилировать для float, double и long double. Они выглядят примерно так:

template <typename T>
T calculate_a(T x) { ... }

template <typename T>
T calculate_b(T x, T y) { ... }

// ...

Если у меня есть M шаблонов функций и N базовых типов, то у меня есть M*N явных экземпляров для ввода. Можно ли написать эти экземпляры более лаконично?

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

#define EXPLICITLY_INSTANTIATE(T) \
    template T calculate_a<T>(T x); \
    template T calculate_b<T>(T x, T y); \
    // ...

EXPLICITLY_INSTANTIATE(float);
EXPLICITLY_INSTANTIATE(double);
EXPLICITLY_INSTANTIATE(long double);

Однако это неоптимально, потому что мне нужно отдельно поддерживать еще одну копию подписи каждого шаблона функции. Кроме того, если я хочу сделать это в нескольких единицах перевода, мне нужно отдельно поддерживать список базовых типов в каждой. (Предположим, что C++2a добавляет тип long long double, который я хочу поддерживать; мне придется добавлять EXPLICITLY_INSTANTIATE(long long double); в каждый файл.)

Другой возможный подход — собрать все мои функции в класс шаблона (только статический):

template <typename T>
class calculate {
    T a(T x) { ... }
    T b(T x, T y) { ... }
};

template class calculate<float>;
template class calculate<double>;
template class calculate<long double>;

Это решает первую проблему раздельного обслуживания двух копий каждой подписи, но требует от меня изменения каждого вызова calculate_a на calculate::a<T>. Это не решает вторую проблему.


person David Zhang    schedule 14.05.2018    source источник
comment
Например, если у вас есть все типы с плавающей запятой и все целочисленные типы, решение SFINAE может быть полезным.   -  person DeiDei    schedule 15.05.2018
comment
@DeiDei Не могли бы вы уточнить? Я не уверен, как использовать SFINAE для написания явных экземпляров.   -  person David Zhang    schedule 15.05.2018
comment
@DeiDei дело в том, чтобы не писать явные экземпляры, а использовать SFINAE только для объявления функции шаблона для тех типов, которые вам нужны. Другой вариант — static_assert, если тип шаблона не поддерживается. Например: template <typename T> T calculate() { static_assert( std::is_floating_point<T>::value, "Only supports floating points types" ); ... }   -  person clcto    schedule 15.05.2018
comment
@DavidZhang Мой комментарий был скорее альтернативным подходом (например, упоминания clcto). На самом деле нет способа легко сгенерировать явное создание экземпляра для каждого встроенного типа в языке. Вам придется свернуть их один за другим. Макро-решение только экономит часть ввода и, возможно, менее читабельно.   -  person DeiDei    schedule 15.05.2018
comment
@clcto Ах, я не ясно выразил свое намерение в вопросе. Цель моих явных экземпляров не в том, чтобы ограничить типы, с которыми могут быть вызваны эти функции, а в том, чтобы проинформировать компилятор о создании исполняемого кода для float, double и long double. Эти функции являются частью библиотечного компонента, который компилируется отдельно от основной программы, в которой они используются. Я отредактирую вопрос, чтобы уточнить это.   -  person David Zhang    schedule 15.05.2018
comment
Почему бы не использовать обычную перегрузку для нужных вам типов, чтобы они вызывали шаблон функции?   -  person Nevin    schedule 15.05.2018
comment
@DavidZhang: я признаю, что мои знания о явном создании экземпляров шаблонов не идеальны, но у меня сложилось впечатление, что явное создание экземпляра шаблона класса не приводит к автоматическому созданию всех его функций-членов. Я могу ошибаться в этом. Я совершенно уверен, что выведение аргументов шаблона класса не позволяет вам выводить аргументы шаблона класса из аргументов, переданных одному из его статических членов.   -  person Nicol Bolas    schedule 15.05.2018
comment
@NicolBolas Я знаю, что G ++ создает экземпляры статических функций-членов, когда видит явное создание экземпляра шаблона класса. (Насколько мне известно, это может быть нестандартное расширение GNU, но G++ достаточно хорош для моих целей.) Однако хороший вывод — это действительно не работает.   -  person David Zhang    schedule 15.05.2018
comment
Какой подход вы использовали в итоге?   -  person cppBeginner    schedule 22.09.2020


Ответы (4)


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

// forward declarations in a header file
template<typename T>
T square(T num);

template<typename T>
T add(T left, T right);

// implementations and instantiations in a single implementation file
template<typename T>
T square(T num) {
    return num * num;
}

template<typename T>
T add(T left, T right) {
    return left + right;
}

// instantiations for specific types
#include <tuple>

template<typename... Ts>
auto instantiate() {
    static auto funcs = std::tuple_cat(std::make_tuple(
        add<Ts>,
        square<Ts>
    )...);

    return &funcs;
}

template auto instantiate<int, double>();

Накладные расходы здесь — это единый массив указателей на все созданные функции, например, на godbolt.

person h.m.m.    schedule 15.05.2018
comment
(Примечание: в этом случае указатели хранятся в виде кортежа, а не массива.) -- Кстати, можно также избежать накладных расходов на данные, но вместо этого включить накладные расходы на код (godbolt.org/z/G7zPjP); однако если значение оптимизировано, то функции не будут реализованы (почему?) - person user202729; 31.01.2021
comment
Это отличный ответ, но я обнаружил, что с указателями на функции-члены мне пришлось проделать больше работы, чтобы заставить GCC не выдавать дополнительный объектный код. С C++ 17 вместо выполнения template auto instantiate<int, double>(); я сделал это static constexpr auto function_templates __attribute__((used)) = instantiate<int, double>(); плюс изменил instantiate(), чтобы вернуть tuple_cat вместо статической локальной функции. Это заставило компилятор вычислить кортеж во время компиляции. Без этого он не осознавал, что funcs был полностью постоянным. - person jwnimmer-tri; 20.06.2021
comment
Вот ссылка на разновидность constexpr godbolt.org/z/vE56Wrh1f Обратите внимание, что дополнительных статических данных нет. больше. - person jwnimmer-tri; 20.06.2021

Для этого и созданы Макросы X. Это работает довольно просто.

У вас есть файл, содержащий все типы, к которым вы хотите применить это. Назовем его "type_list.inc". Это будет выглядеть так:

X(float)
X(double)
X(long double)

Когда вы хотите выполнить какую-либо операцию над этим списком типов, вы #include файл, но вокруг точки включения вы #define макрос X для выполнения операции, которую вы хотите выполнить:

#define X(T) \
    template T calculate_a<T>(T x); \
    template T calculate_b<T>(T x, T y); \
    // ...
#include "type_list.inc"
#undef X

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

person Nicol Bolas    schedule 15.05.2018
comment
Отличная идея! Как вы заметили, это не оптимально, но мне удалось сократить количество строк кода до O(M+N). - person David Zhang; 15.05.2018

Я не ясно выразил свое намерение в вопросе. Цель моих явных экземпляров не в том, чтобы ограничить типы, с которыми могут быть вызваны эти функции, а в том, чтобы проинформировать компилятор о создании исполняемого кода для float, double и long double.

Ну... если все ваши типы являются конструктивными по умолчанию (как float, double и long double)... используя свертывание в функции шаблона foo() следующим образом

template <typename ... Ts>
void foo ()
 { ((calculate_a(Ts{}), calculate_b(Ts{}, Ts{})), ...); }

и вызов foo() с желаемыми типами

foo<float, double, long double>();

должно работать, я полагаю.

Ниже приведен полный пример компиляции

template <typename T>
T calculate_a (T x)
 { return x; }

template <typename T>
T calculate_b (T x, T y)
 { return x+y; }

template <typename ... Ts>
void foo ()
 { ((calculate_a(Ts{}), calculate_b(Ts{}, Ts{})), ...); }

int main ()
 {
   foo<float, double, long double>();
 }
person max66    schedule 14.05.2018

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

float calculate_a(float x) { return calculate_a<float>(x); }
float calculate_b(float x, float y) { return calculate_b<float>(x, y); }

double calculate_a(double x) { return calculate_a<double>(x); }
double calculate_b(double x, double y) { return calculate_b<double>(x, y); }

long double calculate_a(long double x) { return calculate_a<long double>(x); }
long double calculate_b(long double x, long double y) { return calculate_b<long double>(x, y); }
person Nevin    schedule 14.05.2018
comment
Это сработало бы, но я не пытаюсь не писать все объявления функций M*N самостоятельно. Есть ли способ обеспечить перегрузки, которые не требуют повторной записи всех подписей? - person David Zhang; 15.05.2018
comment
Либо вы остаетесь в стране шаблонов, где компилятор может написать их за вас, либо нет, и в этом случае вы должны написать их самостоятельно. Лучшее, что вы можете сделать в последнем случае, это использовать макросы для облегчения генерации этих функций. - person Nevin; 15.05.2018