C++

C++

0

Auto und for-Schleifen

Posted on by

Nein, man lernt tatsächlich nie aus. Seit C++11 können wir ja schreiben

vector data{};
...
for(auto e : data) { cout << e; };

Das ist schon ganz schön praktisch gegenüber for(vector it=data.begin();...) { cout << *it; }, hat aber den großen Nachteil, dass erstens e hier oimmer kopiert wird und zweitens Änderungen an e sich nicht in data auswirken.

Stimmt, und deshalb erzähle ich auch immer, dass man daran danken sollte, in solchen Schleifen zuerst immer zu prüfen, ob man stattdessen nicht auto& oder const auto& verwenden sollte:

vector<int> data{};
...
for(auto& e : data) { e += 1; };

Das verhindert Kopie und data wird auch wirklich modifiziert.

Jetzt werde ich gerade erinnert, dass das nicht immer reicht. Zum Beispiel kann es keine (echte) Referenz in einen vector<bool> geben, weil man einzelne Bits nicht adressieren (also referenzieren) kann. Das gäbe mit auto& einen Fehler.

Daher, jetzt meine neue Regel: Man probiere es mit auto&&. Das ist dann der Einsatz der Meyerschen Universellen Referenzen, soweit ich das erkennen kann:

vector<int> data{};
...
for(auto&& e : data) { e += 1; };

Im Effekt bekommt man eine &, wenn nötig und eine &&, wenn erlaubt. Die genaue Erklärung was passiert, folgt in einem Detailbeitrag. Bis dahin probiere ich diese neue Merkregel mal aus.

Nachtrag

In einem Vorschlagsdokument des C++-Standards steht das Dilemma beschrieben:

What should we do about this problem? We can teach users to write
for (auto& elem : range) or for (const auto& elem : range), but they’re
imperfect (for proxies or mutation, respectively), and they require novices to
be introduced to auto and references earlier than otherwise necessary.
As explained immediately below, for (auto&& elem : range) is superior, but
it’s terrible to teach to novices. Even experienced C++98/03 programmers can
find it difficult to learn how rvalue references, the template argument
deduction tweak, and reference collapsing interact to produce what Scott Meyers
calls „universal references“ (see [1]), and auto&& is far less commonly seen
than T&& in perfect forwarding.

Andererseits sagt Herb Sutter in Back To Basics, dass man auto&& nicht für lokale Variablen verwenden soll. Da stimme ich im Prinzip auch zu, aber grüble, ob sein Gesagtes nicht bei diesen auto-for-Schleifen eine Ausnahme sind. Hauptsächlich im Bezug der Einfachheit allerdings.

„STL“ gibt etwas mehr Erklärung hier und bezieht auch den kommenden C++-Standard (wahrscheinlich) mit ein, der…

Ausblick

…eine neue Syntax enthält, die genau diese Schreibweise abkürzt: Einfach den gesamten Typen in der Schleifendeklaration weglassen und der Compiler macht daraus automatisch auto&&:

vector<int> data{};
...
for(e : data) { e += 1; };

…in der (fernen) Zukunft (vielleicht).

Zusammenfassung

Mit all den Informationen, die ich nun habe, möchte es noch einmal zusammenfassen. Vor allem „STL“ (oben) und Howard Hinnant konnten mir Dinge klären

Ab C++17 gilt: Verwende for(e :...) wann immer möglich.

vector<int> data{};
...
for(e : data) { e += 1; };

Bis dahin, mit C++11 alleine, gilt:

In 99.9% der Fälle und somit für alle Entwickler bis zum braunen Gürtel gilt (immernoch): Verwende for(auto &x: ...) oder for(const auto &x: ...) wenn Du kannst und kopiere nur wenn nötig:

vector<int> data{};
...
for(auto &e : data) { e += 1; };
...
for(const auto &e : data) { cout << e; };

Entwickler mit dem schwarzen Gürtel, und/oder innerhalb einer Bibliothek mit einem Funktionstemplate für den Typ von data, kann es sinnvoll sein for(auto &&x: ...) zu verwenden:

vector<int> data{};
template 
void func(T&& data) {
  ...
  for(auto &&e : data) { e += 1; };
  ...
}

Denn ob RValue-Ref oder LValue-Ref ist nur dann relevant, wenn data für seine Iteratoren (temporäre) Proxy-Objekte benötigt (wie vector<bool>). Selbst wenn man dieses Konstrukt außerhalb eines Funktionstemplate verwendet, wird man && nicht brauchen, sondern kann sich bewusst zwischen [const] auto& und [const] auto entscheiden.

0

FAQ001: modern102.cpp compiliert nicht auf Microsoft Visual Studion 2012

Posted on by

FAQ

Habe soweit das Programm modern102.cpp in Visual Studio 2012 übernommen. Wenn ich das Prgramm ausführen lassen möchte, bekomme ich als Fehlermeldung:

error C2601: ‚table‘: Lokale Funktionsdefinitionen sind unzulässig

Mir wird genau an der Stelle:

…static std::map<int, int> table{};…

bei der geschweiften Klammer angezeigt, dass was nicht stimmt. Bei genaueren Hinsehen in Ihrem Buch, stehen unter den beiden geschweiften Klammern {} jeweils ein Punkt.

 


Das klingt sehr danach, dass der Compiler C++11-Konstrukte zurückweist.
Hier sehen Sie

dass „Visual Studio 2012“ zum Beispiel die „Initializer Lists“ nicht kennt, also die
Initialisierung mit „{…}“. Das ist hier das Problem.

Im Anhang können Sie alternative Schreibweisen für C++11-Konstrukte finden, die Ihr Compiler nicht kennt. Konkret schreiben Sie statt

   static std::map<int, int> table{};

in MSVC2012:

   static std::map<int, int> table;

Wenn Sie die Möglichkeit haben, sollten Sie auf die neueste Version von Visual Studio wechseln. Wenn nicht, müssen Sie teilweise auf alternative Schreibweisen ausweichen.

Möglicherweise hilft auch ein kleines Update, das habe ich aber nicht selbst probiert.

In early November 2012, Microsoft announced the Visual C++ Compiler November 2012 CTP, which adds more C++11 functionality to Visual Studio 2012.

Im buch können Sie Punkte unter den Klammern sehen, die auf ein C++11-Feature hindeuten (wird in Abschnitt 1.2, „Verwendete Formatierungen“, S. 27, eingeführt). Wenn an solchen Stellen etwas schief geht und der Compiler meckert, sollten Sie skeptisch werden. Hier ist die „Initialisierungsliste“ gepunktet unterstrichen — weil „{}“ so wenig Text ist, erschient diese Linie als nur zwei Punkte.

0

Kein rand() mehr

Posted on by

Soso, der C++14-Standard wurde also einstimmig angenommen. Ende des Jahres werden wir also das Update kaufen können. Natürlich kann man auch den kostenlosen letzten „Working Draft“ nehmen, aber ein paar kleine Änderungen sind da nicht enthalten.

Wie zum Beispiel, dass das von C kommende rand() als Funktion nicht mehr verwendet werden soll — diese ist jetzt „deprecated“. Stattdessen wird die Verwendung der C++-Template-Sammlung aus <random> empfohlen.

Ich muss dazu mal ein konkretes Beispiel raussuchen.

0

class X : public Y ist böse — concepts lite, gut?

Posted on by

Hier wird argumentiert, dass in Java extends „böse“ ist.

Während die Formel „ist böse“ natürlich zu einfach ist, stimme ich der Argumentation jedoch zu und ich finde es schön begründet. Mal weniger theoretisch als auch schön an praktischen Beispielen.

In C++ entspricht das wohl dem, dass man besser type traits statt Vererbung verwendenden sollte. Ich sollte den Artikel mal versuchen, nach C++ zu übersetzen. Auf die Schnelle kann ich nur sagen, dass es ja in C++ keine wirklichen Interfaces gibt wie in Java, die man mit „implements“ in seinen Klassen verwenden würde. In C++ ist es sehr wenig verbreitet mit puren „Signatur-Klassen“ zu arbeiten (alle Methoden abstrakt, i.e. pure virtual). Daher kommt dem „implements“ von Java in C++ der Weg über Typparameter von Templates am nächsten, i.e. type traits.

Aber eben nicht komplett, „implements“ kann mehr — lesbare Fehlermeldungen zum Beispiel. Und unter anderem deshalb erwarten wir alle Concepts Lite

Man kann natürlich nicht sagen, 80.000 Zeilen C entspricht 4000 Zeilen C++. Nein, das wäre zu einfach — der gleiche Code in C generalüberholt wäre wohl auch auf 40.000 oder 20.000 Zeilen C zusammengeschrumpft. Auch hängt es sehr stark von den Designzielen und den technischen Details ab, wie lang Code wird. Ich denke, da C++ zu 98% eine Obermenge von C ist, ist das einzige, was man mit Sicherheit sagen kann, dass mit den gleichen Zielen und Prinzipien ein C++-Programm kürzer als das gleiche Programm in C wäre — als theoretische Grenze.

Die theoretische Grenze ist aber uninteressant. Wichtiger ist die Frage, ob sich 4k Zeilen C++ auch leichter warten lassen als 80k Zeilen C. Ha, Und darauf will ich hinaus: Ich behaupte, dass sich sogar 80k Zeilen gutes C++ besser warten lassen als 80k Zeilen gutes C. Aber das ist nur eine Meinung und lädt zum Disput ein 🙂

0

Posted on by

2

Keine Wert-Initialisierung mehr? — oder eine Fürsprache für Privatisierung

Posted on by

Ist es nicht ein Segen, dass man eine eigene neue Klasse auf sooo viele Arten Initialisieren kann?

struct Person {
    string name_;
    int alter_;
    string ort_;
};

Nur 3 Membervariablen, aber 4 tolle Möglichkeiten zum Initialisieren:

Person otto {"Otto", 44, "Aachen"};
Person hans {"Hans", 33};
Person paul {"Otto"};
Person kurt {};

Weil wir die {} hier verwenden werden alle Member initialisiert — die „überzähligen“ Wert-Initialisiert“. Also immer mit {} initialisieren, bitteschön, denn Person xxx; wird *nicht initialisiert. Iih-ba!

Aber ist das überhaupt gut so? Warum sollte man nur einige daten von Person initialisieren wollen? Gut, ich kann mir Szenarien vorstellen, wo man mit der Wert-initialierung zufrieden ist, aber alle diese Möglichkeiten zulassen? Wenn der Datentyp so gedacht ist, dass man mit 0, 1, 2 oder 3 Parametern initialisieren kann, wenn das Sinn ergibt, dann kann ich auch die entsprechenden Konstruktoren dafür bereitstellen. Das ist dann expliziter, und der Leser der Datenstruktor muss nicht überlegen „ist das überhaupt so gedacht?“.

Initialisierung von Typen mit private Daten

Daher, warum nicht gleich private Daten haben? Das verhindert nämlich auch die Wert-Initialisierung. Hier kommt dann höchstens die Null-Initialisierung ins Spiel. Aber der Name ist egal, wir kennen alle den Effekt:

„Wenn Sie keinen Konstruktor selbst definieren, dann erzeugt der Compiler den Default-Konstruktor.“

Dankeschön. Und was bedeutet das? Man hört, der macht gar nichts. Ja, fast. Der macht (manchmal) nichts, aber vorher, da passiert das Interessante.

Aber sehen wir uns ein Beispiel an.

class Rect {
    int area_;  // private Daten
public:
    int x_, y_;
    void set(int x, int y) { x_=x; y_=y; area_=x_*y_; }
    int calc() { return area_; }
};

Da Sie von außen nicht mehr direkt auf diese Membervariable zugreifen dürfen, können Sie sie auch nicht mehr einfach mit geschweiften Klammern {...} befüllen. Sie können nicht Rect r{1,2}; oder Rect s{6,2,3}; — durch das Packen von area_ in den privaten Bereich der Klasse ist Wert-Initialisierung für Rect unmöglich.

Sie benötigen also auf jeden Fall einen Konstruktor. Wir akzeptieren niemals, dass etwas nach der Definition uninitialisiert herumliegt! Das sieht der Compiler zum Glück auch so und daher generiert in diesen Fällen einen Konstruktor, nämlich den ohne Parameter. Dadurch können (und sollten) Sie zumindest Rect t{}; verwenden.

Und was macht dieser generierte Konstruktor? Effektiv initialisiert er Ihr Objekt mit Nullen, bzw. bei Membervariablen komplexerem Typs einem passenden Äquivalent.

Durch so viel Automatismen muss man erst einmal durchsteigen. Denken Sie auch an die Leser Ihres Programms und liefern Sie auch einen Konstruktor mit, und sei es nur der Konstruktor ohne Parameter. Andererseits, wenn der ohnehin nur tut, was der Compiler auch generieren würde, dann können Sie dessen Erzeugnis mit =default anfordern — dann haben Sie es wenigstens explizit gemacht und der Leser sieht direkt >>achja, Initialisierung ohne Parameter.<<.

class Rect {
    int area_;  // private Daten
public:
    int x_, y_;
    void set(int x, int y) { x_=x; y_=y; area_=x_*y_; }
    int calc() { return area_; }
    Rect() = default;  // Compiler Konstruktor generieren lassen
};
class Pow {
    int result_;  // private Daten. hält 'base' hoch 'exp'.
public:
    int base_, exp_;
    void set(int b, int e) { /* ... */ }
    int calc() { return result_; }
    Pow() : result_{1} {}  // base_, exp_ wurden 0, dann muss result_=1 werden.
};

Während Rect() = default; nur die Null-Initialisierung anfordert, die der Compiler sowieso eingesetzt hätte, macht der Konstruktor Pow() tatsächlich etwas: Da base_ und exp_ Null-Initialisiert wurden, muss result_ auf 1 gesetzt werden, denn 0^0=1.

Soso, da gibt es also ein neues Buch vom Erfinder: A Tour of C++, Bjarne Stroustrup.

Ich habe viele Stimmen gehört, die sein Standardwerk zum C++-Standard gut fanden, aber vielleicht war ich damals einfach zu früh.

bei Amazon

Ein schneller Blick in sein neues — viel schlankeres — Werk ist nach meinem Geschmack: Viel Überblick, einige Details. Ein wenig vermisse ich die Zusammenhänge und wie die Sprachelemente vernetzt verwendet werden, aber wie gesagt, es war bisher nur ein schneller Blick, ich werde das prüfen. Ich denke zu dem Preis und der „Dünne“ macht man nichts falsch.

0

Posted on by

5

Swap-Operator?

Posted on by

Manuelles Swap

Elemente zu vertauschen ist an vielen Stellen eine gefährliche Operation. Zu Zeiten ohne Exceptions und ohne Multithreading war vertauschen noch einfach:

Typ temp = a;
a = b;
b = temp;

Aber was passiert hier genau:

  • Typ temp = a; — erzeugt eine neue Instanz und initialisiert sie mit a, per Copy ConstructorTyp(const Typ&).
  • a = b; — ruft den Zuweisungsoperator Typ& operator=(const Typ&) auf
  • b = temp; — ruft noch einmal den Zuweisungsoperator Typ& operator=(const Typ&) auf.
  • Weil es eine neue Instanz ist, wird temp später per Destruktor weggeräumt.

Problempunkte

Bei diesem Prozedere gibt es vor allem zwei Dinge zu beachten:

  • Die Ressourcen, die Typ hält, werden diese wegen des Copy C’tors einmal kopiert.
    • Schade, wenn dies zum Beispiel ein riesengroßer Speicherblock ist, zu dem vielleicht nur ein Pointer getauscht werden müsste
    • Manche Resourcen können nicht kopiert werden: Locks, Mutexe, Filehandles machen Probleme, etc. Dann funktioniert das ganze Vorgehen nicht.
  • Was, wenn b = temp eine Exception wirft? Der Zuweisungsoperator sollte das nicht, aber wenn man dessen Code nicht unter Kontrolle hat? Dann sind a und b nicht vertauscht, sondern identisch. Das alte a ist verloren.

Der bessere Weg

Daher wird seit jeher die Verwendung der swap-Funktion aus der Standardbibliothek empfohlen:

using std::swap;
swap(a, b);

oder seit C++11 auch einfach

std::swap(a, b);

denn die Namensauflösung der swap-Implementierung wurde leicht geändert (Details im Buch 😉 oder bei Stackoverflow).

Edit: Es ist ein Idiom, die swap-Funktion eine friend-Memberfunktion zu machen, auch dazu mehr bei Stackoverflow.

Auch swap soll — wie der Zuweisungsoperator (und andere Beteiligte) — keine Exceptions werfen. Und da für die swap-Implementierung empfohlen wird, die Member elementweise wieder mit swap zu vertauschen, würde statt der Kopie der Ressourcen * im Falle des großen Datenblocks nur der Pointer ge-swapped * im Falle der nicht-kopierbaren Ressource diese vertauscht, was gehen sollte.

Daher merke (seit jeher): Eigene Datentypen sollten swap implementieren.

Neuer Operator :=:

Um die Wichtigkeit von swap zu unterstreichen, gibt es den Vorschlag, swap als Operator :=: einzuführen. Das sähe dann so aus:

struct Typ {
    Data a;
    Feld b;
};
Typ& operator:=:(Typ& lhs, Typ& rhs) noexcept {
    lhs.a :=: std::move(rhs.a);
    lhs.b :=: std::move(rhs.b);
    return lhs;
}
Typ& operator:=:(Typ& lhs, Typ&& rhs) noexcept {
    return rhs;
}

Statt nacheinander die Elemente mit swap zu vertauschen verwendet man den neuen :=:-Operator. Beziehungsweise die &&-Variante, die für das swappen mit der rhs-Instanz da ist, die ohnehin bald weggeworfen würde gestaltet sich die Implementierung besonders leicht: Nimm als lhs einfach rhs.

Dadurch ist hervorgehoben, dass es sich um eine sehr spezielle Funktion handelt, und kommt somit namenstechnisch dem operator= näher. Der Benutzer wird eher darauf gestoßen, dass er sie implementieren sollte.

Es gibt nich weitere kleine Ergänzungen für :=:, zum Beispiel für die eingebauten Typen.

Glaskugel

Hauptsächlich handelt es sich um eine Sytnax-Erweiterung mit nur wenig funktionalen Ergänzungen. Meine Vorraussage ist, dass dieses Feature es nicht in den Standard schafft. Wenn es nur um Syntax geht, war das Kommitee eher zurückhaltend.

Mal was ganz anderes. Wöchentlich bekomme ich von Linkedin Mails, wie folgt:

Torsten,

Congratulations! Your connection Xyz Abc has endorsed you for the following skills and expertise:

Perl

Zugegeben, ich habe mein Profil vor ca 11 Jahren dort erstellt, als ich aus der Bioinformatik (Perl-lastig) heraus einen neuen Job suchte, aber wir wissen ja: Das Internet vergisst nie.

Ja, Perl habe ich mal gemacht, ja ich kann Perl-Programme lesen und zur Not auch erweitern, ja, darum steht da Perl. Nicht dass ich da besonders storlz drauf bin, auch egal ob ich jemals wieder Perl anfassen möchte oder nicht — es steht da.

Schade nur, dass von meinen Kollegen damals niemand wusste, dass ich seit jeher viel lieber (und besser) C++ mache, aber wahrscheinlich mehr „hinter den Kulissen“. Und die noch älteren Kollegen wissen nicht, dass ich vor 12 Jahren zu Python gewechselt bin — in gewissen Kreisen scheinbar nicht so verbreitet, seltsamerweise ;-).

Sagen wir ich habe o.B.d.A. 100 nette Kollegen und Ex-Kollegen…. dann kann ich jetzt zu 90% Perl, zu 50% Python und zu 5% diese extrem esoterischen Sprache… uh, wie heißt die noch? Achja… C++.

Hach, das Internet funktioniert einfach nicht.

/schmunzel
/rant.

Was für ein Überseher! Da gibt es die wichtige und nützliche Funktion make_shared() um einen shared_ptr ssicher zu erzeugen, und in dem ganzen Trubel um die Fertigstellung von C++11 wurde das Pendant make_unique() übersehen. Das lässt sich zum Glück leicht korrigieren und die meisten C++11-konformen Compiler sollten mit dieser Bibliothekskorrektur mitgeliefert werden. In C++14, was auf dem besten Weg ist, ist es dann enthalten.

0

Posted on by

1 2