Dlaczego powinienem unikać std :: enable_if w sygnaturach funkcji

165

Scott Meyers opublikował treść i status swojej następnej książki EC ++ 11. Napisał, że jedną z pozycji w książce może być „Unikaj std::enable_ifw sygnaturach funkcji” .

std::enable_if może być używany jako argument funkcji, jako typ zwracany lub jako szablon klasy lub parametr szablonu funkcji do warunkowego usuwania funkcji lub klas z rozpoznawania przeciążenia.

W tym pytaniu pokazane są wszystkie trzy rozwiązania.

Jako parametr funkcji:

template<typename T>
struct Check1
{
   template<typename U = T>
   U read(typename std::enable_if<
          std::is_same<U, int>::value >::type* = 0) { return 42; }

   template<typename U = T>
   U read(typename std::enable_if<
          std::is_same<U, double>::value >::type* = 0) { return 3.14; }   
};

Jako parametr szablonu:

template<typename T>
struct Check2
{
   template<typename U = T, typename std::enable_if<
            std::is_same<U, int>::value, int>::type = 0>
   U read() { return 42; }

   template<typename U = T, typename std::enable_if<
            std::is_same<U, double>::value, int>::type = 0>
   U read() { return 3.14; }   
};

Jako typ zwrotny:

template<typename T>
struct Check3
{
   template<typename U = T>
   typename std::enable_if<std::is_same<U, int>::value, U>::type read() {
      return 42;
   }

   template<typename U = T>
   typename std::enable_if<std::is_same<U, double>::value, U>::type read() {
      return 3.14;
   }   
};
  • Które rozwiązanie powinno być preferowane i dlaczego powinienem unikać innych?
  • W jakich przypadkach „Unikaj std::enable_ifw sygnaturach funkcji” dotyczy użycia jako typu zwracanego (co nie jest częścią normalnej sygnatury funkcji, ale specjalizacji szablonów)?
  • Czy są jakieś różnice w szablonach funkcji składowych i niebędących członkami?
hansmaad
źródło
Ponieważ przeładowanie jest zwykle równie przyjemne. Jeśli już, deleguj do implementacji, która używa (wyspecjalizowanych) szablonów klas.
2013
Funkcje członkowskie różnią się tym, że zestaw przeciążeń zawiera przeciążenia zadeklarowane po bieżącym przeciążeniu. Jest to szczególnie ważne, gdy robi variadics opóźniony typ zwracany (gdzie typ zwracany jest do wywnioskować z innego przeciążenie)
sehe
1
Cóż, tylko subiektywnie muszę powiedzieć, że chociaż często jestem całkiem przydatny, nie lubię std::enable_ifzaśmiecać moich sygnatur funkcji (zwłaszcza brzydkiej nullptrwersji argumentu funkcji dodatkowej ), ponieważ zawsze wygląda na to, co to jest, dziwny hack (coś static ifmoże zrobić dużo piękniejsze i bardziej przejrzyste), używając czarnej magii szablonów, aby wykorzystać ciekawą funkcję języka. Dlatego wolę wysyłanie tagów, gdy tylko jest to możliwe (cóż, nadal masz dodatkowe dziwne argumenty, ale nie w interfejsie publicznym, a także znacznie mniej brzydkie i tajemnicze ).
Christian Rau
2
Chcę zapytać, co się =0w tym stanie typename std::enable_if<std::is_same<U, int>::value, int>::type = 0? Nie mogłem znaleźć odpowiednich zasobów, aby to zrozumieć. Wiem, że pierwsza część =0ma typ członka, intjeśli Ui intjest taki sam. Wielkie dzięki!
astroboylrx
4
@astroboylrx Zabawne, właśnie zamierzałem umieścić komentarz, zwracając na to uwagę. Zasadniczo, że = 0 oznacza, że ​​jest to domyślny parametr szablonu inny niż typ . Jest to robione w ten sposób, ponieważ domyślne parametry szablonu typu nie są częścią podpisu, więc nie można ich przeciążać.
Nir Friedman

Odpowiedzi:

107

Umieść hack w parametrach szablonu .

enable_ifOd parametru szablonu podejście ma co najmniej dwie zalety nad innymi:

  • czytelność : użycie enable_if i typy return / argument nie są łączone w jeden niechlujny fragment ujednoznaczników nazw typów i zagnieżdżonych typów dostępu; mimo że bałagan związany z ujednoznaczniaczem i typem zagnieżdżonym można złagodzić za pomocą szablonów aliasów, nadal łączyłoby to ze sobą dwie niepowiązane rzeczy. Użycie enable_if jest związane z parametrami szablonu, a nie z typami zwracanymi. Posiadanie ich w parametrach szablonu oznacza, że ​​są bliżej tego, co ma znaczenie;

  • uniwersalność zastosowania : konstruktory nie mają typów zwracanych, a niektóre operatory nie mogą mieć dodatkowych argumentów, więc żadna z pozostałych dwóch opcji nie może być stosowana wszędzie. Umieszczenie parametru enable_if w parametrze szablonu działa wszędzie, ponieważ i tak możesz używać SFINAE tylko w szablonach.

Dla mnie aspekt czytelności jest dużym czynnikiem motywującym do tego wyboru.

R. Martinho Fernandes
źródło
4
Korzystanie z FUNCTION_REQUIRESmakro tutaj , sprawia, że znacznie ładniejszy czytać, i to działa w C ++ 03 kompilatory, jak również i to polega na użyciu enable_ifw rodzaju powrotnego. Ponadto użycie enable_ifparametrów szablonu funkcji powoduje problemy z przeciążeniem, ponieważ teraz sygnatura funkcji nie jest unikalna, co powoduje niejednoznaczne błędy związane z przeciążeniem.
Paul Fultz II
3
To stare pytanie, ale dla każdego, kto wciąż czyta: rozwiązaniem problemu podniesionego przez @Paul jest użycie enable_ifz domyślnym parametrem szablonu innego niż typ, który umożliwia przeciążenie. To znaczy enable_if_t<condition, int> = 0zamiast typename = enable_if_t<condition>.
Nir Friedman
link zwrotny do prawie statycznego-if: web.archive.org/web/20150726012736/http://flamingdangerzone.com/…
davidbak
@ R.MartinhoFernandes flamingdangerzoneodsyłacz w Twoim komentarzu wydaje się teraz prowadzić do strony instalacji oprogramowania szpiegującego. Oznaczyłem to do uwagi moderatora.
nispio
58

std::enable_ifopiera się na zasadzieAwaria podmiany nie jest błędem ” (aka SFINAE) podczas dedukcji argumentów szablonu . Jest to bardzo delikatna funkcja językowa i musisz bardzo uważać, aby zrobić ją dobrze.

  1. jeśli twój warunek wewnątrz enable_ifzawiera zagnieżdżony szablon lub definicję typu (wskazówka: szukaj:: tokenów), wtedy rozdzielczość tych zagnieżdżonych szablonów lub typów jest zwykle niewyedukowanym kontekstem . Każde niepowodzenie podstawienia w takim niewyedukowanym kontekście jest błędem .
  2. różne warunki w wielu enable_ifprzeciążeniach nie mogą się nakładać, ponieważ rozdzielczość przeciążenia byłaby niejednoznaczna. Jest to coś, co ty jako autor musisz sprawdzić samodzielnie, chociaż otrzymujesz dobre ostrzeżenia kompilatora.
  3. enable_ifmanipuluje zestawem wykonalnych funkcji podczas rozwiązywania przeciążenia, które mogą mieć zaskakujące interakcje w zależności od obecności innych funkcji, które są sprowadzane z innych zakresów (np. przez ADL). To sprawia, że ​​nie jest zbyt wytrzymały.

Krótko mówiąc, kiedy to działa, działa, ale jeśli nie, debugowanie może być bardzo trudne. Bardzo dobrą alternatywą jest użycie wysyłania tagów , tj. Delegowanie do funkcji implementacji (zwykle w detailprzestrzeni nazw lub w klasie pomocniczej), która odbiera fałszywy argument oparty na tym samym warunku kompilacji, którego używasz w enable_if.

template<typename T>
T fun(T arg) 
{ 
    return detail::fun(arg, typename some_template_trait<T>::type() ); 
}

namespace detail {
    template<typename T>
    fun(T arg, std::false_type /* dummy */) { }

    template<typename T>
    fun(T arg, std::true_type /* dummy */) {}
}

Wysyłanie tagów nie manipuluje zestawem przeciążenia, ale pomaga wybrać dokładnie żądaną funkcję, dostarczając odpowiednie argumenty za pomocą wyrażenia w czasie kompilacji (np. W charakterystyce typu). Z mojego doświadczenia wynika, że ​​jest to znacznie łatwiejsze do debugowania i poprawienia. Jeśli jesteś początkującym pisarzem bibliotecznym o wyrafinowanych cechach typu, możesz tego potrzebować enable_if, ale w przypadku większości regularnych zastosowań warunków kompilacji nie jest to zalecane.

TemplateRex
źródło
22
Wysyłanie tagów ma jednak jedną wadę: jeśli masz jakąś cechę, która wykrywa obecność funkcji, a ta funkcja jest zaimplementowana w podejściu do wysyłania tagów, zawsze zgłasza ten element jako obecny i powoduje błąd zamiast potencjalnego niepowodzenia zastępowania . SFINAE to przede wszystkim technika usuwania przeciążeń z zestawów kandydatów, a wysyłanie tagów jest techniką wyboru między dwoma (lub więcej) przeciążeniami. Funkcje częściowo się pokrywają, ale nie są one równoważne.
R. Martinho Fernandes
@ R.MartinhoFernandes Czy możesz podać krótki przykład i zilustrować, jak enable_ifmożna to zrobić dobrze?
TemplateRex,
1
@ R.MartinhoFernandes Myślę, że oddzielna odpowiedź wyjaśniająca te punkty może wnieść wartość dodaną do PO. :-) is_f_ableSwoją drogą , pisanie takich cech jest zadaniem, które uważam za zadanie dla pisarzy bibliotek, którzy oczywiście mogą używać SFINAE, jeśli daje im to przewagę, ale dla „zwykłych” użytkowników i posiadających cechę is_f_able, myślę, że wysyłanie tagów jest łatwiejsze.
TemplateRex,
1
@hansmaad Opublikowałem krótką odpowiedź na Twoje pytanie, a zamiast tego odniosę się do kwestii „do SFINAE albo nie SFINAE” w poście na blogu (jest to trochę nie na temat). To znaczy, gdy tylko zdążę to skończyć.
R. Martinho Fernandes
8
SFINAE jest „krucha”? Co?
Wyścigi lekkości na orbicie,
5

Które rozwiązanie powinno być preferowane i dlaczego powinienem unikać innych?

  • Parametr szablonu

    • Można go używać w konstruktorach.
    • Można go używać w operatorze konwersji zdefiniowanym przez użytkownika.
    • Wymaga C ++ 11 lub nowszego.
    • To IMO, tym bardziej czytelne.
    • Może być łatwo użyty nieprawidłowo i powoduje błędy przy przeciążeniach:

      template<typename T, typename = std::enable_if_t<std::is_same<T, int>::value>>
      void f() {/*...*/}
      
      template<typename T, typename = std::enable_if_t<std::is_same<T, float>::value>>
      void f() {/*...*/} // Redefinition: both are just template<typename, typename> f()

    Zauważ typename = std::enable_if_t<cond>zamiast poprawiaćstd::enable_if_t<cond, int>::type = 0

  • typ zwrotu:

    • Nie można go używać w konstruktorze. (bez typu zwrotu)
    • Nie można go używać w operatorze konwersji zdefiniowanym przez użytkownika. (nie podlega odliczeniu)
    • Może być używany przed C ++ 11.
    • Druga bardziej czytelna IMO.
  • Na koniec parametr funkcji:

    • Może być używany przed C ++ 11.
    • Można go używać w konstruktorach.
    • Nie można go używać w operatorze konwersji zdefiniowanym przez użytkownika. (bez parametrów)
    • To nie może być stosowany w metodach z określoną liczbę argumentów (jednoargumentowych / operatorów binarnych +, -,* , ...)
    • Można go bezpiecznie używać w dziedziczeniu (patrz poniżej).
    • Zmień sygnaturę funkcji (masz w zasadzie dodatkowy jako ostatni argument void* = nullptr) (więc wskaźnik funkcji będzie się różnić i tak dalej)

Czy są jakieś różnice w szablonach funkcji składowych i niebędących członkami?

Istnieją subtelne różnice w dziedziczeniu i using :

Według using-declarator(wyróżnienie moje):

namespace.udecl

Zestaw deklaracji wprowadzonych przez using-deklarator znajduje się przez wykonanie wyszukiwania kwalifikowanej nazwy ([basic.lookup.qual], [class.member.lookup]) dla nazwy w using-deklarator, z wyłączeniem funkcji, które są ukryte zgodnie z opisem poniżej.

...

Gdy deklarator używania przenosi deklaracje z klasy bazowej do klasy pochodnej, funkcje składowe i szablony funkcji składowych w klasie pochodnej przesłaniają i / lub ukrywają funkcje składowe i szablony funkcji składowych o tej samej nazwie, lista typów parametrów, cv- kwalifikacja i kwalifikator ref (jeśli istnieje) w klasie bazowej (zamiast sprzecznych). Takie ukryte lub nadpisane deklaracje są wykluczone z zestawu deklaracji wprowadzonych przez using-deklarator.

Tak więc zarówno dla argumentu szablonu, jak i typu zwracanego, metody są ukryte, wygląda to następująco:

struct Base
{
    template <std::size_t I, std::enable_if_t<I == 0>* = nullptr>
    void f() {}

    template <std::size_t I>
    std::enable_if_t<I == 0> g() {}
};

struct S : Base
{
    using Base::f; // Useless, f<0> is still hidden
    using Base::g; // Useless, g<0> is still hidden

    template <std::size_t I, std::enable_if_t<I == 1>* = nullptr>
    void f() {}

    template <std::size_t I>
    std::enable_if_t<I == 1> g() {}
};

Demo (gcc błędnie znajduje funkcję podstawową).

Podczas gdy w przypadku argumentacji działa podobny scenariusz:

struct Base
{
    template <std::size_t I>
    void h(std::enable_if_t<I == 0>* = nullptr) {}
};

struct S : Base
{
    using Base::h; // Base::h<0> is visible

    template <std::size_t I>
    void h(std::enable_if_t<I == 1>* = nullptr) {}
};

Próbny

Jarod42
źródło