SFINAE - откат к функции по умолчанию, если более сложная не удалась - PullRequest
0 голосов
/ 20 мая 2018

Скажем, я написал обобщенную функцию с именем interpolate.Его подпись выглядит следующим образом:

template<typename T>
T interpolate(T a, T b, float c);

Где a и b - значения для интерполяции, а c - число с плавающей запятой в [0.0,1.0].

Если T имеет T operator*(float)и T operator+(T) определено, я бы хотел, чтобы это велось определенным образом (линейная интерполяция).В противном случае он будет вести себя по-другому - таким образом, что любой T может быть использован (интерполяция ближайшего соседа).

Как мне добиться этого поведения?

Например:

interpolate<std::string>("hello","world!", 0.798); //uses nearest neighbor, as std::string does not have the necessary operators

interpolate<double>(42.0,128.0, 0.5);              //uses linear, as double has the needed operators

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

Ответы [ 2 ]

0 голосов
/ 20 мая 2018

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

using prefer_overload_t = int;
using backup_overload_t = long;

template <typename T>
auto interpolate_impl(T a, T b, float c, prefer_overload_t)
    -> std::enable_if_t<
           std::is_same_v<T, decltype(a * c)>
           && std::is_same_v<T, decltype(a + b)>,
           T
       >
{
    // linear interpolation
}

template <typename T>
T interpolate_impl(T a, T b, float c, backup_overload_t)
{
    // nearest neighbor
}

template<typename T>
T interpolate(T a, T b, float c)
{
    return interpolate_impl(std::move(a), std::move(b), c, prefer_overload_t());
}

Поскольку для перехода с int на int преобразование не требуется, предпочтительной является прежняя перегрузка, но она не соответствует SFINAE.когда это не работает.


Если вы хотите заказать произвольное количество перегрузок, вам придется использовать какой-то специальный тип, такой как:

template <std::size_t N>
struct rank : rank<N - 1>
{};

template <>
struct rank<0>
{};

Затем,rank<N> предпочтительнее rank<N - 1>.

0 голосов
/ 20 мая 2018

Это звучит как основной вариант использования для отправки тегов :

Мы создаем два разных класса тегов, чтобы различать два варианта использования

struct linear_tag {};
struct nn_tag {};

template <typename T>
T impl(T a, T b, float c, linear_tag) {
    // linear interpolation here
}

template <typename T>
T impl(T a, T b, float c, nn_tag) {
    // nearest neighbor interpolation here
}

Теперь,нам нужно выяснить тип тега из T:

template <typename T>
linear_tag tag_for(
    T* p,
    std::enable_if_t<std::is_same_v<T, decltype((*p + *p) * 0.5)>>* = nullptr
);
nn_tag tag_for(...); // Fallback

Первая перегрузка существует, только если для любого T t выражение (t + t) * 0.5f возвращает другое T. 1 Вторая перегрузка всегда существует, но из-за аргумента variadic в стиле C она никогда не используется, если первая перегрузка не совпадает.

Затем мы можем отправить любую версию, создав соответствующуюtag:

template <typename T>
T interpolate(T a, T b, float c) {
    return impl(a, b, c, decltype(tag_for(static_cast<T*>(nullptr))){});
}

Здесь decltype(tag_for(static_cast<T*>(nullptr))) дает нам правильный тип тега (как тип возвращаемого значения правильной перегрузки tag_for).

Вы можете добавить дополнительные типы тегов с помощьюочень небольшие накладные расходы и проверка произвольно сложных условий в enable_if_t.Эта конкретная версия предназначена только для C ++ 17 (из-за is_same_v), но вы также можете легко сделать ее совместимой с C ++ 11, используя вместо этого typename std::enable_if<...>::type и std::is_same<...>::value - это просто более многословно.

1 Это то, что вы указали в вопросе - но это опасно!Например, если вы используете целые числа, вы будете использовать интерполяцию ближайшего соседа, потому что * возвращает float, а не int.Вместо этого вы должны проверить, возвращает ли выражение (*t + *t) * 0.5f что-то конвертируемое обратно в T, используя такой тест, как std::is_constructible_v<T, decltype((*t + *t) * 0.5f)>


В качестве бонуса,Вот реализация , основанная на концепциях, которая больше не нуждается в тегах (как кратко упомянуто в комментариях).К сожалению, на этом уровне еще нет компилятора, поддерживающего requires, и, конечно, проект стандарта всегда может быть изменен:

template <typename T>
concept LinearInterpolatable = requires(T a, T b, float c) {
    { a + b } -> T;
    { a * c } -> T;
};

template <LinearInterpolatable T>
T interpolate(T a, T b, float c)
{
    // Linear interpolation
}

template <typename T>
T interpolate(T a, T b, float c)
{
    // Nearest-neighbor interpolation
}
...