Подходы к функции SFINAE в C ++

40

Я активно использую функцию SFINAE в проекте и не уверен, есть ли какие-либо различия между следующими двумя подходами (кроме стиля):

#include <cstdlib>
#include <type_traits>
#include <iostream>

template <class T, class = std::enable_if_t<std::is_same_v<T, int>>>
void foo()
{
    std::cout << "method 1" << std::endl;
}

template <class T, std::enable_if_t<std::is_same_v<T, double>>* = 0>
void foo()
{
    std::cout << "method 2" << std::endl;
}

int main()
{
    foo<int>();
    foo<double>();

    std::cout << "Done...";
    std::getchar();

    return EXIT_SUCCESS;
}

Вывод программы соответствует ожиданиям:

method 1
method 2
Done...

Я видел метод 2, который используется чаще в stackoverflow, но я предпочитаю метод 1.

Существуют ли обстоятельства, когда эти два подхода различаются?

Кит
источник
Как вы запускаете эту программу? Это не скомпилируется для меня.
alter igel
@alter igel для этого понадобится компилятор C ++ 17. Я использовал MSVC 2019 для тестирования этого примера, но в основном я работаю с Clang.
Кейта
Связанный: сигнатуры «почему следует избегать stdenable-if-in-in-function-сигнатуры» и C ++ 20 также представляют новые способы с концепцией :-)
Jarod42
@ Jarod42 Понятия - одна из самых необходимых вещей для меня из C ++ 20.
говорит

Ответы:

35

Я видел метод 2, который используется чаще в stackoverflow, но я предпочитаю метод 1.

Предложение: предпочитаю метод 2.

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

Предположим, вы хотите включить foo()версию 1, когда bar<T>()(представьте, что это constexprфункция) true, и foo()версию 2, когда bar<T>()есть false.

С

template <typename T, typename = std::enable_if_t<true == bar<T>()>>
void foo () // version 1
 { }

template <typename T, typename = std::enable_if_t<false == bar<T>()>>
void foo () // version 2
 { }

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

Но следующее решение

template <typename T, std::enable_if_t<true == bar<T>(), bool> = true>
void foo () // version 1
 { }

template <typename T, std::enable_if_t<false == bar<T>(), bool> = true>
void foo () // version 2
 { }

работает, потому что SFINAE изменяет сигнатуру функций.

Несвязанное наблюдение: есть и третий метод: включить / отключить тип возвращаемого значения (за исключением конструкторов класса / структуры, очевидно)

template <typename T>
std::enable_if_t<true == bar<T>()> foo () // version 1
 { }

template <typename T>
std::enable_if_t<false == bar<T>()> foo () // version 2
 { }

Как и метод 2, метод 3 совместим с выбором альтернативных функций с одинаковой сигнатурой.

max66
источник
1
Спасибо за отличное объяснение, теперь я буду предпочитать методы 2 и 3 :-)
keith
«параметр шаблона по умолчанию не меняет подпись» - чем он отличается во втором варианте, который также использует параметры шаблона по умолчанию?
Эрик
1
@Eric - Не просто сказать ... Полагаю, другой ответ объяснит это лучше ... Если SFINAE включает / отключает аргумент шаблона по умолчанию, foo()функция остается доступной, когда вы вызываете ее с явным вторым параметром шаблона ( foo<double, double>();вызовом). И если остаются доступными, есть неоднозначность с другой версией. С помощью метода 2 SFINAE включает / отключает второй аргумент, а не параметр по умолчанию. Таким образом, вы не можете вызвать его, объясняя параметр, потому что произошла ошибка замещения, которая не позволяет второй параметр. Так что версия недоступна, поэтому нет двусмысленности
max66
3
Метод 3 имеет дополнительное преимущество, как правило, в том, чтобы не просочиться в имя символа. Этот вариант auto foo() -> std::enable_if_t<...>часто полезен, чтобы избежать сокрытия сигнатуры функции и разрешить использование аргументов функции.
дедупликатор
@ max66: так что ключевой момент в том, что ошибка замещения в параметре шаблона по умолчанию не является ошибкой, если параметр задан и по умолчанию не требуется?
Эрик
21

В дополнение к ответу max66 , другая причина предпочтения метода 2 заключается в том, что с методом 1 вы можете (случайно) передать явный параметр типа в качестве второго аргумента шаблона и полностью отказаться от механизма SFINAE. Это может произойти как опечатка, ошибка копирования / вставки или недосмотр в более крупном шаблонном механизме.

#include <cstdlib>
#include <type_traits>
#include <iostream>

// NOTE: foo should only accept T=int
template <class T, class = std::enable_if_t<std::is_same_v<T, int>>>
void foo(){
    std::cout << "method 1" << std::endl;
}

int main(){

    // works fine
    foo<int>();

    // ERROR: subsitution failure, as expected
    // foo<double>();

    // Oops! also works, even though T != int :(
    foo<double, double>();

    return 0;
}

Живая демо здесь

Alter Igel
источник
Хорошая точка зрения. Механизм может быть угнан.
max66