Überladung von Operatoren in C++ (Teil 1)



  • SpongeBob11 schrieb:

    Vielleicht sollte man erwaehnen das die Uberladung der drei Operatoren (|| && ,) zu undefinierten Verhalten fuehren kann. Und das weil die Kurzschlußauswertung durch einen Funktionsaufruf ersetzt wird.

    Beispiel bitte. Es führt IMO nichtmal zu unspezifiziertem oder implementationsdefiniertem Verhalten.



  • SpongeBob11 schrieb:

    Vielleicht sollte man erwaehnen das die Uberladung der drei Operatoren (|| && ,) zu undefinierten Verhalten fuehren kann. Und das weil die Kurzschlußauswertung durch einen Funktionsaufruf ersetzt wird. ME C++ erwaehnt das glaube ich, bin mir aber jetzt nicht sicher.

    Klares jein für die beiden Logik-Operatoren, definitives nein für den Kommaoperator. Der Kommaoperator führt nur dann zu undefiniertem Verhalten, wenn eines seiner Argumente zu undefiniertem Verhalten führt oder wenn die Implementation zu undefiniertem Verhalten führt. Im ersten Fall tut das der eingebaute Kommaoperator dann auch, im zweiten Fall ist einfach die Überladung schlecht.
    Für die logischen Operatoren gilt im Prinzip das Gleiche, hinzu kommt, dass oft Pointerdereferenzierungen die Kurzschlusssemantik ausnutzen, z.B.

    X* xPtr;
    /* ... */
    if (xPtr && xPtr->foo())
      /* ... */
    

    wird op&& jetzt überladen kann sowas zu undefiniertem Verhalten führen, wenn das zweite Argument im Kurzschlussfall zu undefiniertem Verhalten führt. Das ist dann aber auch eine Folge des veränderten Verhaltens bei Überladung, das ich allerdings auch beschrieben habe.



  • Erstmal sehr schöner Artikel.

    hab nur einen kleinen Fehler gefunden

    2.1 Wann sollte man Operatoren überladen?
    ....
    ... für die Streams der Standarbibliothek, weitere Beispiele folgen in Kapitel 4.2.

    nur gibt es kein kapitel 4.2. Hast vielleicht Kapitel 3.2 oder 3.4 gemeint? Soll nicht böse gemeint sein ist mir nur aufgefallen.

    So long



  • Kapitel 4.2 wird kommen, allerdings stimmt die Angabe nicht mehr. Geändert auf "in einem späteren Artikel" 😉



  • Was mich interessieren würde, sind Operatoren bei Vererbungsstrukturen. Im Scott Meyers Buch habe ich da leider keine Antwort gefunden (Kapitel 12 oder 13 war das). Hier meine Frage: http://www.c-plusplus.net/forum/viewtopic-var-t-is-232293.html



  • hab dort geantwortet.



  • Ich würde das hier mit const schreiben, also statt

    X operator+(X const& lhs, X const& rhs)
    

    das hier

    const X operator+(X const& lhs, X const& rhs)
    

    Dann kann man nicht "(a+b)=c" schreiben.



  • *Edit
    Sry, habe etwas in den falschen Thread kopiert.



  • Sehr guter Artikel.

    Solltest du gewillt sein, an der ein oder anderen Stelle etwas ausführlicher zu werden, würde ich mich sehr darüber freuen.

    Mir ist allerdings etwas aus deinem Beispiel aufgefallen. Folgender Code dürfte nicht ganz richtig sein, schließlich hast du extra den Konstruktor programmiert, verwendest ihn aber nicht. Bei der Erzeugung von a, b und c solltest du noch Standardwerte nachreichen, ansonsten kommt bei der Addition nur Mumpitz raus.

    class Rational 
    { 
    public: 
      Rational(int i); //Konstruktor für Konvertierungen von Ganzzahlen 
      Rational operator+(Rational const& rhs) const; 
    }; 
    
    Rational a, b, c; 
    int i; 
    a = b + c; //ok, keine Konvertierung nötig 
    a = b + i; //ok, implizite Konvertierung des zweiten Arguments 
    a = i + c; //FEHLER: erstes Argument kann nicht konvertiert werden, da operator+ keine freie Funktion
    


  • nuke1600 schrieb:

    schließlich hast du extra den Konstruktor programmiert, verwendest ihn aber nicht.

    Doch, er wird für die implizite Konvertierung von int nach Rational verwendet.

    nuke1600 schrieb:

    Bei der Erzeugung von a, b und c solltest du noch Standardwerte nachreichen, ansonsten kommt bei der Addition nur Mumpitz raus.

    Der Code würde gar nicht kompilieren, weil kein Defaultkonstruktor vorhanden ist. Aber wie die Objekte erzeugt werden, ist in dem Beispiel nebensächlich...



  • Nexus schrieb:

    wie die Objekte erzeugt werden, ist in dem Beispiel nebensächlich...

    Richtig. Ein weitgehend vollständiges beispiel für so eine Rational-Zahlenklasse findest du im zweiten und dritten Teil des Artikels.



  • Btw, jetzt mit C++11 können einige Operatoren wie operator?: überladen werden. Kommt dazu noch was?



  • Sone schrieb:

    Btw, jetzt mit C++11 können einige Operatoren wie operator?: überladen werden. Kommt dazu noch was?

    Wo hast du das den her?



  • pyhax schrieb:

    Sone schrieb:

    Btw, jetzt mit C++11 können einige Operatoren wie operator?: überladen werden. Kommt dazu noch was?

    Wo hast du das den her?

    Mein Fehler, hab mich im Standard verguckt. Siehe 13.6.24, war schon ne Weile her, hab mich falsch erinnert... 😃



  • Mmh, wäre es möglich, Beispiele für die Überladung von operator-> und operator->* zu bekommen? Insbesonders, da das hier anders als der Artikel zu behaupten scheint, dass typ * operator->() für den Zugriff auf Elemente von typ gedacht ist (mag aber auch einfach nur schlecht formuliert sein).

    ... Kann mir nicht recht vorstellen, wie man das zu verwenden hat.

    Ich glaube, dass ich damit elegant dieses Ding hier vereinfachen könnte:

    typedef struct Nest
    {
      double wert;
      double * koordinaten;
      double * wertigkeiten;
    }
    
    Nest * LeeresNest(const size_t & dimension, const size_t & bewohner)
    {
      Nest * ausgabe = (Nest *)operator new(2 * sizeof(double *) + (1 + dimension + bewohner) * sizeof(double));
      ausgabe->koordinaten  = (double *)(&ausgabe->wertigkeiten + 1); 
      ausgabe->wertigkeiten = ausgabe->koordinaten + dimension;
    
      return ausgabe;
    }
    

    (siehe dieses Thema, falls die Motivation kümmert)

    Weiterhin:
    Wenn ich mir diesen überladenen Operator anschaue:

    vector<T,Allocator>& operator= (const vector<T,Allocator>& x);
    

    dann wird derselbe so benutzt:

    vector a;
    vector b = a;
    

    Mit diesem hier:

    void* operator new (std::size_t size) throw (std::bad_alloc);
    

    sollte die Verwendung also dergestalt aussehen:

    Nest * (Nest *)new 2 * sizeof(double *) + (1 + dimension + bewohner) * sizeof(double);
    

    Funktioniert aber nicht - die Syntax aus dem oberen Beispiel ist erforderlich. Wie kommt das?

    Gleichsam wird aus

    void* operator new (std::size_t size, void* ptr) throw();
    

    in der Anwendung nicht etwa

    new <wert> <zeiger>;
    

    sondern eines von diesen beiden:

    operator new(<wert>, <zeiger>); // keine Ahnung, wie hier der Konstruktor zu wählen wäre
    new (<zeiger>) <Konstruktor>;
    

    Praktisch. Aber woher!?



  • Der string literal operator fehlt.



  • Du meinst wohl user-defined-literal. Hier:

    3.23 operator"" - benutzerdefinierte Literale

    ~Der Standard erklärt alle folgenden Informationen ausführlicher in §2.14.8.~

    Seit C++11 ist es nun möglich, für String-, Zeichen- und Skalar-Literale eigene Suffixe zu definieren. Ein Suffix ist ein Identifier, der unmittelbar nach einem Literal folgt*. Bekannte Suffixe aus C++03 sind bspw. f, LL oder auch u.
    Wem vielleicht schon die Ähnlichkeit der Syntax mit Whitespace-Overloading aufgefallen ist, wird enttäuscht werden. Oder auch nicht, denn dieses Feature hat es auch in sich.

    Neue Suffixe, vorgeschlagen für C++14, sind u.a. auch s (für std::string ), i / il / i_f für std::complex oder h , min , s , ms , us und ns für std::chrono::duration .

    Allgemeines

    Genau wie bei überladenen Operatoren (nur dort mit operator-functions bzw. operator-function-templates) wird ein user-defined-literal als Aufruf des literal-operators bzw. einer Spezialisierung eines literal-operator-templates behandelt.

    Findet die Implementierung nun also ein Literal der Form 684s , kann sie zu einem Aufruf einer bestimmten Form umgewandelt werden, die ggf. von der Deklaration des literal operators oder auch literal-operator-templates abhängt.

    Die verschiedenen user-defined-literals

    Es gibt vier verschiedene benutzerdefinierte Literale:

    • user-defined-integer-literal
    • user-defined-floating-literal
    • user-defined-string-literal
    • user-defined-character-literal

    Die ersten beiden ähneln sich und werden daher unten gemeinsam behandelt.

    Im Folgenden wird angenommen, dass SFX das Suffix ist, und LIT das Literal ohne Suffix.

    user-defined-integer-literal / user-defined-floating-literal

    Für Literale der Form 684SFX , 14.6884SFX oder auch 0.684e47SFX .
    Für Skalar-Literale gibt es verschiedene Formen. Entweder ein literal-operator ist vorhanden; in dem Fall muss er einen Parameter des entsprechenden Typs haben. Sonst muss ein literal-operator-template vorhanden sein.
    Es darf vom name-lookup nicht ein raw-literal-operator und ein literal-operator-template gefunden werden (dies resultiert in einer Ambiguität).

    Per Parameter:

    • unsigned long long (für integrale Skalare)
      Hier wird ein Aufruf der Form operator "" SFX ( LIT ULL ) gemacht.
    • long double (für Fließkomma-Skalare)
      Hier wird ein Aufruf der Form operator "" SFX ( LIT L ) gemacht.
    • char const*
      Ein literal-operator mit einem solchen Parameter wird raw literal operator genannt, da er einfach einen String mit LIT als Inhalt nimmt. Aufruf: operator "" SFX ("LIT") .

    Beispiel:

    #include <iostream>
    
    std::ostream& operator"" _print( unsigned long long i )
    {
    	return std::cout << i;
    }
    
    long double operator"" _pi( long double i )
    {
    	return i * 3.141592635;
    }
    
    char const* operator"" _cut( char const* str )
    {
    	return strchr(str, '.');
    }
    
    int main()
    {
    	16848_print << '\n' << 2.47_pi << '\n' << 684.486_cut << '\n';
    }
    

    Per variadischem Funktionstemplate:

    Die Implementierung fordert ein variadic template, genauer ein Template mit einem non-type template parameter pack mit Elementtyp char und einer leeren Parameterliste. Beim Aufruf wird jedes Zeichen cic_i vor dem Suffix als Template-Argument weitergegeben, die Syntax sieht folgendermaßen aus:
    operator "" SFX <’c1’, ’c2’, ... ’ck’>()

    user-defined-string-literal

    Für Literale der Form P"..."SFX **

    Hier wird lediglich der String (LIT) und die Länge des Strings ( len ) übergeben.
    Der literal-operator muss als ersten Parameter
    const wchar_t* ,
    const char16_t* ,
    const char32_t* oder
    const char*
    haben, und als zweiten einen std::size_t .

    Aufruf: operator "" SFX ( LIT, len )

    user-defined-character-literal

    Für Literale der Form P'C'SFX ***

    Resultiert im Aufruf operator "" SFX ( LIT ) . Ein entsprechender literal-operator muss den genauen Typ des Zeichens als Parameter haben.

    Alle möglichen Parameterlisten für literal-operators

    Ein literal-operator darf keine Parameterliste außer den unten genannten haben, auch keine mit default-Argumenten.
    Noch einmal zusammengefasst, alle möglichen Parameterlisten für literal-operators:

    ** const char* **
    Sowohl für Fließkomma- als auch für integrale Literale. Übergibt die komplette Zahl als String.

    ** unsigned long long int **
    Für integrale Literale. Übergibt die Zahl.

    ** long double **
    Für Fließkomma-Literale. Übergibt die Fließkommazahl.

    ** `char

    wchar_t

    char16_t

    char32_t` **
    Für Zeichen-Literale; Das Zeichen wird übergeben.

    ** `const char*, std::size_t

    const wchar_t*, std::size_t

    const char16_t*, std::size_t

    const char32_t*, std::size_t` **
    Für Strings. Nicht nullterminierter String wird als erstes Argument übergeben, Länge als zweites.

    * Ein Literal selbst bezeichnet natürlich das gesamte Token, mitsamt Präfixen und Suffixen. Hier ist das Literal ohne Suffix gemeint.
    ** Hier stellt P das Präfix dar, welches den Typ/Kodierung des Strings bestimmt; L , u , U , u8 . Möglich ist auch ein R für ein raw-string-literal.
    *** Hier stellt P das Präfix dar, welches den Typ/Kodierung des Zeichens bestimmt; s.o. . C ist das Zeichen selbst.

    Weiteres

    • Suffixe die nicht mit einem Unterstrich beginnen sind reserviert. (§17.6.4.3.5)
    • Die Deklarationen applikabler literal-operators oder der Templates werden per unqualified name lookup gefunden. Es wird die literal operator [template] id eingesetzt.
    • Ein literal operator muss in namespace-scope deklariert sein (wobei er allerdings auch ein Freund sein kann).
    • literal-operators und Spezialisierungen von literal-operator-templates sind auch nur gewöhnliche Funktionen, deren Adresse genommen werden kann, die als inline und (wie im Beispiel unten) constexpr deklariert sein können usw. Es können auch template-ids für literal-operator-templates gebildet werden. Zum Beispiel operator "" SFX < Parameter > . (Siehe auch oben)

    Beispiel

    Das oben verlinkte Proposal zeigt auch Möglichkeiten, Zahlen zur Compile-Zeit zu parsen und so interessante Effekte zu erzielen. Hier im Beispiel wird (das im GCC bereits als Erweiterung zu findende) Binary-Literal implementiert.

    #include <iostream>
    #include <chrono>
    #include <type_traits>
    #include <cstddef>
    
    using namespace std;
    
    using int_type = unsigned long long;
    
    chrono::seconds operator "" _s( int_type i ) { return chrono::seconds{i}; }
    chrono::minutes operator "" _m( int_type i ) { return chrono::minutes{i}; }
    
    /// binary-literals:
    
    template<char ... ch>
    struct parse_int
    {
    	template<char to_check>
    	struct require_01 : integral_constant<bool, to_check - '0'> 
            {
                static_assert( to_check == '0' or to_check == '1', "Invalid digit in bit sequence" );
            };
    
    	static constexpr bool bits[]{ require_01<ch>::value... };
    
    	static constexpr size_t length = sizeof...(ch);
    
    	static constexpr int_type compute_value( int cur_index = length - 1,
    											 int_type current = 0 )
    	{
    		return cur_index != -1 ?
    		       compute_value( cur_index - 1, (static_cast<int_type>(bits[cur_index]) << (length - cur_index - 1)) | current )
    		     : current;
    	}
    };
    
    template<char... ch>
    constexpr int_type operator "" _bin()
    {
    	return parse_int<ch...>::compute_value();
    }
    
    int main()
    {
    	cout << "1m 20s sind " << (1_m + 10_s).count() << "s\n";
    	cout << "Binaeres Literal: " << 10101101_bin;
    }
    

    Edit³: Oh-Oh, ich fürchte die Erklärung oben ist scheiße.



  • Noch mal überarbeitet. Damit könnte man möglicherweise einen kompletten Artikel füllen.



  • Vielen Dank! 😋



  • Der arme kleine vollkommen unterschätze xor Operator wird immer vergessen. 😞


Anmelden zum Antworten