C++ vor 10 Jahren



  • Ich habe ca. 10 Jahre alten C++ Code vorliegen und der ist teilweise wirklich grausam. Waren vor 10 Jahren so Sachen wie maps, smart pointer und RAII noch nicht bekannt?



  • Nee, die sind erst kürzlich entdeckt worden, als zufällig mal jemand im Standard umgeblättert hat.



  • Wenn du in 10 Jahren heutigen C++-Code anschaust, wirst du auch die Haare raufen.

    Hier ist der korrekte Code, in idiomatischem C++11 ein Tripel zu coden:

    template <typename A, typename B, typename C>
    struct triple {
      A a;
      B b;
      C c;
    
      triple(const A& a, const B& b, const C& c) : a(a), b(b), c(c) {}
      triple(const A& a, const B& b, C&& c) : a(a), b(b), c(std::move(c)) {}
      triple(const A& a, B&& b, const C& c) : a(a), b(std::move(b)), c(c) {}
      triple(const A& a, B&& b, C&& c) : a(a), b(std::move(b)), c(std::move(c)) {}
      triple(A&& a, const B& b, const C& c) : a(std::move(a)), b(b), c(c) {}
      triple(A&& a, const B& b, C&& c) : a(std::move(a)), b(b), c(std::move(c)) {}
      triple(A&& a, B&& b, const C& c) : a(std::move(a)), b(std::move(b)), c(c) {}
      triple(A&& a, B&& b, C&& c) : a(std::move(a)), b(std::move(b)), c(std::move(c)) {}
    };
    

    Ein anderes Beispiel sind Ranges, die den Code um einiges kürzer und sogar effizienter (da lazy) machen würden. Leider wird das mit denen nichts bis C++17.

    Schau dir mal andere Sprachen an, die sind C++ bezüglich Ausdrucksstärke um einiges voraus.



  • Willkommen in der wirklichen Welt 😉 In großen Projekten musst du oft auch mit 20 Jahre altem Code arbeiten. Und früher hat man teilweise echt grauenhaften C++ Code geschrieben, wobei das heute auch noch oft genug der Fall ist.



  • Auch heutiger C++-Code ist grösstenteils grausam. Modernes C++ (im Sinne von Sutter, Meyers etc.) ist leider nur einem kleinen Teil von Programmierern bekannt. Wenn du durchschnittlichen Produktivcode anschaust, ist z.B. konsequente Anwendung von RAII recht unüblich.

    bashy, extrem generische Programmierung ist tatsächlich nicht besonders schön, aber darum gibts vieles (wie std::tuple ) schon in der Standardbibliothek oder Boost. Für den Normalanwender wird der Code mit C++11 jedoch um einiges aussagekräftiger (Typinferenz, Range-Based For, Lambdas).



  • Nexus schrieb:

    Für den Normalanwender wird der Code mit C++11 jedoch um einiges aussagekräftiger (Typinferenz, Range-Based For, Lambdas).

    Ach meinst du? Typinferenz macht normalen Code meiner Meinung nach eher unübersichtlicher. Ich verstehe zwar die Argumente dafür, aber wenn ich in meinen Code von einem halben Jahr reinschaue mit auto und etwas ergänzen möchte, fehlt mir Typinformation. Dann muss ich entweder ausprobieren, ob ich diese Methode aufrufen kann oder ich schaue mir an, was die Funktion zurückgibt. Alles nicht so optimal.

    Das range-based For ist sehr oft unnütz. Entweder gibt es dafür schon ein Algorithmus in der Standardbibliothek oder es ist zu wenig möchtig. Oft brauche ich zusätzlich zum Element noch den Index => umständlich. Oder ich will manche Elemente überspringen => unmöglich. Im Zweifel schreibe ich die Loop dann lieber von Hand.

    Abgesehen davon ist die Unterstützung noch rudimentär.

    for (int key, std::string const& value : my_map) {} // nicht erlaubt
    

    Was ich oft brauche ist von a bis b zählen, aber das geht auch wieder nicht.

    for (int i : range(1, 10)) {} // geht nicht
    

    Wie die Lambdas ankommen muss man mal schauen. Stellt sich die Frage, ob es für viele den Aufwand wert ist, sie zu lernen, weil ganz trivial sind sie nicht.



  • bashy schrieb:

    Wenn du in 10 Jahren heutigen C++-Code anschaust, wirst du auch die Haare raufen.

    Hier ist der korrekte Code, in idiomatischem C++11 ein Tripel zu coden:

    Mal abgesehen davon, dass der idiomatische Weg im Zweifel

    template<typename A, typename B, typename C> using triple = std::tuple<A, B, C>;
    

    sein dürfte:

    template <typename A, typename B, typename C>
    struct triple {
      A a;
      B b;
      C c;
    
      template<typename A2, typename B2, typename C2>
      triple(A2 &&a2, B2 &&b2, C2 &&c2) // universal references!
        : a(std::forward(a2)),
          b(std::forward(b2)),
          c(std::forward(c2)) { }
    };
    

    Siehe dazu auch http://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers



  • bashy schrieb:

    Was ich oft brauche ist von a bis b zählen, aber das geht auch wieder nicht.

    for (int i : range(1, 10)) {} // geht nicht
    

    Natürlich geht das!

    #include <iostream>
    
    struct range
    {
        int min, max;
        range(int a, int b)
        : min(a), max(b) {}
    
        struct iterator
        {
            int value;
    
            iterator(int val)
            : value(val) {}
    
            int operator*()
            {return value;}
    
            bool operator != (iterator other)
            {return other.value !=  value;}
    
            iterator operator++()
            {return ++value;}
        };
    
        iterator begin()
        {return iterator(min);}
    
        iterator end()
        {return iterator(max);}
    };
    
    int main()
    {
        for (auto i : range(1, 10))
            std::cout << i << '\t';
    }
    

    Siehste?



  • seldon schrieb:

    Mal abgesehen davon, dass der idiomatische Weg im Zweifel [...] sein dürfte:

    template<typename A2, typename B2, typename C2>
      triple(A2 &&a2, B2 &&b2, C2 &&c2) // universal references
    

    Eben nicht. Dann geht die Brace-Initialisation nicht mehr, die Stroustrup so mag. Mit std::tuple geht das zwar (da const& Parameter), aber nur auf Kosten einer Kopie der anderen Parameter.

    Nathan schrieb:

    Natürlich geht das!

    Ja, es geht. So mache ich das auch. Aber es dürfte nur von wenigen benutzt werden, weil es nicht in der Standardbibliothek implementiert ist.



  • Nathan schrieb:

    <Code-Bloat>
    

    Das witzige ist, dass das noch lange nicht die Bedingungen für einen Iterator erfüllt. Dazu sind erst noch ein paar hundert Zeilen Boilerplate nötig.



  • Ich habe das mal eben in ein paar Minuten gecodet, um es dir zu zeigen. 🙄



  • seldon schrieb:

    template<typename A2, typename B2, typename C2>
      triple(A2 &&a2, B2 &&b2, C2 &&c2) // universal references!
        : a(std::forward(a2)),
          b(std::forward(b2))
    

    Ein weiterer Indikator dafür, dass C++11 viel zu kompliziert ist. Du müsstest std::forward<A2>(a2) schreiben, sonst ist das forward sinnlos.



  • bashy schrieb:

    Ach meinst du?

    Ja, bestimmt. Man muss Typinferenz aber verhältnismässig einsetzen. Die Verlockung ist gerade für Konsistenz-Fanatiker da, nun überall auto hinzuschreiben. Ich benutze auto hauptsächlich für Iteratoren, und da ist es definitiv übersichtlicher als die ausgeschriebene Version.

    bashy schrieb:

    Das range-based For ist sehr oft unnütz.

    Findest du? Ich finde es extrem nützlich. So nützlich, dass ich mir sogar ein Workaround-Makro für VS 2010 à la BOOST_FOREACH geschrieben habe. In vielen Fällen will ich schlicht etwas für alle Elemente tun, und da ist es perfekt.

    Die Standardalgorithmen sind oft praktisch, aber teilweise muss man sie mit Lambdas oder std::bind() derart zurechtbiegen, dass ein Range-Based For massiv übersichtlicher ist. Ein Index ist nicht umständlicher als wenn du normale Iteratoren verwendest. Elemente überspringen kannst du mit continue .

    bashy schrieb:

    Abgesehen davon ist die Unterstützung noch rudimentär.

    for (int key, std::string const& value : my_map) {} // nicht erlaubt
    

    Ich bezweifle, dass man das jemals einführen wird. Und

    for (auto pair : my_map)
    

    ist schon eine massive Verbesserung zu vorher.

    bashy schrieb:

    Was ich oft brauche ist von a bis b zählen, aber das geht auch wieder nicht.

    for (int i : range(1, 10)) {} // geht nicht
    

    Doch, du musst range() halt einen sinnvollen Typen zurückgeben lassen. Mit vorhandenen Mitteln, dafür ineffizienter:

    std::vector<int> range(int begin, int end)
    {
    	std::vector<int> sequence(end - begin);
    	std::iota(sequence.begin(), sequence.end(), begin);
    
    	return sequence;
    }
    


  • bashy hat schon recht gewissermaßen, C++ ist auch heute noch völlig kaputt. C++17 könnte die schlimmsten Fehler vielleicht fixen indem man das Kompilations(Kompilierungs?)-Model umbaut und den "normalen" Einsatz von Templates schmerzfrei macht, aber das wird den ganzen Ballast einer über 10-20(!) Jahre alten Sprachbasis trotzdem nicht los, weil niemand die "backwards compatibility" deutlich brechen will. Traurigerweise ist C++ trotz all dieser wirklich schrecklichen Fehler immer noch die angenehmste mir bekannte Sprache. 🙄



  • bashy schrieb:

    So mache ich das auch. Aber es dürfte nur von wenigen benutzt werden, weil es nicht in der Standardbibliothek implementiert ist.

    Eben, Ranges sind als Bibliotheksfeature nicht vorhanden. Das hat nichts mit dem Sprachfeature Range-Based For Loop zu tun. Wie dir gezeigt wurde, wäre dieses mächtig genug.

    bashy schrieb:

    Ein weiterer Indikator dafür, dass C++11 viel zu kompliziert ist.

    Neben den "netten" Features bietet C++11 eben vieles, um mehr Möglichkeiten für die generischen Programmierung zu bieten (z.B. Variadic Templates, Perfect Forwarding). Wenn du nicht gerade Boost-Bibliotheken schreibst, wirst du sowas sehr selten einsetzen.

    cooky451 schrieb:

    bashy hat schon recht gewissermaßen, C++ ist auch heute noch völlig kaputt. C++17 könnte die schlimmsten Fehler vielleicht fixen [...], aber das wird den ganzen Ballast einer über 10-20(!) Jahre alten Sprachbasis trotzdem nicht los, weil niemand die "backwards compatibility" deutlich brechen will.

    Das hat schon was. Traurigerweise macht man alles noch schlimmer, wenn man Dinge wie die Uniform Initialization schon derart defekt einführt, dass sie nie mehr geradezubiegen sind.

    Ein rücksichtloser Schnitt wäre das einzige, was die Sprache retten könnte. Wird aber kaum passieren, daher muss langfristig eine andere Sprache her. Am ehesten käme noch D in Frage. Dieser Stackoverflow-Thread beschreibt die Probleme von D, insbesondere im Bezug auf dessen Verbreitung.



  • Nexus schrieb:

    std::vector<int> sequence(end - begin);
    std::iota(sequence.begin(), sequence.end(), begin);
    

    iota ist wohl ein weiteres Beispiel für eine völlig misslungene Funktion. Es macht aus meiner Sicht keinen Sinn, ein Iteratorenpaar anzugeben. Etwas im Stil von copy_n wäre angebrachter gewesen. Oder noch besser ein Iterator

    std::copy_n(std::iota(begin), end-begin, std::back_inserter(sequence));
    

    Oder auch

    std::copy_n(std::iota(begin), end-begin, std::ostream_iterator<int>(std::cin, "\n"));
    

    oder auch

    std::for_each_n(std::iota(begin), end-begin, [](int i){
      // für alle i von begin bis end
    });
    

    Zur Namensgebung sage ich mal nichts, da sind die anderen Algorithmen auch nicht besser (von Streambufs ganz zu schweigen).



  • Dafür gibt's dann ja boost::iterator_facade oder wie es hieß!



  • Warum denn boost::, warum nicht std::?



  • Vor 10 Jahren wurde afaik noch VC6 benutzt. Das waren noch geile Zeiten!


Log in to reply