Operator-Überladung / Safe bool Idiom / Cast auf Funktionspointer



  • Hallo,

    ich beschäftige mich gerade mit Operator-Überladung. Dabei bin ich auf den

    operator bool()
    

    gestoßen, der mir ein paar Schwierigkeiten bereitet. Das man Konvertierungs-Operatoren mit Bedacht einsetzen sollte, habe ich schon hier im Magazin gelesen, aber den bool()-Operator wollte ich dann doch mal ausprobieren.

    Das Problem sieht so aus:

    struct MyStruct {
        int data;
        MyStruct(int d = 1) : data(d) {}
        operator bool() const {
            return data != 0;
        }
    };
    
    int main(){
        MyStruct a(5), b(10);
        if(a == b) //Compiliert und ergibt true
            cout << "a == b" << endl;
        return 0;
    }
    

    a und b werden impliziet zu bool gecastet und anschließend verglichen. Das war nicht in meinem Sinne.

    Etwas Recherche im Netz hat mich zum Safe bool Idiom geführt.

    Dort fand ich drei Lösungsansätze, von denen ich den ersten und den letzten betrachtet habe.
    Erst der letzte, operator bool() explicit machen (C++11):

    struct Testable{
        explicit operator bool() const {
              return false;
        }
    };
    
    int main(){
      Testable a, b;
      if (a)      { /*do something*/ }  // this is correct
      if (a == b) { /*do something*/ }  // compiler error
      return 0;
    }
    

    IntelliSense meckert zwar tatsächlich, dass es keine Operatorübereinstimmung für == gibt, allerdings bekomme ich den Compiler-Fehler schon viel früher, nämlich in Zeile 2:

    error C2071: 'Testable::operator bool': Ungültige Speicherklasse

    Ist mein Compiler zu alt? Ich nutze VS 2012 und dachte der hat vollständige C++11-Unterstützung. Oder liegt der Fehler wo anders?

    Der erste Lösungsansatz funktioniert bei mir. Dafür verstehe ich ihn nicht 😕

    class Testable {
        bool ok_;
        typedef void (Testable::*bool_type)() const;
        void this_type_does_not_support_comparisons() const {}
      public:
        explicit Testable(bool b=true):ok_(b) {}
    
        operator bool_type() const {
          return ok_==true ? 
            &Testable::this_type_does_not_support_comparisons : 0;
        }
    };
    template <typename T>
    bool operator!=(const Testable& lhs, const T&) {
        lhs.this_type_does_not_support_comparisons();	
        return false;
    }
    template <typename T>
    bool operator==(const Testable& lhs, const T&) {
        lhs.this_type_does_not_support_comparisons();
        return false;
    }
    
    int main (void)
    {
      Testable t1, t2;
    
      if (t1) {} // Works as expected
      if (t2 == t1) {} // Fails to compile
      if (t1 < 0) {} // Fails to compile
    
      return 0;
    }
    

    Was ich davon verstehe:

    • Statt des bool()-Operators, wird ein Cast-Operator auf einen Funktions-Zeiger implementiert.
    • Durch die Rückgabe eines Zeigers auf this_type_... wird ein Wert != 0 zurückgegeben, der true ergibt
    • Die Templates sorgen für einen Compiler-Fehler, da dort private Methoden aufgerufen werden (Zeile 29).

    Was ich nicht verstehe:

    • Warum wird der bool_type()-operator überhaupt in Zeile 28 aufgerufen?
    • Warum wird der Operator dann nicht auch in Zeile 30 aufgerufen? Schließlich funktioniert ja auch folgendes:
    void myFunc() { /*...*/ }
    
    int main (){
      void (*func)() = myFunc;
      if(func < 0) { /*...*/ } 
      return 0;
    }
    
    • Geht das ganze auch ohne typedef? Ich habe verschieden Varianten von
    operator void (Testable::*)() const () const { /*...*/}
    

    ausprobiert, aber ohne Erfolg...

    • Für Zeile 30 kriege ich viele Fehlermeldungen, die ich (abgesehen von der letzten) nicht wirklich verstehe:
    • error C2784: "bool std::operator <(const std::move_iterator<_RanIt> &,const std::move_iterator<_RanIt2> &)": template-Argument für "const std::move_iterator<_RanIt> &" konnte nicht von "Testable" hergeleitet werden.
    • ...\include\xutility(1983): Siehe Deklaration von 'std::operator <'
    • error C2784: "bool std::operator <(const std::reverse_iterator<_RanIt> &,const std::reverse_iterator<_RanIt2> &)": template-Argument für "const std::reverse_iterator<_RanIt> &" konnte nicht von "Testable" hergeleitet werden.
    • ...\include\xutility(1259): Siehe Deklaration von 'std::operator <'
    • error C2784: "bool std::operator <(const std::_Revranit<_RanIt,_Base> &,const std::_Revranit<_RanIt2,_Base2> &)": template-Argument für "const std::_Revranit<_RanIt,_Base> &" konnte nicht von "Testable" hergeleitet werden.
    • ...\include\xutility(1075): Siehe Deklaration von 'std::operator <'
    • error C2784: "bool std::operator <(const std::pair<_Ty1,_Ty2> &,const std::pair<_Ty1,_Ty2> &)": template-Argument für "const std::pair<_Ty1,_Ty2> &" konnte nicht von "Testable" hergeleitet werden.
    • ...\include\utility(232): Siehe Deklaration von 'std::operator <'
    • error C2676: Binärer Operator '<': 'Testable' definiert diesen Operator oder eine Konvertierung in einen für den vordefinierten Operator geeigneten Typ nicht
      vielleicht hat ja jemand Lust mir zu erklären, was Iteratoren damit zu tun haben? 🙂

    Und meine letzte Frage: Sind denn mit obigem Beispiel alle Eventualitäten abgedeckt, also ist das jetzt wirklich Safe bool, oder kann mir das ganze wieder an anderer Stelle um die Ohren fliegen?

    Vielen Dank für eure Hilfe und viele Grüße
    Matze



  • MatzeHHC schrieb:

    IntelliSense meckert zwar tatsächlich, dass es keine Operatorübereinstimmung für == gibt, allerdings bekomme ich den Compiler-Fehler schon viel früher, nämlich in Zeile 2:

    error C2071: 'Testable::operator bool': Ungültige Speicherklasse

    Ist mein Compiler zu alt? Ich nutze VS 2012 und dachte der hat vollständige C++11-Unterstützung. Oder liegt der Fehler wo anders?

    Jupp, sieht danach aus.

    Was ich nicht verstehe:
    Warum wird der bool_type()-operator überhaupt in Zeile 28 aufgerufen?

    Das sind halt die relativ komplexen Regeln von C++ Konvertierungssequenzen.
    In kurz: Der Compiler sucht nach einer Konvertierungssequenz, die mit einem bool endet. Solche Konvertierungssequenzen bestehen aus einer bestimmten Kombination verschiedener Konvertierungen. Eine gültige ist benutzerdefinierte Konvertierung (operator bool_type) mit Standardkonvertierung (pointer-to-bool).
    Deshalb wird da operator bool_type aufgerufen.

    Warum wird der Operator dann nicht auch in Zeile 30 aufgerufen? Schließlich funktioniert ja auch folgendes:

    void myFunc() { /*...*/ }
    
    int main (){
      void (*func)() = myFunc;
      if(func < 0) { /*...*/ } 
      return 0;
    }
    

    Das liegt an den noch komplexeren Konvertierungssequenzen bei Funktionsaufrufen.
    Der Compiler sucht in Zeile 30 nach einem bool operator<(Testable&, int). Er findet aber keinen. Der, der in deinem Beispiel aufgerufen wird hätte die Signatur bool operator<(int, int). Um das hinzubekommen, muss aber eine benutzerdefinierte Konvertierung (Testable -> bool_type), gefolgt von einer Standardkonvertierung (bool_type -> bool), gefolgt von einer Promotion (bool -> int) stattfinden. Das ist schlicht und ergreifend illegal definiert werden.

    Geht das ganze auch ohne typedef? Ich habe verschieden Varianten von

    operator void (Testable::*bool_type)() const () const { /*...*/}
    

    ausprobiert, aber ohne Erfolg...

    http://stackoverflow.com/a/6755760

    Für Zeile 30 kriege ich viele Fehlermeldungen, die ich (abgesehen von der letzten) nicht wirklich verstehe:

    • error C2784: "bool std::operator <(const std::move_iterator<_RanIt> &,const std::move_iterator<_RanIt2> &)": template-Argument für "const std::move_iterator<_RanIt> &" konnte nicht von "Testable" hergeleitet werden.
    • ...\include\xutility(1983): Siehe Deklaration von 'std::operator <'
    • error C2784: "bool std::operator <(const std::reverse_iterator<_RanIt> &,const std::reverse_iterator<_RanIt2> &)": template-Argument für "const std::reverse_iterator<_RanIt> &" konnte nicht von "Testable" hergeleitet werden.
    • ...\include\xutility(1259): Siehe Deklaration von 'std::operator <'
    • error C2784: "bool std::operator <(const std::_Revranit<_RanIt,_Base> &,const std::_Revranit<_RanIt2,_Base2> &)": template-Argument für "const std::_Revranit<_RanIt,_Base> &" konnte nicht von "Testable" hergeleitet werden.
    • ...\include\xutility(1075): Siehe Deklaration von 'std::operator <'
    • error C2784: "bool std::operator <(const std::pair<_Ty1,_Ty2> &,const std::pair<_Ty1,_Ty2> &)": template-Argument für "const std::pair<_Ty1,_Ty2> &" konnte nicht von "Testable" hergeleitet werden.
    • ...\include\utility(232): Siehe Deklaration von 'std::operator <'
    • error C2676: Binärer Operator '<': 'Testable' definiert diesen Operator oder eine Konvertierung in einen für den vordefinierten Operator geeigneten Typ nicht

    vielleicht hat ja jemand Lust mir zu erklären, was Iteratoren damit zu tun haben? 🙂

    Der Compiler listet lediglich alle operator< auf, die er findet.

    Und meine letzte Frage: Sind denn mit obigem Beispiel alle Eventualitäten abgedeckt, also ist das jetzt wirklich Safe bool, oder kann mir das ganze wieder an anderer Stelle um die Ohren fliegen?

    Es sollte gehen. TM


  • Mod

    MatzeHHC schrieb:

    Ist mein Compiler zu alt? Ich nutze VS 2012 und dachte der hat vollständige C++11-Unterstützung.

    vollständig ist etwas anderes.

    MatzeHHC schrieb:

    Der erste Lösungsansatz funktioniert bei mir. Dafür verstehe ich ihn nicht 😕

    ...
    

    Was ich davon verstehe:

    • Statt des bool()-Operators, wird ein Cast-Operator auf einen Funktions-Zeiger implementiert.
    • Durch die Rückgabe eines Zeigers auf this_type_... wird ein Wert != 0 zurückgegeben, der true ergibt
    • Die Templates sorgen für einen Compiler-Fehler, da dort private Methoden aufgerufen werden (Zeile 29).

    Genau genommen handelt es sich um einen Memberfunktionszeiger (also kein Zeiger).

    MatzeHHC schrieb:

    Was ich nicht verstehe:

    • Warum wird der bool_type()-operator überhaupt in Zeile 28 aufgerufen?

    Es wird nach einer Konvertierung nach bool gesucht, und eine entsprechende implizite Konvertierungssequenz existiert (nämlich: Qualifikations-Konvertierung (+const) -> nutzerdefinierter Konvertierungsoperator (ergibt Zeiger auf Memberfunktion) -> Konvertierung nach bool

    MatzeHHC schrieb:

    [list][*]Warum wird der Operator dann nicht auch in Zeile 30 aufgerufen?

    Weil der < Operator einen arithmetischen Typ (int oder größer oder Gleitkommatyp oder Zeigertyp) erwartet. Und eine entsprechende implizite Konvertierungssequenz ausgehend von einem Memberfunktionszeiger existiert nicht.

    MatzeHHC schrieb:

    Schließlich funktioniert ja auch folgendes:

    void myFunc() { /*...*/ }
    
    int main (){
      void (*func)() = myFunc;
      if(func < 0) { /*...*/ } 
      return 0;
    }
    

    Ein konstanter intergraler Ausdruck mit dem Wert 0 ist eine Nullpointerkonstante und kann implizit in jeden Zeigertyp (Zeiger auf Member(-funktionen) sind keine Zeiger) umgewandelt werden. Zeiger gleichen Typs können per < verglichen werden (auch Funktionszeiger).

    MatzeHHC schrieb:

    Geht das ganze auch ohne typedef?

    Nein, die Möglichkeiten, den Typ zu spezifizieren, sind von der Grammatik her begrenzt (Klammern sind nicht zulässig).

    MatzeHHC schrieb:

    vielleicht hat ja jemand Lust mir zu erklären, was Iteratoren damit zu tun haben?

    Der Compiler führt alle < Operatoren auf, die er vergeblich versucht hat anzuwenden.

    MatzeHHC schrieb:

    Und meine letzte Frage: Sind denn mit obigem Beispiel alle Eventualitäten abgedeckt, also ist das jetzt wirklich Safe bool, oder kann mir das ganze wieder an anderer Stelle um die Ohren fliegen?

    Es ist die beste Variante mit prä-C++11



  • Hallo,

    vielen Dank für eure Antworten, da sind keine Fragen offen geblieben.
    Bin gerade bei der Installation von VS 2013.
    Memberfunktionszeiger habe ich mir jetzt auch mal genauer angeschaut. Ich denke da halten sich die Anwendungsfälle eher in Grenzen...

    Bei den ganzen impliziten Konvertierungen fällt es mir schwer zu überblicken, was erlaub ist und was nicht. Kennt ihr eine gute Quelle dafür?
    Bei C++ kann man als Neuling leider nicht immer einschätzen, ob ein Autor wirklich Ahnung hat, von dem was er da schreibt...

    Viele Grüße
    Matze



  • MatzeHHC schrieb:

    Bei C++ kann man als Neuling leider nicht immer einschätzen, ob ein Autor wirklich Ahnung hat, von dem was er da schreibt...

    Haha, ja siehe Jürgen Wolf 😃

    Manche nehmen sich als Regel "Beispielcode, der unleserlich[im Sinne von schwer verständlich] ist, ist gut"
    Andere sagen, möglichst technicher Code ist gut (Zeiger, pointer und manuelle Speicherverwaltung und so.
    Und wiederum andere sagen, dass einfacher Code gut sei.

    Tja, Recht haben alle und auch keiner 😉
    Zumal es öfters auf den Blickwinkel, auf die Anforderungen und auf den Spezialfall ankommt :>



  • Skym0sh0 schrieb:

    Haha, ja siehe Jürgen Wolf 😃

    Ja, an den hatte ich auch gedacht 😃

    Skym0sh0 schrieb:

    Manche nehmen sich als Regel "Beispielcode, der unleserlich[im Sinne von schwer verständlich] ist, ist gut"
    Andere sagen, möglichst technicher Code ist gut (Zeiger, pointer und manuelle Speicherverwaltung und so.
    Und wiederum andere sagen, dass einfacher Code gut sei.

    Ok, da fehlt mir gerade der Zusammenhang 😕



  • Ich meine damit genau das was du sagst. Es ist schwer (für Anfänger) zu entscheiden, ob und ab wann ein Text (Buch) gut ist.

    Wenn du die Beispielcodes in einem Buch siehst, dann kannst du dir anhand derer denken, ob diese guter Code sind oder eben nicht. Und ich habe einige Merkmale (quasi Heuristiken) genannt. Auch wenn das nicht allgemeingültig belegbar ist.

    Aber das ist eigentlich alles Offtopic und geht nicht auf deine Frage zum Safe-Bool-idiom ein.


Log in to reply