Warum sollte ich std :: enable_if in Funktionssignaturen vermeiden?


165

Scott Meyers veröffentlichte Inhalt und Status seines nächsten Buches EC ++ 11. Er schrieb, dass ein Punkt im Buch "Vermeiden Sie std::enable_ifin Funktionssignaturen" sein könnte .

std::enable_if kann als Funktionsargument, als Rückgabetyp oder als Klassenvorlage oder Funktionsvorlagenparameter verwendet werden, um Funktionen oder Klassen bedingt aus der Überlastungsauflösung zu entfernen.

In dieser Frage werden alle drei Lösungen gezeigt.

Als Funktionsparameter:

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

Als Vorlagenparameter:

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

Als Rückgabetyp:

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;
   }   
};
  • Welche Lösung sollte bevorzugt werden und warum sollte ich andere vermeiden?
  • In welchen Fällen betrifft " std::enable_ifIn Funktionssignaturen vermeiden " die Verwendung als Rückgabetyp (der nicht Teil der normalen Funktionssignatur, sondern der Vorlagenspezialisierungen ist)?
  • Gibt es Unterschiede zwischen Funktionsvorlagen für Mitglieder und Nichtmitglieder?

Weil Überladung normalerweise genauso schön ist. Wenn überhaupt, delegieren Sie an eine Implementierung, die (spezialisierte) Klassenvorlagen verwendet.
sehe

Mitgliedsfunktionen unterscheiden sich darin, dass der Überlastsatz Überlastungen enthält, die nach der aktuellen Überlastung deklariert wurden . Dies ist besonders wichtig, wenn variadics verzögerter Rückgabetyp ausgeführt wird (wobei der Rückgabetyp aus einer anderen Überlastung abgeleitet werden soll)
siehe

1
Nun, nur subjektiv muss ich sagen, dass ich, obwohl ich oft sehr nützlich bin std::enable_if, meine Funktionssignaturen (insbesondere die hässliche nullptrVersion mit zusätzlichen Funktionsargumenten) nicht gerne überladen möchte , weil es immer so aussieht, wie es ist, ein seltsamer Hack (für etwas, das static ifvielleicht etwas ist) viel schöner und sauberer machen) mit Vorlage Black-Magic, um eine interessante Sprachfunktion auszunutzen. Aus diesem Grund bevorzuge ich das Versenden von Tags, wann immer dies möglich ist (nun, Sie haben immer noch zusätzliche seltsame Argumente, aber nicht in der öffentlichen Oberfläche und auch viel weniger hässlich und kryptisch ).
Christian Rau

2
Ich möchte fragen , was macht =0in typename std::enable_if<std::is_same<U, int>::value, int>::type = 0erreichen? Ich konnte keine richtigen Ressourcen finden, um es zu verstehen. Ich weiß, dass der erste Teil zuvor =0einen Mitgliedstyp hat, intwenn Uund intder gleiche ist. Danke vielmals!
Astroboylrx

4
@astroboylrx Witzig, ich wollte gerade einen Kommentar dazu abgeben. Grundsätzlich bedeutet = 0, dass dies ein standardmäßiger Vorlagenparameter ohne Typ ist. Dies geschieht auf diese Weise, da standardmäßige Typvorlagenparameter nicht Teil der Signatur sind und Sie sie daher nicht überladen können.
Nir Friedman

Antworten:


107

Setzen Sie den Hack in die Vorlagenparameter .

Der enable_ifOn-Template-Parameter-Ansatz hat mindestens zwei Vorteile gegenüber den anderen:

  • Lesbarkeit : Die Typen enable_if use und return / argument werden nicht zu einem unordentlichen Teil von Typnamen-Disambiguatoren und verschachtelten Typzugriffen zusammengeführt. Auch wenn die Unordnung des Disambiguators und des verschachtelten Typs mit Alias-Vorlagen verringert werden kann, würde dies dennoch zwei nicht miteinander verbundene Dinge zusammenführen. Die Verwendung von enable_if bezieht sich auf die Vorlagenparameter und nicht auf die Rückgabetypen. Wenn sie in den Vorlagenparametern enthalten sind, sind sie näher an dem, was wichtig ist.

  • Universelle Anwendbarkeit : Konstruktoren haben keine Rückgabetypen, und einige Operatoren können keine zusätzlichen Argumente haben, sodass keine der beiden anderen Optionen überall angewendet werden kann. Das Einfügen von enable_if in einen Vorlagenparameter funktioniert überall, da Sie SFINAE ohnehin nur für Vorlagen verwenden können.

Für mich ist der Lesbarkeitsaspekt der große Motivationsfaktor bei dieser Wahl.


4
Die Verwendung des FUNCTION_REQUIRESMakros hier erleichtert das Lesen erheblich, funktioniert auch in C ++ 03-Compilern und basiert auf der Verwendung enable_ifim Rückgabetyp. Die Verwendung enable_ifvon Funktionsvorlagenparametern führt außerdem zu Problemen beim Überladen, da die Funktionssignatur jetzt nicht eindeutig ist und mehrdeutige Überladungsfehler verursacht.
Paul Fultz II

3
Dies ist eine alte Frage, aber für alle, die noch lesen: Die Lösung für das von @Paul aufgeworfene Problem besteht darin, enable_ifeinen standardmäßigen Nicht-Typ-Vorlagenparameter zu verwenden, der eine Überladung ermöglicht. Dh enable_if_t<condition, int> = 0statt typename = enable_if_t<condition>.
Nir Friedman


@ R.MartinhoFernandes Der flamingdangerzoneLink in Ihrem Kommentar scheint jetzt zu einer Seite zu führen, auf der Spyware installiert wird. Ich habe es für die Aufmerksamkeit des Moderators markiert.
Nispio

58

std::enable_ifstützt sich beim Abzug von Vorlagenargumenten auf das Prinzip " Substitionsfehler ist kein Fehler " (auch bekannt als SFINAE) . Dies ist eine sehr fragile Sprachfunktion, und Sie müssen sehr vorsichtig sein, um sie richtig zu machen.

  1. Wenn Ihre Bedingung in der enable_ifeine verschachtelte Vorlage oder Typdefinition enthält (Hinweis: ::Suchen Sie nach Token), ist die Auflösung dieser verschachtelten Vorlagen oder Typen normalerweise ein nicht abgeleiteter Kontext . Jeder Substitutionsfehler in einem solchen nicht abgeleiteten Kontext ist ein Fehler .
  2. Die verschiedenen Bedingungen bei mehreren enable_ifÜberladungen können keine Überlappung aufweisen, da die Überlastungsauflösung nicht eindeutig wäre. Dies ist etwas, das Sie als Autor selbst überprüfen müssen, obwohl Sie gute Compiler-Warnungen erhalten würden.
  3. enable_ifmanipuliert den Satz funktionsfähiger Funktionen während der Überlastungsauflösung, die überraschende Wechselwirkungen haben können, abhängig vom Vorhandensein anderer Funktionen, die aus anderen Bereichen (z. B. über ADL) eingebracht werden. Dies macht es nicht sehr robust.

Kurz gesagt, wenn es funktioniert, funktioniert es, aber wenn es nicht funktioniert, kann es sehr schwer zu debuggen sein. Eine sehr gute Alternative ist die Verwendung des Tag-Dispatchings , dh das Delegieren an eine Implementierungsfunktion (normalerweise in einem detailNamespace oder in einer Hilfsklasse), die ein Dummy-Argument empfängt, das auf derselben Bedingung zur Kompilierungszeit basiert, die Sie in der verwenden 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 */) {}
}

Das Tag-Dispatching manipuliert den Überladungssatz nicht, hilft Ihnen jedoch bei der Auswahl genau der gewünschten Funktion, indem die richtigen Argumente über einen Ausdruck zur Kompilierungszeit (z. B. in einem Typmerkmal) bereitgestellt werden. Nach meiner Erfahrung ist dies viel einfacher zu debuggen und richtig zu machen. Wenn Sie ein aufstrebender Bibliotheksschreiber mit ausgefeilten Typmerkmalen sind, benötigen Sie möglicherweise etwas enable_if, aber für die meisten regelmäßigen Anwendungen zur Kompilierungszeit wird dies nicht empfohlen.


22
Das Versenden von Tags hat jedoch einen Nachteil: Wenn Sie ein Merkmal haben, das das Vorhandensein einer Funktion erkennt, und diese Funktion mit dem Tag-Dispatching-Ansatz implementiert wird, wird dieses Mitglied immer als vorhanden gemeldet und führt zu einem Fehler anstelle eines möglichen Substitutionsfehlers . SFINAE ist in erster Linie eine Technik zum Entfernen von Überladungen aus Kandidatensätzen, und das Versenden von Tags ist eine Technik zum Auswählen zwischen zwei (oder mehr) Überladungen. Es gibt einige Überschneidungen in der Funktionalität, aber sie sind nicht gleichwertig.
R. Martinho Fernandes

@ R.MartinhoFernandes können Sie ein kurzes Beispiel geben und veranschaulichen, wie enable_ifes richtig gemacht werden würde?
TemplateRex

1
@ R.MartinhoFernandes Ich denke, eine separate Antwort, die diese Punkte erklärt, könnte dem OP einen Mehrwert verleihen. :-) Übrigens, das Schreiben von Merkmalen wie is_f_ableist etwas, das ich als eine Aufgabe für Bibliotheksschreiber betrachte, die natürlich SFINAE verwenden können, wenn dies ihnen einen Vorteil verschafft, aber für "normale" Benutzer und mit einem bestimmten Merkmal is_f_abledenke ich, dass das Versenden von Tags einfacher ist.
TemplateRex

1
@hansmaad Ich habe eine kurze Antwort auf Ihre Frage gepostet und werde das Problem "an SFINAE oder nicht an SFINAE" stattdessen in einem Blog-Beitrag behandeln (es ist ein bisschen unangebracht zu dieser Frage). Sobald ich Zeit habe, es zu beenden, meine ich.
R. Martinho Fernandes

8
SFINAE ist "zerbrechlich"? Was?
Leichtigkeitsrennen im Orbit

5

Welche Lösung sollte bevorzugt werden und warum sollte ich andere vermeiden?

  • Der Vorlagenparameter

    • Es ist in Konstruktoren verwendbar.
    • Es kann in benutzerdefinierten Konvertierungsoperatoren verwendet werden.
    • Es erfordert C ++ 11 oder höher.
    • Es ist IMO, desto besser lesbar.
    • Es kann leicht falsch verwendet werden und führt zu Fehlern bei Überlastungen:

      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()

    Beachten Sie typename = std::enable_if_t<cond>statt richtigstd::enable_if_t<cond, int>::type = 0

  • Rückgabetyp:

    • Es kann nicht im Konstruktor verwendet werden. (kein Rückgabetyp)
    • Es kann nicht in benutzerdefinierten Konvertierungsoperatoren verwendet werden. (nicht ableitbar)
    • Es kann Pre-C ++ 11 verwendet werden.
    • Zweitens besser lesbare IMO.
  • Zuletzt im Funktionsparameter:

    • Es kann Pre-C ++ 11 verwendet werden.
    • Es ist in Konstruktoren verwendbar.
    • Es kann nicht in benutzerdefinierten Konvertierungsoperatoren verwendet werden. (keine Parameter)
    • Es kann nicht mit fester Anzahl von Argumenten (einstellige / binäre Operatoren in Verfahren verwendet werden +, -, *, ...)
    • Es kann sicher bei der Vererbung verwendet werden (siehe unten).
    • Ändern Sie die Funktionssignatur (Sie haben im Grunde ein Extra als letztes Argument void* = nullptr) (der Funktionszeiger würde sich also unterscheiden und so weiter).

Gibt es Unterschiede zwischen Funktionsvorlagen für Mitglieder und Nichtmitglieder?

Es gibt subtile Unterschiede bei der Vererbung und using:

Nach dem using-declarator(Schwerpunkt Mine):

namespace.udecl

Der vom using-Deklarator eingeführte Satz von Deklarationen wird gefunden, indem eine qualifizierte Namenssuche ([basic.lookup.qual], [class.member.lookup]) für den Namen im using-Deklarator durchgeführt wird, mit Ausnahme von Funktionen, die wie beschrieben ausgeblendet sind unten.

...

Wenn ein using-Deklarator Deklarationen von einer Basisklasse in eine abgeleitete Klasse bringt, überschreiben und / oder verbergen Elementfunktionen und Elementfunktionsvorlagen in der abgeleiteten Klasse Elementfunktionen und Elementfunktionsvorlagen mit demselben Namen, Parametertypliste, cv- Qualifikation und Ref-Qualifikation (falls vorhanden) in einer Basisklasse (anstatt widersprüchlich). Solche versteckten oder überschriebenen Deklarationen sind von den vom using-Deklarator eingeführten Deklarationen ausgeschlossen.

Sowohl für das Vorlagenargument als auch für den Rückgabetyp sind die folgenden Methoden ausgeblendet:

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 findet fälschlicherweise die Basisfunktion).

Während mit Argument ein ähnliches Szenario funktioniert:

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

Demo

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.