Podejścia do działania SFINAE w C ++

40

Używam mocno funkcji SFINAE w projekcie i nie jestem pewien, czy istnieją jakieś różnice między następującymi dwoma podejściami (innymi niż styl):

#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;
}

Wynik programu jest zgodny z oczekiwaniami:

method 1
method 2
Done...

Widziałem metodę 2 częściej używaną w przepełnieniu stosu, ale wolę metodę 1.

Czy są jakieś okoliczności, w których te dwa podejścia się różnią?

Keith
źródło
Jak uruchomić ten program? Nie skompiluje się dla mnie.
Zmień igel
@alter igel będzie potrzebował kompilatora C ++ 17. Użyłem MSVC 2019 do przetestowania tego przykładu, ale głównie pracuję z Clangiem.
keith
Powiązane: dlaczego-i-unikać-stdenable-jeśli-w-funkcji-podpisy i C ++ 20 wprowadza również nowe sposoby z koncepcją :-)
Jarod42
@ Jarod42 Koncepcje są dla mnie jedną z najbardziej potrzebnych rzeczy z C ++ 20.
val mówi Przywróć Monikę

Odpowiedzi:

35

Widziałem metodę 2 częściej używaną w przepełnieniu stosu, ale wolę metodę 1.

Sugestia: preferuj metodę 2.

Obie metody działają z pojedynczymi funkcjami. Problem pojawia się, gdy masz więcej niż funkcję o tej samej sygnaturze i chcesz włączyć tylko jedną funkcję zestawu.

Załóżmy, że chcesz włączyć opcję foo()1, gdy bar<T>()(udawaj, że jest to constexprfunkcja) true, a foo()wersja 2, kiedy bar<T>()jest false.

Z

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
 { }

pojawia się błąd kompilacji, ponieważ masz dwuznaczność: dwie foo()funkcje z tym samym podpisem (domyślny parametr szablonu nie zmienia podpisu).

Ale następujące rozwiązanie

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
 { }

działa, ponieważ SFINAE modyfikuje podpis funkcji.

Niepowiązana obserwacja: istnieje również trzecia metoda: włączanie / wyłączanie typu zwracanego (oczywiście z wyjątkiem konstruktorów klas / struktur)

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
 { }

Jako metoda 2 metoda 3 jest kompatybilna z wyborem alternatywnych funkcji o tej samej sygnaturze.

max66
źródło
1
Dzięki za świetne wyjaśnienie, odtąd wolę metody 2 i 3 :-)
keith
„domyślny parametr szablonu nie zmienia podpisu” - czym to się różni w drugim wariancie, który również używa domyślnych parametrów szablonu?
Eric
1
@Eric - Niełatwo powiedzieć ... Przypuszczam, że druga odpowiedź wyjaśnia to lepiej ... Jeśli SFINAE włączy / wyłączy domyślny argument szablonu, foo()funkcja pozostanie dostępna po wywołaniu go z jawnym drugim parametrem szablonu ( foo<double, double>();wywołanie). A jeśli pozostaną dostępne, istnieje niejednoznaczność z inną wersją. W metodzie 2 SFINAE włącza / wyłącza drugi argument, a nie parametr domyślny. Nie można więc nazwać tego objaśnieniem parametru, ponieważ występuje błąd podstawienia, który nie pozwala na drugi parametr. Więc wersja jest niedostępna, więc nie ma dwuznaczności
max66
3
Metoda 3 ma dodatkową zaletę polegającą na tym, że zasadniczo nie przecieka do nazwy symbolu. Wariant auto foo() -> std::enable_if_t<...>jest często przydatny, aby uniknąć ukrywania podpisu funkcji i umożliwić użycie argumentów funkcji.
Deduplicator,
@ max66: więc kluczową kwestią jest to, że niepowodzenie podstawienia w parametrze domyślnym parametru szablonu nie jest błędem, jeśli parametr jest podany i nie jest wymagana żadna wartość domyślna?
Eric
21

Oprócz odpowiedzi max66 , innym powodem preferowania metody 2 jest to, że w metodzie 1 możesz (przypadkowo) przekazać parametr typu jawnego jako drugi argument szablonu i całkowicie pokonać mechanizm SFINAE. Może się to zdarzyć jako błąd literówki, błąd kopiowania / wklejania lub niedopatrzenie w większym mechanizmie szablonów.

#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;
}

Demo na żywo tutaj

alter igel
źródło
Słuszna uwaga. Mechanizm można przejąć.
max66