std :: enable_if zum bedingten Kompilieren einer Mitgliedsfunktion

156

Ich versuche, ein einfaches Beispiel zum Arbeiten zu bringen, um zu verstehen, wie man es benutzt std::enable_if. Nachdem ich diese Antwort gelesen hatte , dachte ich, es sollte nicht zu schwierig sein, ein einfaches Beispiel zu finden. Ich möchte verwenden std::enable_if, um zwischen zwei Elementfunktionen zu wählen und nur eine davon zuzulassen.

Leider lässt sich das Folgende nicht mit gcc 4.7 kompilieren und nach stundenlangen Versuchen frage ich euch, was mein Fehler ist.

#include <utility>
#include <iostream>

template< class T >
class Y {

    public:
        template < typename = typename std::enable_if< true >::type >
        T foo() {
            return 10;
        }
        template < typename = typename std::enable_if< false >::type >
        T foo() {
            return 10;
        }

};


int main() {
    Y< double > y;

    std::cout << y.foo() << std::endl;
}

gcc meldet folgende Probleme:

% LANG=C make CXXFLAGS="-std=c++0x" enable_if
g++ -std=c++0x    enable_if.cpp   -o enable_if
enable_if.cpp:12:65: error: `type' in `struct std::enable_if<false>' does not name a type
enable_if.cpp:13:15: error: `template<class T> template<class> T Y::foo()' cannot be overloaded
enable_if.cpp:9:15: error: with `template<class T> template<class> T Y::foo()'

Warum löscht g ++ nicht die falsche Instanziierung für die Funktion des zweiten Mitglieds? std::enable_if< bool, T = void >::typeExistiert standardmäßig nur, wenn der boolesche Template-Parameter true ist. Aber warum betrachtet g ++ dies nicht als SFINAE? Ich denke, dass die Überladungsfehlermeldung von dem Problem herrührt, dass g ++ die zweite Mitgliedsfunktion nicht löscht und glaubt, dass dies eine Überladung sein sollte.

evnu
quelle
1
Ich bin nicht sicher, aber ich denke, es ist das Folgende: enable_if basiert auf SFINAE (Substitutionsfehler ist kein Fehler). Sie haben hier jedoch keine Substitution, da kein Parameter verwendet werden kann, um zu bestimmen, welche Überladung verwendet werden soll. Sie sollten das "wahre" und "falsche" von T. abhängen lassen (ich weiß, dass Sie es im einfachen Beispiel nicht tun wollten, aber es ist jetzt wahrscheinlich zu einfach ...)
Philipp
3
Ich habe auch daran gedacht und versucht zu verwenden std::is_same< T, int >::valueund ! std::is_same< T, int >::valuedas ergibt das gleiche Ergebnis.
Evnu

Antworten:

117

SFINAE funktioniert nur, wenn durch Ersetzen eines Vorlagenarguments durch Argumentation das Konstrukt schlecht geformt wird. Es gibt keine solche Substitution.

Ich habe auch daran gedacht und versucht zu verwenden std::is_same< T, int >::valueund ! std::is_same< T, int >::valuedas ergibt das gleiche Ergebnis.

Dies liegt daran, dass beim Instanziieren der Klassenvorlage (was unter anderem beim Erstellen eines Objekts vom Typ geschieht Y<int>) alle Elementdeklarationen instanziiert werden (nicht unbedingt deren Definitionen / Körper!). Darunter befinden sich auch die Mitgliedsvorlagen. Beachten Sie, dass dies Tdann bekannt ist und !std::is_same< T, int >::valuefalse ergibt. Es wird also eine Klasse erstellt, Y<int>die enthält

class Y<int> {
    public:
        /* instantiated from
        template < typename = typename std::enable_if< 
          std::is_same< T, int >::value >::type >
        T foo() {
            return 10;
        }
        */

        template < typename = typename std::enable_if< true >::type >
        int foo();

        /* instantiated from

        template < typename = typename std::enable_if< 
          ! std::is_same< T, int >::value >::type >
        T foo() {
            return 10;
        }
        */

        template < typename = typename std::enable_if< false >::type >
        int foo();
};

Der std::enable_if<false>::typegreift auf einen nicht existierenden Typ zu, so dass die Deklaration fehlerhaft ist. Und somit ist Ihr Programm ungültig.

Sie müssen dafür sorgen, dass die Mitgliedsvorlagen enable_ifvon einem Parameter der Mitgliedsvorlage selbst abhängen. Dann sind die Deklarationen gültig, da der gesamte Typ noch abhängig ist. Wenn Sie versuchen, einen von ihnen aufzurufen, erfolgt die Argumentableitung für ihre Vorlagenargumente und SFINAE wie erwartet. Siehe diese Frage und die entsprechende Antwort dazu.

Johannes Schaub - litb
quelle
14
... Nur zur Verdeutlichung, falls es nützlich ist: Wenn eine Instanz der YVorlagenklasse instanziiert wird, kompiliert der Compiler die Funktionen der Vorlagenelemente nicht. Der Compiler führt jedoch die Ersetzung Tin die Elementvorlage DECLARATIONS durch, damit diese Elementvorlagen zu einem späteren Zeitpunkt instanziiert werden können. Dieser Fehlerpunkt ist nicht SFINAE, da SFINAE nur beim Bestimmen des Satzes möglicher Funktionen für die Überlastauflösung gilt und das Instanziieren einer Klasse nicht das Bestimmen eines Satzes von Funktionen für die Überlastauflösung ist. (Zumindest denke ich!)
Dan Nissenbaum
93

Ich habe dieses kurze Beispiel gemacht, das auch funktioniert.

#include <iostream>
#include <type_traits>

class foo;
class bar;

template<class T>
struct is_bar
{
    template<class Q = T>
    typename std::enable_if<std::is_same<Q, bar>::value, bool>::type check()
    {
        return true;
    }

    template<class Q = T>
    typename std::enable_if<!std::is_same<Q, bar>::value, bool>::type check()
    {
        return false;
    }
};

int main()
{
    is_bar<foo> foo_is_bar;
    is_bar<bar> bar_is_bar;
    if (!foo_is_bar.check() && bar_is_bar.check())
        std::cout << "It works!" << std::endl;

    return 0;
}

Kommentar, wenn Sie möchten, dass ich näher darauf eingehe. Ich denke, der Code ist mehr oder weniger selbsterklärend, aber andererseits habe ich ihn so gemacht, dass ich mich möglicherweise irre :)

Sie können es hier in Aktion sehen .

jpihl
quelle
2
Dies wird auf VS2012 nicht kompiliert. error C4519: default template arguments are only allowed on a class template.
PythonNut
1
Das ist bedauerlich. Ich habe es nur mit gcc getestet. Vielleicht hilft das: stackoverflow.com/a/17543296/660982
jpihl
1
Dies ist sicherlich die beste Antwort hier und genau das, wonach ich gesucht habe.
Weipeng L
3
Warum muss eine andere Vorlagenklasse erstellt werden Q, obwohl diese gleich ist T?
ilya1725
1
Weil Sie die testMitgliedsfunktion vorlegen müssen. Beide können nicht gleichzeitig existieren. QLeitet einfach den Klassenvorlagentyp weiter T. Sie könnten die Klassenvorlage Twie folgt entfernen : cpp.sh/4nxw, aber das macht den Zweck irgendwie zunichte .
Jpihl
13

Für diejenigen, die nach einer Lösung suchen, die "einfach funktioniert":

#include <utility>
#include <iostream>

template< typename T >
class Y {

    template< bool cond, typename U >
    using resolvedType  = typename std::enable_if< cond, U >::type; 

    public:
        template< typename U = T > 
        resolvedType< true, U > foo() {
            return 11;
        }
        template< typename U = T >
        resolvedType< false, U > foo() {
            return 12;
        }

};


int main() {
    Y< double > y;

    std::cout << y.foo() << std::endl;
}

Kompilieren mit:

g++ -std=gnu++14 test.cpp 

Laufen gibt:

./a.out 
11
user1284631
quelle
6
Ähm, warum würden Sie umbenennen std::enable_if_tzu resolvedType.
Qwertie
1
Weil nicht jeder C ++ 17 aus Gründen verwenden kann, die stark variieren können.
James Yang
9

Aus diesem Beitrag:

Standardvorlagenargumente sind nicht Teil der Signatur einer Vorlage

Aber so etwas kann man machen:

#include <iostream>

struct Foo {
    template < class T,
               class std::enable_if < !std::is_integral<T>::value, int >::type = 0 >
    void f(const T& value)
    {
        std::cout << "Not int" << std::endl;
    }

    template<class T,
             class std::enable_if<std::is_integral<T>::value, int>::type = 0>
    void f(const T& value)
    {
        std::cout << "Int" << std::endl;
    }
};

int main()
{
    Foo foo;
    foo.f(1);
    foo.f(1.1);

    // Output:
    // Int
    // Not int
}
Janek Olszak
quelle
Es funktioniert, aber dies sind im Grunde genommen Vorlagenfunktionen, nicht die Klasse selbst ... Es erlaubt auch nicht, eine von zwei Funktionen mit identischem Prototyp zu löschen (wenn Sie eine Überladung übergehen müssen). Die Idee ist jedoch schön. Könnten Sie das OP-Beispiel bitte in eine Arbeitsform umschreiben?
user1284631
5

Eine Möglichkeit, dieses Problem zu lösen, besteht darin, die Spezialisierung in eine andere Klasse zu verschieben und dann von dieser Klasse zu erben. Möglicherweise müssen Sie die Reihenfolge der Vererbung ändern, um Zugriff auf alle anderen zugrunde liegenden Daten zu erhalten. Diese Technik funktioniert jedoch.

template< class T, bool condition> struct FooImpl;
template<class T> struct FooImpl<T, true> {
T foo() { return 10; }
};

template<class T> struct FoolImpl<T,false> {
T foo() { return 5; }
};

template< class T >
class Y : public FooImpl<T, boost::is_integer<T> > // whatever your test is goes here.
{
public:
    typedef FooImpl<T, boost::is_integer<T> > inherited;

    // you will need to use "inherited::" if you want to name any of the 
    // members of those inherited classes.
};

Der Nachteil dieser Technik besteht darin, dass Sie, wenn Sie viele verschiedene Dinge für verschiedene Elementfunktionen testen müssen, für jede eine Klasse erstellen und diese im Vererbungsbaum verketten müssen. Dies gilt für den Zugriff auf allgemeine Datenelemente.

Ex:

template<class T, bool condition> class Goo;
// repeat pattern above.

template<class T, bool condition>
class Foo<T, true> : public Goo<T, boost::test<T> > {
public:
    typedef Goo<T, boost::test<T> > inherited:
    // etc. etc.
};
Gary Powell
quelle
4

Der Boolesche Wert muss vom abgeleiteten Vorlagenparameter abhängen. Eine einfache Möglichkeit zur Behebung besteht darin, einen booleschen Standardparameter zu verwenden:

template< class T >
class Y {

    public:
        template < bool EnableBool = true, typename = typename std::enable_if<( std::is_same<T, double>::value && EnableBool )>::type >
        T foo() {
            return 10;
        }

};

Dies funktioniert jedoch nicht, wenn Sie die Member-Funktion überladen möchten. Verwenden Sie stattdessen am besten TICK_MEMBER_REQUIRESdie Tick- Bibliothek:

template< class T >
class Y {

    public:
        TICK_MEMBER_REQUIRES(std::is_same<T, double>::value)
        T foo() {
            return 10;
        }

        TICK_MEMBER_REQUIRES(!std::is_same<T, double>::value)
        T foo() {
            return 10;
        }

};

Sie können auch implementieren, dass Ihr eigenes Mitglied ein solches Makro benötigt (nur für den Fall, dass Sie keine andere Bibliothek verwenden möchten):

template<long N>
struct requires_enum
{
    enum class type
    {
        none,
        all       
    };
};


#define MEMBER_REQUIRES(...) \
typename requires_enum<__LINE__>::type PrivateRequiresEnum ## __LINE__ = requires_enum<__LINE__>::type::none, \
class=typename std::enable_if<((PrivateRequiresEnum ## __LINE__ == requires_enum<__LINE__>::type::none) && (__VA_ARGS__))>::type
Paul Fultz II
quelle
So hat es bei mir nicht funktioniert. Maaybe fehlt etwas? Könnten Sie das OP-Beispiel bitte in eine Arbeitsform umschreiben?
user1284631
Das ursprüngliche Beispiel funktioniert nicht mit Überladung. Ich habe meine Antwort aktualisiert, wie Sie es mit Überladung tun können.
Paul Fultz II
0

Hier ist mein minimalistisches Beispiel mit einem Makro. Verwenden Sie doppelte Klammern, enable_if((...))wenn Sie komplexere Ausdrücke verwenden.

template<bool b, std::enable_if_t<b, int> = 0>
using helper_enable_if = int;

#define enable_if(value) typename = helper_enable_if<value>

struct Test
{
     template<enable_if(false)>
     void run();
}
Aedoro
quelle