No New New: Das Ende von Zeigern in C++

Na, das ist mal ein Paukenschlag:

*sich zurücklehnt und einen Schluck Java-Kaffee nimmt* „Sucks, heh?“ :sunglasses:

(EDIT: BTW, ja, es wird noch diskutiert, ob das ein Aprilscherz ist. Wir werden sehen…)

Der Titel ist im zweiten Teil ein Clickbait: Die Zeiger wurden nicht entfernt, lediglich die Möglichkeit neue Objekte über new zu erzeugen, wurde abgeschafft.

Streng genommen hat nicht mal Java Zeiger abgeschafft, da prinzipiell alle neu erzeugen Objekte pass-by-reference sind, also nicht anders wie Pointer funktionieren. Das Einzige, was vorteilhaft für den Programmierer ist, dass er selbst nicht Verwaltung nicht übernehmen muss, da der GC alles für ihn macht.

Es gibt auch Sachen die bei C/C++ besser sind als bei Java: Beispielsweise die Rückgabe von Werte über Pointer. Das geht, glaube ich, nicht so elegant wie bei Java. Sogar Pointer auf nicht null überprüfen geht eleganter.

void some_function(long *number) {
    if(number)        /* number pointer auf nicht null geprüft */
        *number = 42; /* Rückgabe von 42 über die Variable number */
}

int main() {
    long number;
    some_function(&number); /* Übergabe eines Pointer der Stackvariable number */
    printf("%l\n", number); /* Gibt 42 aus. */
}

Offtopic: Ich verstehe bisher immer noch nicht warum Java keine primitiven unsigned Typen hat. Würde jede Menge negativ Tests ersparen, indem man im vorhinein sagt, dass keine negativen Werte zugelassen sind.

char?

(Ich weiß, nu werd ich fertiggemacht :wink: )

Hmm… doch wohl ein Aprilscherz, muss aber immerhin nicht alles, was ich gesagt habe, zurückziehen.

1 „Gefällt mir“

Zumindest haben sie sich Mühe gegeben, mit mehreren simultanen, ausführlichen Blogbeiträgen.

Sie hatten Recht :slight_smile:

Dazu passt:

Es ist tatsächlich so, dass damit viel Verwirrung und viele Fehler entstehen. Und auch wenn das folgende erstmal kein fachliches Argument ist: Man kann sich denken, dass es schwer wird, für unsigned types zu argumentieren, wenn dort das halbe C++ - Standardkommitee sitzt und mehrmals kleinlaut-zähneknirschend zugibt, dass die Tatsache, dass die STL size_t (also einen unsigned type) verwendet, ein Fehler war („It was a mistake“, „We were young“…). Der arme Bjarne wirkt auch ganz geknickt, wenn er dort über die Fehlerträchtigkeit redet :wink:

(Und wenn man sich mal sowas wie Fundamental types - cppreference.com ansieht, weiß man die Verläßlichkeit und Klarheit von Javas primitiven Typen vielleicht eher zu schätzen. Das alles soll aber nicht heißen, dass unsigned nicht manchmal praktisch wäre - in sehr wenigen Fällen, aber dann schon recht praktisch…)

Ansonten könnte das hier zum üblichen Sprachbashing werden. Wer darüber argumentieren will, kann sich

durchlesen und sich dann sein eigenes Urteil bilden :sunglasses:

Ja, es wäre wohl sicher besser gewesen size_t signed zu machen. Das unsigned hat C/C++ wohl denkbar schlecht gelöst. Ich schwärme da jedenfalls für Rust. Wenn man bei Java nicht an ein unsigned gedacht hat, hätte man meiner Meinung nach eine andere Möglichkeit zur Definition vom Wertebereich anbieten können. Würde auf jeden Fall mühsame Checks ersparen

Jetzt bin ich neugierig: Auf welche „mühsamen checks“ beziehst du dich hier? Der einzige Fall, wo mir das bisher relevant erschien, war bei seeehr low-leveligen Dingen, wie wenn man Daten zwischen Java und einer nativen Lib (via JNI) austauschen will, oder bei Dingen wie low-level-Netzwerkprotokollen. Und auch da nur selten: Oft kann man die Daten „wie-sie-sind“ hin-und-her schaufeln, und muss nur ggf. auf der einen oder anderen Seite aufpassen, dass man nicht einen C-unsigned char mit einem Wert von 210 auf Java-Seite in ein byte zu stecken und damit dann zu Rechnen (d.h. echte Arithmetik zu machen!) versucht.

Die Integer types in Rust sehen beim ersten Überfliegen recht straightforward aus. Dass bei einer frisch designten Sprache keine Auswüchse wie unsigned long long int vorkommen, sollte man aber erwarten können. Das haben sie in C++ halt schon richtig verbockt :slight_smile: (Es gibt Gründe dafür, dass es so ist, wie es ist, aber IMHO kaum echte Rechtfertigungen für eine High-Level-OOP-Sprache…)

Ein einfaches Beispiel: Wenn ich beispielsweise mehrere Klassen für die Uni programmiere, die oft nur mehrere positive Zahlen enthalten, nervt es mich die Setter zu schreiben, da jedes mal die Übergabewerte auf ‘Positivheit’ überprüft werden müssen.

Grundsätzlich leuchtet das ein. Aber der Teil, den sie bei C++ eben besonders verbockt haben, ist der, der gerade dann relevant wird (und ein Beispiel hatte ich in meiner Antwort auf obige Stackoverflow-Frage beschrieben) :

void foo(size_t size) { ... }

int x = ...;
int y = ...;
foo(x-y);

Sicher, size wird nie negativ. Aber wenn y>x gilt, dann ist size eben auf einmal z.B. 4294967295, und es gibt keine Möglichkeit, diesen Fehler zu erkennen.

Spezifischer: Wenn man unsigned-Typen anbietet, (schwurbel: bilden die arithmetischen Operationen einen affinen Raum) dann muss man sehr viele Regeln für die Konvertierung etablieren.

Beim Überfliegen und einer kurzen Suche hab’ ich da jetzt in der Rust-Doku nichts gesehen (aber ich hoffe, dass es irgendwo definiert ist).

Was ist denn nun das Ergebnis von

u32 x = ...;
u32 y = ...;
??? z = x - y;

Ist ??? nun i32 oder u32, oder kann das beides sein? Und was passiert bei y>x, und wie werden Überläufe gehandhabt? Das ist alles nicht so einfach.

Meine long-standing challenge ist ja folgende: Implementiere folgende Funktion in C++:

int32_t add(int32_t x, int32_t y) {
    ...
}

Sie soll die übergebenen Zahlen addieren.

Sonst nichts.

(Bisher hat sich niemand getraut, diese Herausforderung anzunehmen. Und das liegt vielleicht daran, dass ich sie so stelle, und klar ist, dass praktisch jeder, der es versucht, es erstmal “falsch” machen wird, aber kaum jemand weiß, was er falsch machen wird…)

Lies dir mal Myths and Legends about Integer Overflow in Rust durch. Vielleicht wird da einiges klarer über das Verhalten von Rust. Prinzipiell wird dich der Rust Compiler warnen, so eine Zuweisung zu machen. Zur Runtime kommt es zu einem Panic und beendet abrupt, falls das Ergebnis negativ war, wenn du u32 als Ergebnisdatentyp versuchst.

Hängt davon ab, was du für eine Zahl erwartest. Binär addieren ist ja kein Problem, lediglich die Interpretationsmöglichkeiten des Ergebnisses und der Fall, dass sich keine sinnvolle Interpretationsmöglichkeit findet, ist das Problem.

Der Artikel ist etwas zu lang und detailliert in Relation zu dem geringen Bezug, den ich zu Rust habe, aber zumindest haben sie sich was überlegt, und die „modi“ für debug/release klingen zumindest schonmal sinnvoll.

Versuch’s - irgendwie :wink:

Deine gewünschte Funktionsdefinition bietet nicht sehr viel Freiraum. Zumal keine weitere Informationen beigelegt werden, um mit absoluter Sicherheit zu sagen, dass das Ergebnis stimmt. Insofern bringt so eine Funktion nicht wirklich etwas, da x + y dasselbe machen würden. Außerdem würde ich Optionals in dem Fall gegenüber Exceptions bevorzugen, wenn es C++ sein muss.

Ich weiß, es ist gemein. Aber doch unterhaltsam, wie du dich windest :smiling_imp:

Personalchef: „Können sie eine Funktion schreiben, die zwei Zahlen addiert?“
Bewerber: "Najaaaaa… "

:smiley:

Probier du doch mal mit multiplizieren. Vielleicht funktioniert es ja auch ohne meiner add Funktion :smiling_imp: .

int32_t multiply(int32_t x, int32_t y) {
    ...
}

(Diese Beispiele wird man vermutlich deswegen falsch machen, weil man nicht hundertprozentig davon ausgehen kann, dass sich bei den Eingabewerte um signed integer handelt, sondern es sich auch um gecastete unsigned Werte handeln könnte).

OK, um das aufzulösen: Wenn man das mit return x+y; implementieren und es dann mit

int32_t x = 2000000000;
int32_t y = 2000000000;
int32_t z = add(x,y);

aufrufen würde, gäbe es einen integer overflow, und signed integer overflow ist undefined behavior. Das heißt: Wenn man dieses Programm dann compiliert und ausführt, könnte es

  • -294967296 ausgeben (was es vermutlich tun würde)
  • 0 ausgeben
  • 42 ausgeben
  • abstürzen
  • auf YouTube gehen und ein lustiges Katzenvideo abspielen
  • die Festplatte formatieren

und das alles wäre richtig und zulässig im Rahmen dessen, was durch die Sprache C++ selbst zugesichert wird. Klingt verrückt. Ist aber so.

Was wäre denn eine von dir akzeptierte Lösung? :wink:

Auf int64_t casten, dann rechnen und zurück casten (wäre der Downcast auch undefined behavior)?

Alternativ vor’m zurück casten das Ergebnis prüfen, und wenn’s zu groß ist, entsprechend behandeln (für das „entsprechend“ müsste man dann aber mehr als nur die Methoden-Signatur kennen)?

Kann mich noch an dein UB rant vage erinnern. Ja, die Sprache sichert dir kein definiertes Verhalten zu, daher bist du als Programmierer in der Verantwortung, wenn du kritisches Zeugs machst, dass dir das gewünschte Verhalten zugesichert wird. C/C++ ist halt keine Sprache bei der die Batterien dazubekommst. Du kriegst eigentlich nur das, was dir der Prozessor an Befehlen bereits bietet, mehr nicht. Die Batterien um verlässliche Programme zu schreiben, musst dir irgendwo her holen (Internet, Bücher, etc.) oder dir selber zusammenbasteln.

Außerdem gibt es sowieso keine Möglichkeit zwei Zahlen zu addieren, die unabhängig von Rechnerarchitektur das exakt gleiche Verhalten bewirkt. Manch werfen halt ein Interrupt, wenn es zu einem Overflow kommt, andere setzen halt ihre Register Flags. Das zu vereinheitlichen halte ich zwar für einen netten Versuch, aber nicht für besonders effizient und würde eine ohnehin schon große semantische Lücke vergrößern.

Außerdem vergisst du, das Compiler auf den jeweiligen Befehlsatz deines Prozessors hinoptimieren. Insofern, wenn du einen Prozessor hinkriegst, der es schafft mit einem einzigen Assemblerbefehl auf Youtube zu gehen und lustige Katzenvideos abzuspielen oder die Festplatte zu formatieren, dann hast du mich überzeugt. :wink:

.

https://java.com/en/ :sunglasses:

Aber mal im Ernst: Es kann richtig kompliziert werden. Einiges steht dazu hier:

Und nochmal: Wenn man das liest, und sich denkt: „Was soll das denn für’n schräger Sche!ß sein, das ist ja vollkommen unpraktikabel?!“ dann kann ich dazu nur sagen: Ja. Stimmt. Ist aber so.

Wie man damit umgeht, ist dann nochmal eine andere Frage. Man kann sich das „2-complement-wrap-around“ vermutlich selbst bauen. Aber da die function kein noexcept hat, kann man da auch irgendwas rauswerfen. bad_alloc oder so. Man hat ja alle Freiheiten :clown_face:

Irgendein Prozessor. Gerechtfertigt wird diese Beliebigkeit (UB) ja immer gerade damit, dass man mit C++ ja auch einen chinesichen 14-bit-Microcontroller von 1973 programmieren kann, der eben nicht das „übliche 2-complment-overflow-Verhalten“ hat, und man den (aus Performancegründen) nicht in das Korsett einer klaren, verläßlichen Spezifikation zwingen will.

https://java.com/en/ :sunglasses:

Ich denke, dass an diesem spezfischen Punkt die Beliebigkeit und Unspezifiziertheit schlicht zu weit getrieben wurde. Wenn man sowas macht wie

int array[10];
array[1234] = 5678;

dann schreibt man damit an eine ungültige Speicherstelle. Dass man das als „UB“ deklariert, und mit einer Mischung aus Neid und Verachtung(!?) auf die ArrayIndexOutOfBoundsException schielt, ist noch akzeptabel. Der Fall ist auch leicht zu prüfen: if (index < 0 || index >= length) throw up; und gut.

Aber das auf die gleiche Stufe zu stellen wie die Addition zweier Zahlen ist schlicht unangemessen. Für den Integer Overflow wäre es m.E. vollkommen legitim, zu sagen, dass „das Ergebnis nicht spezifiziert“ ist, und demnach -29496729, oder 0 oder 42 rauskommen darf. Aber dem Programm bei so einer alltäglichen und praktisch nicht überprüfbaren (!) Sache alles zu erlauben zieht für mich (und ich weiß, dass andere das anders sehen) der Programmiersprache C++ an sich jegliche Kredibilität unter den Füßen weg.

Wohl wahr. Unter diesem Gesichtspunkt wäre vermutlich Assembler eine ernstzunehmendere Programmiersprache.

Assembler ist spezifisch(er) für die darunterliegende Hardware. Und da würde man wohl sagen, dass man schlicht wissen muss, wie sich der Prozessor im Falle eines Überlaufes verhält. Manche setzen ein Carry-Bit. Andere könnten irgendeinen Interrupt triggern oder whatever. Aber diese Beliebigkeit durch eine ““High-Level-Sprache”” zum Programmierer durchzuschleifen (soweit ich bisher gehört habe nur mit dem Argument, dass man den Compilerbauern nicht vorschreiben will, wie sie das jeweilige Verhalten des Prozessors auf die Beobachtung umbiegen müssen, die der Programmierer in diesem Fall macht) finde ich fragwürdig.