Forbidden C++

Ich schreibe kein C++, es ist aber trotzdem gut, daran erinnert zu werden, dass Java-Bashing eigentlich nur Jammern auf hohem Niveau ist…

Man mag C++ Programmierer belächeln, aber die Möglichkeit hardwarenah zu programmieren, bietet eben auch nicht jede Programmiersprache und kaum eine Sprache hat so eine Verbreitung gefunden. Natürlich kann man auch alles in Assembler programmieren, aber wer macht das schon heute? Zumal, Assembler noch schwieriger zu portieren ist. Da wird man daran erinnert, dass C++ -Bashing eigentlich nur Jammern auf hohem Niveau ist…

C++ ist die einzige weitverbreitete hardwarenahe OOP Sprache. Das macht sie noch lange nicht gut, nur alternativlos.

*Edit: Wobei die letzten Jahre zum Glück Rust an Fahrt aufgenommen hat.

Ich habe auch nicht behauptet sie wäre gut. Ich fand nur, dass das Jammern über eine Sprache relativ nichtssagend ist, wenn man nicht den hauptsächlichen Verwendungszweck berücksichtigt. Und ja auch wenn mein Beitrag eher sarkastisch in Bezug zu Assembler gemeint war, nehme ich mich jetzt auch da ein wenig mit der Kritik an Assembler zurück, da es durchaus Fälle gibt bei denen Kenntnisse derer durchaus notwendig sein können.

Sowas wie globale Variablen gibt es in Java auch. Nur sind sie (wie so vieles anderes) da zumindest „sauberer“, und wenn man darauf zugreift, ist viel leichter (am Code) zu erkennen, wo sie eigentich liegen. (Aber dass man sie nicht verwenden sollte, ist klar…).

Dass #include und macros eigentlich nur fancy Text-Ersetzer sind, ist ein Grund letztere praktisch auch nicht mehr zu verwenden. Aber um include kommt man eben nicht rum :wink: Wenn man mal ein „etwas größeres“ Projekt mit C++ schreibt, und da ein paar Klassen drin vorkommen, ist die Redundanz, die man mit Headern generiert, schon frustrierend: Da steht nur eine Liste von Funktionen, die genau so nochmal in der .cpp-Datei stehen. Zu 99% könnten die Header automatisch aus den Implementierungen generiert werden … wenn man denn bei den Implementierungen noch die Sichtbarkeits-Modifier dazu schreiben könnte…

(Außer natürlich, wenn man templates benutzt - dann steht der ganze Code auf einmal in den Headern. Warum? Naja, das muss halt so sein. Templates sind auch nicht mehr als „Textersetzung“. Und tatsächlich gibt es keinen C++ - Compiler, der bei jedem beliebigen Stück Code feststellen kann, ob dieser Code ein gültiges C++ - Programm ist: Template-Metaprogrammierung ist Turing-Vollständig, d.h. so ein Compiler müßte das Halteproblem lösen können - autsch…)

Memory leaks sind ja der Klassiker: Erst wurden malloc und free durch new und delete ersetzt (oder delete[] - immer schön aufpassen, das richtige zu verwenden!), und jetzt geht die Tendenz da hin, das auch zu verbannen. Und wenn man die Alternativen einsetzt … verhält es sich fast so wie Java, auch, wenn es nicht so aussieht.

Einige der schlimmsten Hirnverknotern von C++ hat er gar nicht erwähnt: Sowas wie https://de.wikipedia.org/wiki/Argument_dependent_name_lookup (mal ehrlich: Meint irgendjemand, dass das (nein, ich frage nicht, ob es ‚Sinn ergibt‘, sondern nur, ob das) irgendwie gerechtfertigt sein kann?), oder die komplett verbockten Integer-typen: long unsigned int long ist ein Typ - yay…)

Man kann mit C++ viele Sachen machen, die in Java ein Krampf sind:

  • Wenn man sich java.util.Arrays ansieht, wünscht man sich schon, dass man die Spezialbehandlungen der ganzen primitiven Datentypen mit sowas wie Templates vereinfachen könnte. Darüber hinaus könnte man argumentieren, dass es blöd ist, dass man in C++ bei Funktionen, die etwas mit einem Pointer machen sollen, immer ein int length übergeben muss - aber bei Java muss/müsste man konsequent bei jedem primitiven Array eigentlich int fromIndex, int toIndex übergeben (wie es auch in java.util.Arrays gemacht wird … meistens…). Es gibt eben keine Zeigerarithmetik… (Die buffers, wie ByteBuffer, bieten teilweise Funktionen, die dem recht ähnlich sind, aber … die sind sperrig…)

  • Das komplett manuelle Speichermanagement mit new und delete ist natürlich eine eklige Fehlerquelle, und sorgt für einen „kognitiven Overhead“ beim Programmieren, der meistens unnötig ist: Man muss immer im Hinterkopf behalten, wann und wo welcher Speicher freigegeben wird (bzw. welcher Speicher wem „gehört“). Aber… manchmal sind Destruktoren schon nicht schlecht: Es gibt einen definierten Punkt (in Zeit und Code (!)) , an dem das Objekt gekillt wird. Das kann praktisch sein, für Aufräumarbeiten (irgendwelche Channels oder Verbindungen schließen und so). Zu versuchen, das mit Object::finalize zu emulieren ist grundsätzlich zum Scheitern verurteilt. Soft- und Weak References sind nur für wenige Szenarien ein geeigneter Ersatz. Zumindest konzeptuell kann man einiges davon durch try-with-resources emulieren, aber … das ist immer auf den { block scope } eingeschränkt…

  • Einige Typen in Java, wie z.B. sowas wie BiFunction<? super Collection<? extends Number>, ? super Collection<? extends Number>, ? extends Collection<? extends Number>> sind schlicht grauslig - und in mancher Hinsicht noch schlimmer als sowas wie std::vector<std::shared_ptr<my::namespace::Person>> persons;. Aber in C++ gibt es wenigstens typedef, um das ganze abzukürzen…

Wenn man die Sprache geschickt einsetzt, kann man wahnsinnig generischen, eleganten und effizienten Code schreiben. (Gut, er ist halt fehleranfällig, schwer zu debuggen, und tendenziell unwartbar für alle, die ihn nicht selbst geschrieben haben, aber … es gibt Situationen, in denen das nicht so wichtig ist). Und vieles aus std und boost ist ungeheuer mächtig, und ~„sowas hätte ich gern in Java“. Aber der Preis, den man dafür zahlen müßte, ist einfach zu hoch.

1 Like

m2c,

ich benutzere Java gerne, ich benutze C# gerne und ich benutze C++ gerne. Ein Vergleich zwischen C++ und Java / C# hinkt gewaltig. Die Ziele sind einfach andere. Auf einem kleinen Prozessor (z.B. <2kB Programspeicher) wird Java und C# nicht laufen. Wer C++ für GUI und größere Anwendungen verwendet, hat sich selbst ein Problem geschafft.

ACK - Java/C# versuchen Probleme zu eliminieren die in C++ einfach vorhanden sind (auf Grund der History). Bestes Beispiel ist die Länge des übergebenen Array. In Java/C# wird einfach der Luxus gemacht die Länge immer mitzuschleppen, in C++ habe ich evt. nicht den Speicherplatz.

hypotetisches Beispiel (ich kenne die Implemntierung in Java nicht): In Java wird die Länge mit 4 Bytes gespeichert. Das kann auf einem embedded Prozessor schon zu Speicherplatz führen. Wenn ich weis das die Länge Arrays nie 255 übersteigt, habe ich bei einem int (4-byte-int) schon mal 3 bytes verschwendet. Würde mir aber C++ wie in Java die Länge liefern muss der Compiler den Speicherplatz für die Länge festlegen. Ich habe also gute Chancen das Speicher verschwendet wird.

Verwende ich eine globale Variable muss ich damit rechnen, das mir irgend jemand die Variable ändert. Wenn ich da natürlich blauäugig ran gehe, habe ich ein Problem. Allerdings erschließt sich mir jetzt nicht wieso mir da Java hilft, wenn ich die Variable in eine Klasse packe. Ich kann sie auch schön mit Gett & Setter abschotten - aber ein schreibender Zugriff ist denn noch möglich.

Und ob die Variable in einer Klasse liegt oder irgendwo in einer *.cpp Datei ist zur Laufzeit egal. Denn dort liegt sie zum Schluß auf dem Heap.

Aufräumen und sinnvoll Strukturieren! Gleiches Problem hast Du auch in Java. In Java ist es noch gefährlicher. Während man unter Java ganz schnell einen Kreis bekommt, meckert der Compiler unter C++ erstmal kräftig rum.

public class ClassA {
	public ClassB generate() { return new ClassB(); }
}
public class ClassB {
	public ClassA generateA() { return new ClassA(); }
}

Unter C++ muss man mit Forward-Deklarationen „tricksen“. Und von Forward-Deklarationen sollte man (IMO) einfach mal die Finger lassen.

das ist purer Syntax-Zucker für den Programmierer, wird auf free() und malloc() umgeleitet

void * operator new(size_t size) { return malloc(size); }
void operator delete(void * ptr) { free(ptr); }

Fancy-Stuff um seine Existenz als Freelancer zu rechtfertigen, ist für mich nicht wirklich lesbar. Hat (IMO) auch nichts mehr von vernünftiger Programmierung.

:grin: das ist der Kritikpunkt von meiner Seite an Java / C# & Co. Das ist nicht mehr deterministischen Programmverhalten. Zumal mit Dispose() unter C# eine Destruktor eingeführt wurde, den ich manuell aufrufen muss! Damit ist das: „delete ist egal, brauchst du nicht mehr“ völlig hinfällig. Wer unter C# mit Grafik gearbeitet hat um Bilder zu bearbeiten, merkt schnell wie der Speicher sich voll frisst. Man muss ein Dispose() auf das Objekt aufrufen, bevor man es vergessen darf.

Wie üblich beim Sprachbashing: Man kann Argumente anführen, die nur dann Gewicht haben, wenn man die Verwendung einer Sprache in einem bestimmten Kontext betrachtet. Natürlich muss man das in vieler Hinsicht. Aber um mir mal einen Vergleich für den Vergleich (!) aus den Figern zu saugen: Man kann (ganz objektiv) sagen: „Deutsch verwendet das lateinische Alphabet, das für die am meisten gesprochenen Sprachen verwendet wird, und Russisch verwendet das kyrillische Alphabet, das nur für wenige Sprachen verwendet wird“. Das Argument „Wenn man in Russland ist, ist es besser, Russisch zu können“ liegt auf einer ganz anderen Ebene. Wenn ich nur 640KB Speicher habe, verwende ich keine JVM. (Und auch nicht C++, sondern C, aber das nur nebenbei).

Ähja… dieser „Kreis“, wie du es nennst, bezieht sich auf die Ausführung. In C++ hat man ja schon ein Problem mit der Modellierung dieses Kreises. Und wenn sich zwei „Dinge“ gegenseitig einfach nur kennen sollen, führt an irgendeiner Header-Gymnastik kein Weg vorbei…

Mein Argument mit der Redundanz bezog sich übrigens auf was anderes. Als sehr einfaches Beispiel (an dem der tatsächliche Overhead und die Redundanz nur andeutungsweise erkennbar wird), mal folgende Implementierung:

Date::Date(int y, int m, int d)
{
    SetDate(y, m, d);
}

void Date::SetDate(int y, int m, int d)
{
    year = y;
    month = m;
    day = d;
}

An diesem Code (aus einer .CPP-Datei) kann man ablesen:

  • Es gibt eine Klasse Date
  • Die Klasse hat einen Konstruktor mit 3 ints
  • Die Klasse hat eine SetDate-Methode mit 3 ints
  • Die Klasse hat 3 fields namens year, month und day.

Und was steht im Header? Ja nun, genau das - nur eben noch gruppiert nach private und public:

class Date
{
private:
    int year;
    int month;
    int day;

public:
    Date(int y, int m, int d);
    void setDate(int y, int m, int d);
};

Man stelle sich da jetzt noch jeweils ein setter+getter für die 3 fields dazu, dann merkt man langsam, wie albern das ist. Ich weiß, dass es „Gründe“ gibt, warum das so ist - aber auch wenn man es begründen kann, ändert das nichts daran, dass es redundant und lästig ist. Pub Quiz: Im Header ist ein Fehler. Finde ihn, durch Draufschauen…

Worauf bezieht sich das? ADL ist ein inhärenter Teil von C++, den man kennen und verstehen muss, um souverän C++ programmieren zu können.

(Ein eher subjektives Argument für C++ - bashing ist: Leute meinen, sie würden C++ programmieren, aber in Wirklichkeit picken sie sich einige Sprachfeatures raus und schreiben damit „ihren“ Code in „ihrem“ Stil. Der eine verwendet einen eher C-ischen Stil, der andere einen Java-ishen, jemand anderes geht die extra Meile mit Templates… und keiner wird den Code des anderen verstehen oder warten können. Nicht falsch verstehen: An sich ist nicht verkehrt, wenn eine Sprache viele Möglichkeiten bietet. Aber das kann auch nach hinten losgehen…)

Ich stimme dir generell zu. Und trotzdem finde ich ist C++ der letzte Rotz. Nicht verglichen mit irgendeiner anderen Sprache, sondern absolut gesehen die Sprache selbst, die Compiler, die IDEs, die Tools, die Dokumentation, die Bibliotheken und die „Community“ die dahinter steht.

Microcontroller hin oder her. Ich wünschte mir so sehr, man könnte einfach in einer abgespeckten C-Syntax OO Sprache hardwarenah programmieren, ohne den ganzen unausgegorenen eierlegende Wollmilchsau Nonsense.

Die Sprache hat kein Ziel, kein Problem das sie zu lösen versucht, sie wird in praktisch allen Projekten zum Problem selbst. Die eine Sprache ist ein Hammer, die Andere ein Schraubenzieher, aber C++ ist nur ein Klotz Stahl aus dem du dir dein Werkzeug erst passgenau schmieden kannst - oder erst musst.

Das wird zu einem Problem moderner agiler Softwareentwicklung. Zu C Zeiten hat man vielleicht eine Software einmal entwickelt und war fertig, aber das ist halt nicht mehr.
OOP hat an dem Problem leider nichts geändert, wie einige sicherlich anfangs gehofft hatten mit „austauschbarem Code“. Java ist aber einfach schneller und sicherer heruntergeschrieben, während bei C++ all die Redundanz zu nutzloser Fehleranfälligkeit und zusätzlicher verlorener Zeit führt.

Soll der Code zudem „sicher“ sein, also das was man bei Java durch striktes Regelwerk und limitierten Features erreicht hat, muss man C++ so ineinander verkapseln und mit Sicherheitschecks versehen, dass man sich davon verabschieden sollte den jemals wieder anzufassen.
Aber gut, das mag bei einem Microcontroller sicherlich weniger ins Gewicht spielen.

Ja, erzähl das mal C++ Entwicklern. In meiner letzten Firma hatten wir so einen Fanboy. Alles musste in C++ sein und wenn er alles selber neu schreiben musste. Ergo:

  • Die Firma hat eine eigene C++ GUI-Lib (die fernab von jedem Standard ist. Per design musst du Redundanten Code schreiben!)
  • Die Firma hat auch eine eigene C++ Lib für Mobile CE-Geräte!
  • Ich durfte einen kleinen WebServer in C++ schreiben (der zum Glück nie zum Einsatz gekommen!)

Es war ein Kraus. Wenn du ein C++ Programm nicht im Detail kennst, dann wirst du sehr viel Spaß haben. Bei uns kam der Fanboy mal auf die Idee die standard-string-lib zu überschreiben. Sachen von denen man erwartet, dass Sie einfach gehen, gehen nicht mehr. Wie wahrscheinlich ist es, dass man da den Fehler vermutet?

Aber mein Hauptkritik-Punkt an C++ und warum ich es niemals wieder verwenden möchte ist die build-time!

So hab ich z.B. immer mal wieder auf die Unreal-Engine „geschielt“. Hab mich gefragt, ob ich Sie nicht doch wenigstens mal hätte ausprobieren sollen. Und dann hab ich vor kurzem mitbekommen, was andere Projekte da z.T. für Build-Zeiten haben. Manche warten bis zu 4h auf ihren Build (was das für Projekte sind - ka. Aber in Vergleichen Unreal VS Unity werden die Build-Zeiten immer wieder als Kritikpunkt aufgeführt).
Da ich aber ähnliche Probleme auch in meiner alten Firma hatte glaube ich das gerne. Auch wenn ich da keine Stunden auf meinen build warten durfte. Trotzdem hab ich mir immer sehr gut überlegt, ob ich eine Änderung machen möchte oder nicht. Es gab Klassen, die wolltest du nicht anfassen, weil klar war: eine Anpassung und du kannst in die Mittagspause gehen.

Build-Time. Grauenhaft. Selbst für µC Projekte läuft der mehrere Minuten. Irgendwoher kommt halt der Begriff „night“ build. Weil große Projekte gerne über Nacht kompiliert werden.

Schön wenn man dann am nächsten Tag sich endlich an die Arbeit machen möchte und der Compiler kommt dann mit einer typisch kryptischen C++ Warnung, das irgendwas, irgendwo nicht funktioniert hat, in der Zeile 1:0. Und nach stundenlangen Suchen stellt sich dann heraus das irgendwo ein Semikolon gefehlt hat.

Dann wieder am nächsten Tag, fertig compiled, das Programm wird ausgeführt und gibt falsche Ausgaben aus. Stellt sich nach weiteren Stunden heraus, dass in irgendeinem if Statement, irgendwo im Code, ein Zuweisungsoperator „=“ statt einem Vergleichsoperator „==“ geschrieben wurde. Weil das ist völlig valides C++.

Und dann haut man das wieder durch den Compiler. Das ist Zeit die da verloren geht, hör mir auf.