1 Punkte von GN⁺ 18 시간 전 | 2 Kommentare | Auf WhatsApp teilen
  • Die C++-Standardbibliothek hat seit C++11 wiederholt fehlkonzipierte Entwürfe entweder offiziell verworfen oder neben neuen Ersatzfunktionen stehen lassen, sodass Entwickler wissen müssen, aus welcher Zeit eine „Ebene, die man nicht verwenden sollte“, stammt
  • Zur Ebene der offiziellen Rücknahmen gehören Einträge wie std::auto_ptr, dynamische Ausnahmespezifikationen, die C++11-Garbage-Collection-Schnittstelle und std::aligned_storage, also Elemente mit Deprecation- oder Removal-Papers; auch std::function befindet sich in einem 15-jährigen Ersatzpfad, flankiert von std::move_only_function, std::copyable_function und std::function_ref
  • Zur Ebene der inoffiziell gemiedenen Features gehören das langsame std::regex, std::async, dessen Warten im Destruktor Deadlock-Fallen erzeugt, sowie <iostream>, std::list, std::deque und std::vector<bool> – Funktionen, die im Standard bleiben, aber in produktivem Code umgangen werden
  • Besonders deutlich zeigen sich Probleme bei den Standard-Containern std::unordered_map, std::map und std::list; im Benchmark mit identischer Workload liegt P99 für die naive C++-Implementierung bei 302.653 cycles, für die naive Rust-Implementierung bei 5.177 cycles – ein Faktor von 58
  • Die Entscheidung für ABI-Stabilität ist der zentrale Unterschied dazu, wie andere Sprachen Fehler durch Entfernen, Editions oder große Versionswechsel reduzieren: In C++ bleiben falsche Defaults innerhalb von std:: dadurch faktisch dauerhaft erhalten

Ausgangspunkt: die Einstufung von std::function als „Legacy“

  • Sandor Dargos Schnellübersicht zu std::copyable_function ordnet std::function als „Legacy. Avoid in new code.“ ein
  • std::function wurde in C++11 aufgenommen, der neueste Ersatz-Wrapper std::copyable_function kommt in C++26, und die Empfehlung des neuen Features ist weniger „verwende dies, wenn du ein kopierbares Callable brauchst“ als vielmehr „verwende das ursprüngliche nicht“
  • std::function hat bei const operator() einen const-correctness-Fehler, durch den non-const-Callables aufgerufen werden können; beheben lässt sich das nicht, ohne die ABI zu brechen
  • Als Reaktion auf diesen Fehler stehen std::move_only_function in C++23 mit P0288R9, std::copyable_function in C++26 mit P2548R6 und std::function_ref in C++26 mit P0792R14 in derselben Entwicklungslinie

Standard-Features, die offiziell zurückgenommen wurden

  • std::auto_ptr wurde in C++11 zur Deprecation vorgesehen, weil seine Copy-as-Move-Semantik generischen Code und Standard-Container zerstörte, und in C++17 per N4190 entfernt; dasselbe Paper entfernte auch die C++98-Adapter in <functional> sowie std::random_shuffle
  • std::random_shuffle wurde durch std::shuffle ersetzt, weil es von std::rand und globalem Zustand abhing
  • Dynamische Ausnahmespezifikationen throw(X, Y) wurden in C++11 zur Deprecation vorgesehen und in C++17 mit P0003R5 entfernt; das Alias throw() wurde in C++20 per P1152 entfernt
  • std::iterator wurde in C++17 per P0174R2 zur Deprecation vorgesehen, und die Entfernung in C++26 wird in P3365R1 vorangetrieben; die Ersatzmethode besteht darin, die fünf typedefs direkt zu definieren
  • std::aligned_storage und std::aligned_union wurden in C++11 eingeführt und in C++23 mit P1413R3 zur Deprecation vorgesehen; als Probleme gelten typename ::type-Boilerplate, reinterpret_cast, undefiniertes Verhalten bei Len == 0 und fehlendes constexpr
  • std::not1, std::not2, unary_negate und binary_negate wurden in C++17 zur Deprecation vorgesehen, in C++20 entfernt und durch std::not_fn aus P0005 ersetzt
  • Die C++11-Garbage-Collection-Schnittstellen rund um std::declare_reachable wurden in C++23 per P2186R2 entfernt, nachdem große Implementierungen nie tatsächliche Garbage Collectors bereitgestellt hatten
  • Auch Concepts TS, Modules TS, Coroutines TS, Reflection TS, Executors TS und Networking TS wurden vor der Zusammenführung neu entworfen, ersetzt oder verschoben; Reflection wechselte zu P2996, Executors zur Sender/Receiver-Linie mit P2300

Features, die im Standard bleiben, in der Praxis aber gemieden werden

  • std::regex kam mit C++11, aber P1844R1 hält im Ausschussprotokoll fest, seine Performance sei „deutlich schlechter als andere verfügbare Lösungen“; Ersatzpfade sind CTRE und P1433R0, außerhalb des Standards vor allem Boost.Regex, RE2 und PCRE2
  • Bei std::async blockiert der Destruktor des zurückgegebenen future bis zum Abschluss der asynchronen Aufgabe; N3679 dokumentiert die daraus entstehenden Deadlock-Fallen
  • <iostream> ist langsam, an locale gebunden, beim Formatieren nicht thread-safe und für berüchtigte Fehlermeldungen bekannt; trotzdem ist es auch nach std::format aus C++20 per P0645 sowie std::print und std::println aus C++23 per P2093 nicht zur Deprecation vorgesehen
  • std::list ist eines der Beispiele, bei denen Bjarne Stroustrup in der GoingNative-Keynote 2012 zeigte, dass sogar Workloads mit Einfügungen in der Mitte von std::vector gewonnen werden; auch der spätere Beitrag Are lists evil? kommt praktisch zu „ja“
  • std::deque ist ein Fall, bei dem das öffentliche Microsoft-STL-Issue microsoft/STL#147 festhält, dass die vom Standard erzwungene Blockgröße zu klein ist und beim nächsten ABI-Break eine umfassende Performance-Überarbeitung nötig wäre
  • std::valarray wurde 1998 als numerischer Container eingeführt, doch die Optimierung per Expression Templates wurde nie realisiert, und laut cppreference scheinen Implementierungen keinen besonderen Code über normale Container hinaus zu besitzen
  • std::vector<bool> ist klassisch analysiert in Howard Hinnants On vector<bool>; bitgepackte Speicherung an sich ist nützlich, aber die Benennung als std::vector-Spezialisierung macht sie zur Falle, weil generischer Code bei T = bool falsch reagieren kann
  • volatile wurde in C++20 per P1152R4 für zusammengesetzte Operationen sowie Parameter- und Rückgabepositionen zur Deprecation vorgesehen, in C++23 durch P2327R1 teilweise zurückgenommen und soll in C++26 mit P2866R0 weiter zurückgedreht werden

Standard-Container, die sich wegen ABI nicht korrigieren lassen

  • std::unordered_map verbietet durch die C++11-Spezifikation zu Buckets und Iterator-Stabilität Open Addressing faktisch; die Struktur von Google SwissTable wird mit einem Performance-Vorteil von etwa 3x gegenüber std::unordered_map angegeben
  • Folly F14, Boost unordered_flat_map und ankerl::unordered_dense sind ähnliche Alternativen; Rusts HashMap verwendet den hashbrown-SwissTable-Port als Standardbibliotheks-Default
  • std::map und std::set basieren als node-basierte red-black trees auf Heap-Allokationen pro Knoten und Pointer-Chasing bei jeder Traversierung; Abseils btree_map und Rusts BTreeMap umgehen dieselben Probleme mit einem B-Tree-Ansatz
  • C++23 fügte mit P0429R9 std::flat_map und std::flat_set hinzu, konnte aber die grundlegenden Designs von std::unordered_map, std::map und std::list selbst nicht ändern
  • Der multi-symbol order book benchmark vergleicht bei gleicher Workload, gleichem Seed und gleichem isolierten Core C++ mit std::unordered_map+std::map+std::list gegen Rust mit HashMap+BTreeMap+VecDeque
Implementierung P99 cycles
C++ naive (unordered_map + map + list) 302,653
C++ step 1 (flat_hash_map + map + deque) 9,951
C++ step 2 (flat_hash_map + btree_map + deque) 9,114
C++ step 3 (flat_hash_map + btree_map + vector) 4,268
Rust naive (HashMap + BTreeMap + VecDeque) 5,177
  • Allein der Wechsel von std::list zu std::vector bringt etwa 70x, der Wechsel von std::unordered_map zu flat_hash_map 3–5x, und der Wechsel von std::map zu btree_map 1,09x und damit einen Effekt innerhalb des Rauschens
  • Der Punkt des Vergleichs ist nicht, dass die Sprache Rust selbst 58x schneller sei als C++, sondern dass Rusts Standardbibliothek die richtigen Defaults gewählt hat, während C++ seine drei Defaults wegen ABI nicht korrigieren kann

Das Vasa-Problem und die Ansammlung von Features

  • Bjarne Stroustrups WG21-Dokument P0977R0 von 2018, „Remember the Vasa!“, nutzt den Untergang des schwedischen Kriegsschiffs Vasa von 1628 als Metapher und diagnostiziert, dass es im Ausschuss „etwa 150 Köche“ gebe und zu wenig darüber gesprochen werde, wie einzelne Features das Gesamtsystem beeinflussen
  • std::simd wird in std::simd Is a Solution to the Wrong Problem als typisches Beispiel desselben Musters behandelt; Matthias Kretz brachte das Feature von der Vc-Bibliothek über P0214, Parallelism TS 2 und P1928 bis in C++26
  • Als std::simd standardisiert wurde, existierten außerhalb des Standards bereits Google Highway, ISPC, EVE, xsimd und SIMDe, und auch die Auto-Vectorizer von GCC und Clang hatten sich verbessert, sodass laut Darstellung skalare Schleifen mit -O3 -march=native besser abschnitten als std::simd
  • std::simd kompiliert 10x langsamer als äquivalenter skalarer Code, ist langsamer als der Auto-Vectorizer, den es ersetzen sollte, und kann weder Vektoren mit skalierbarer Breite unter ARM SVE noch Runtime-Dispatch ausdrücken
  • libstdc++, libc++ und MSVC STL werden jeweils von kleinen Engineering-Teams im einstelligen Bereich gepflegt; jedes neue Standard-Feature vergrößert die Testmatrix, die Zahl der Conformance-Bugs, die Wechselwirkungen zwischen Features und die Einträge im Bugtracker für die nächste Wartungsperson
  • std::regex trägt seit 15 Jahren bekannte Probleme mit sich, std::deque hat ein Issue, das einen Redesign-Bedarf festhält, und C++20-Modules werden auch sechs Jahre nach der Standardisierung noch als nicht sauber funktionierend über alle drei Implementierungen hinweg beschrieben
  • Das praktische Betriebswissen zum modernen C++-Standard konzentriert sich dadurch auf eine kleine Zahl hauptberuflicher Experten, die wissen, aus welcher Zeit die fehlerhaften Ebenen stammen, welche Third-Party-Umgehungen es gibt, worin sich die drei Standardbibliotheks-Implementierungen unterscheiden und wo Theorie und Praxis auseinanderlaufen

Unterschied zu anderen Sprachen: nicht Fehler, sondern Aufbewahrungsquote

  • Python entfernte mit PEP 594 mehr als 20 Standardbibliotheksmodule, strich mit PEP 632 distutils in Python 3.12 und hält in PEP 387 ausdrücklich fest, dass der Deprecation-Zeitraum für riskante oder kaputte Features verkürzt werden darf
  • Java markierte die Applet API in Java 9 zur Deprecation, zur Entfernung in Java 17 und vollzog die tatsächliche Entfernung über JEP 504 über einen Pfad von acht Jahren; Nashorn wurde mit JEP 372 in Java 15 entfernt
  • Javas SecurityManager erhielt mit JEP 411 den Status „deprecated for removal“ und wurde mit JEP 486 dauerhaft deaktiviert; JEP 398 behandelt den Pfad zur Entfernung der Applet API
  • Rust bietet die Editions 2015, 2018, 2021 und 2024 als crateweises Opt-in in Cargo.toml; mem::uninitialized wurde durch MaybeUninit, std::error::Error::description durch source und das Makro try! durch den Operator ? ersetzt
  • C# akzeptierte beim Übergang von .NET Framework zu .NET Core einen großen Versionsbruch und ließ BinaryFormatter, AppDomains, Remoting, Code Access Security, WCF server und WebForms zurück
  • JavaScript entfernt wegen Web-Kompatibilität fast nie etwas, aber cancelable promises wurden in Stage 1 zurückgezogen und SIMD.js zugunsten von WebAssembly SIMD aufgegeben; Go belässt wegen des Go-1-Kompatibilitätsversprechens io/ioutil nur im Zustand „deprecated“
  • Der Unterschied bei C++ ist nicht, dass Fehler gemacht wurden, sondern die Aufbewahrungsquote: Dinge wie std::regex, std::unordered_map, std::vector<bool>, std::valarray und der const-correctness-Fehler von std::function lassen sich fast nie wirklich entfernen

Dauerhafte Bewahrung durch ABI-Stabilität

  • P1863R1 „ABI - Now or Never“ war die Linie, die für C++23 fragte, ob man einen ABI-Break in Kauf nehmen oder dauerhafte ABI-Stabilität wählen wolle; der Ausschuss entschied sich faktisch für Letzteres
  • Dadurch werden eine Korrektur von std::regex, der Wechsel von std::unordered_map zu Open Addressing und strukturelle Änderungen an std::list, std::map und std::deque schwierig
  • Die ABI der C++-Standardbibliothek wird vom dynamischen Linker erzwungen: Objekte, die mit einer libstdc++-Version kompiliert wurden, müssen sich mit Objekten aus einer anderen Version linken lassen, sodass Details wie das Layout von std::string oder die Zusammensetzung von std::regex_traits in ausgelieferten Binärdateien festgeschrieben sind
  • Konkretisiert wird diese Einschränkung in Dokumenten wie der libstdc++ ABI policy und der Itanium C++ ABI
  • Python-Nutzer wählen python==3.12, Rust-Nutzer die Edition in Cargo.toml, Java-Nutzer die JDK-Version und C#-Nutzer ein TFM wie net6.0 oder net8.0, aber C++ hat kein Cargo.toml für std::
  • -std=c++26 wählt aus, welche Header und Sprachregeln verwendet werden, liefert aber weder ein anderes std::string noch eine neu entworfene std::unordered_map
  • Deshalb trägt die C++-Standardbibliothek, die 2026 in Produktion ausgeliefert wird, weiterhin jene falschen Defaults mit sich, die der Ausschuss seit 1998 akzeptiert hat – nicht nur historisch, sondern durch Design und Durchsetzung
  • Moderne C++-Codebasen bei tier-one trading firms, Suchmaschinen und Browsern stützen sich daher stark auf nicht standardisierte Bibliotheken wie Boost, Abseil, Folly, EASTL, Chromium //base, selbst gebaute Container, Custom Allocators, CTRE, Outcome und Coroutine-Bibliotheken

2 Kommentare

 
dieafterwork 3 시간 전

Der Originaltext ist ziemlich umfangreich, und wenn man bis zum Ende liest, wirkt er stellenweise schon recht wie eine Huldigung an Rust.
Trotzdem habe ich auch viele Informationen mitgenommen, die ich vorher nicht kannte. Vielen Dank für den guten Artikel.

 
Lobste.rs-Kommentare
  • Wenn ich darüber nachdenke, ob es im Rust-Ökosystem ähnlichen Churn gab, fallen mir nur ein paar größere Fälle ein
    Während der Leakpocalypse kam man zu dem Schluss, dass man sich nicht darauf verlassen kann, dass Drop-Destruktoren immer ausgeführt werden, um Sicherheitsinvarianten zu wahren; tatsächliche API-Änderungen gab es kaum, im Wesentlichen die Entfernung von std::thread::scoped. Später kam ein Ersatz hinzu, der dasselbe sound ermöglicht
    std::mem::uninitialized wurde deprecated und gilt inzwischen als unsound. Die bestehenden Range-Typen sollen nach und nach durch fast identische neue Typen ersetzt werden, um relativ kleine API-Probleme zu beheben. std::error::Error::description wurde deprecated, weil die meisten Fehlertypen keinen String speichern wollen, und es gibt mit der Display-Implementierung einen direkten Ersatz
    Wenn man bedenkt, dass std seit 11 Jahren stabil ist, ist das ziemlich überraschend, und der Rest von std existiert weiterhin und funktioniert, und 98 % davon gelten immer noch als idiomatisches Rust. Dagegen wirkt die C++-Standardbibliothek in einer gefährlichen Lage: viel zu leicht am Abzug bei Feature-Erweiterungen und erstaunlich konservativ beim Deprecaten, ganz gleich unter welchen Umständen

    • Von Leakpocalypse hatte ich irgendwie überhaupt nichts mitbekommen: faultlore (2015)
    • Mir fällt auch das Problem ein, dass das Iterator-Trait seinen eigenen Inhalt ausleiht. Das ist ein chronisches Thema, das in Rust-Diskussionen immer wieder als „Warum kann ich das nicht benutzen und brauche einen Workaround?“ auftaucht
      Ebenso ist es ein lästiger Punkt, über den neue Engineers oft stolpern, dass f32 und f64 nicht Cmp implementieren und stattdessen die Methode f32::total_cmp haben, sodass man seufzend den Hintergrund erklären muss
      Auch der panic-Formatierungsapparat ist nicht besonders gut, und es gibt viele Blogposts darüber, dass der Standard-panic-Handler Formatierung verwendet, sich schwer abschalten lässt und dadurch die Binärgröße ziemlich aufbläht
  • Ich persönlich denke, dass veraltetes Design der Standardbibliothek C++ in Popularität und Benutzbarkeit stark ausbremst
    Viele Probleme, die oft der Sprache selbst zugeschrieben werden, müssten in Wahrheit eher gegen die Standardbibliothek gerichtet werden
    Zum Beispiel ist „C++ kompiliert langsam“ nicht ganz präzise. Nicht die Nutzung von C++-Features an sich ist inhärent langsam, sondern die Standardbibliothek macht es langsam: durch massives Header-Bloat und Abhängigkeiten sowie durch übermäßigen Template-Einsatz selbst für einfache Abstraktionen
    „C++ ist unsicher“ stimmt teilweise auch, aber das Design der Standardbibliothek verschlimmert das noch. Es gibt keinen Grund, warum sicherere Muster aus dem Rust-API-Design nicht auf neue Standardbibliotheken angewendet werden könnten. Natürlich ist einer der großen Vorteile von C++ die Abwärtskompatibilität, daher ist das ein sehr komplexes Problem

    • In einigen Fällen stimmt das. Man könnte vec[idx] so gestalten, dass es bei Zugriff außerhalb des Bereichs eine Exception wirft oder abbricht, statt Out-of-Bounds-Zugriff/Undefined Behavior zu verursachen. Aber wegen Sprachunterschieden gibt es auch viele Fälle, in denen es in C++ viel schwieriger ist, sichere APIs zu bauen
      Rust hat standardmäßig destruktive Moves, C++ dagegen nicht. Deshalb können Smart-Pointer-APIs kaum anders als unsafe oder zumindest überraschend und crash-lastig sein. Zum Beispiel indem das Programm bei Zugriff auf einen moved-from Smart Pointer abbricht
      Rust hat Lifetime-Annotationen, C++ nicht. Deshalb kann Rust bei der Gestaltung von Iterator-APIs Dinge wie Iterator-Invalidation verhindern, während das in C++ praktisch schwer machbar ist. Rust hat Pattern Matching, wodurch APIs wie Option ergonomisch einen „prüfen und direkt benutzen“-Ansatz anbieten können. C++ könnte zwar auch eine Version von std::option bereitstellen, bei der Zugriff auf einen leeren Wert nicht zu UB führt, aber sie wäre deutlich umständlicher zu benutzen als das heutige C++ oder Rust. Der ?-Operator in Rust hilft hier ebenfalls enorm
      Ich weiß, dass man C++ mit Overload-Sets wie bei std::variant etwas hinzufügen kann, das Pattern Matching ähnelt, aber ich halte das für deutlich schwerer zu verwenden und fehleranfälliger
    • Ich finde, für C gilt Ähnliches. Viele Probleme von C entstehen, weil die stdlib miserabel ist
      Schon eine moderne Bibliothek mit String- und Array-Bibliotheken, einigen generischen Containern und nativer Unterstützung für Allocators würde C viel ergonomischer und leichter nutzbar machen. Natürlich verschwinden manche Sprachmängel nicht einfach durch den Austausch der Bibliothek, aber man kommt damit trotzdem ziemlich weit
      Wenn man moderne C-Codebases anschaut, verwenden sie breitflächig Custom-Libraries für Allocators, Strings, Vektoren, Hash-Tabellen und Dateisystemoperationen, und wenn man Erfahrung mit C oder manueller Ressourcenverwaltung hat, ist es nicht allzu schwer, diesem Stil zu folgen
    • Bei uns in der Firma verwenden wir eine Implementierung von slice<T, N>, die „ein Pointer auf genau N Bytes“ oder „ein Pointer auf eine beliebige Anzahl Bytes“ ausdrücken kann
      Es gibt head(n), tail(n), slice(start, end) und den Index-Operator, und alles macht Grenzprüfungen
      Mit solchen Abstraktionen zu arbeiten, macht wirklich Spaß, aber um eine moderne und einigermaßen sichere Sprache zu bekommen, muss man im Grunde die Rust- und Zig-Standardbibliotheken nach C++ portieren. Trotzdem ist es am Ende den Aufwand wert
    • Verliert man nicht Performance, wenn man für einfache Abstraktionen weniger Templates verwendet?
  • Wenn man so einen Text schreibt, dann bitte schreibt ihn selbst. Vielleicht wurde die Liste selbst erstellt, aber sie in ein LLM zu kippen und das Ergebnis auf einer Webseite für Menschen auszuspielen, ist einfach zu respektlos. Wenn ich noch einmal einen Satz lese wie „arbeitende Engineers“ würden schon „am ersten Tag“ lernen, „Feature X“ zu vermeiden, drehe ich durch
    Das Peinliche ist, dass es hier eigentlich viel zu sagen gäbe, aber stattdessen wird gar nichts gesagt. Es muss einen Grund gegeben haben, diesen Text zu erstellen, also hätte ich gern diesen Grund gehört. Irgendetwas an C++ wird die Person verärgert haben, und irgendein Feature wird sie verwirrt haben. Diese Features sind nicht nur wegen objektiver Designfehler schlecht, sondern wegen ihrer Auswirkungen auf uns
    Ob jemand std::iterator benutzt hat und dafür in Slack auseinandergenommen wurde, oder ob jemand auf einen Cast verzichtet hat, weil reinterpret_cast 16 Zeichen lang ist und die Zeilenformatierung leicht verschlechtert hätte — so etwas wäre auf Lobsters viel besser gewesen. Wenn es solche Geschichten nicht gibt, dann sollte man sie nicht erzwingen und auch nicht eine GPU denselben Satz zehnmal per Matrixmultiplikation erzeugen lassen. Kommentiert einfach die Teile, zu denen ihr etwas sagen wollt, und schreibt den Rest als Tabelle und Bullet Points

    • Dieser Text liest sich für mich nicht so, als wäre er von einem LLM geschrieben
  • Ich nutze C++ seit 20 Jahren und auch heute noch, aber ich stimme diesem Artikel in vielen Punkten zu. Was sich bei Rust heutzutage wirklich gut anfühlt, ist weniger die Speichersicherheit als vielmehr die hervorragende Standardbibliothek und das Paket-Ökosystem
    Ein typisches Beispiel ist die Ranges-Bibliothek. Sie ist seit 6 Jahren standardisiert, und trotzdem haben die großen Standardbibliotheken sie bis heute nicht vollständig implementiert; und selbst wenn sie implementiert ist, gibt es nur eine Handvoll Kombinatoren. Das Rust-Gegenstück, die Iterator-Methoden, umfasst 76 Methoden, und mit einmal cargo add kommen über das itertools-Trait noch 130 weitere dazu
    Was ich außerdem wirklich vermisse, ist Pattern Matching. Damit ließen sich Union-Typen wie std::variant ergonomisch nutzbar machen. Ein entsprechender Vorschlag wird diskutiert, hat es aber selbst in C++26 noch nicht geschafft, und das ist schade. Stattdessen kommen Contracts und Executors hinein, obwohl ich ehrlich gesagt noch niemanden in meinem Umfeld gesehen habe, der danach verlangt hätte

    • Eines der Probleme von C++ ist, dass es keine offiziellen und dokumentierten Kriterien dafür gibt, welche Funktionen Sprachfeatures sein sollten und welche in die Standardbibliothek gehören
      Im Allgemeinen ist mein Maßstab folgender: Wenn eine Funktion einen wünschenswerten Anwendungsfall unterstützt und sich nicht mit der Standardbibliothek ausdrücken lässt, sollte sie in die Sprache aufgenommen werden. Wenn möglich, sollte man die gewünschte Funktion in minimale, voneinander unabhängige Bausteine zerlegen, die sich auch für andere Zwecke nutzen lassen
      Funktionen, die in fast jeder Codebasis verwendet werden, sollten in die Standardbibliothek. Wenn ein Typ häufig als Schnittstelle zwischen Bibliotheken verwendet wird, sollte er ebenfalls in die Standardbibliothek. Man will nicht, dass jede Bibliothek ihren eigenen Tuple-Typ oder String definiert. Bei C++ war Ersteres vor C++11 faktisch so, und Letzteres ist wegen des Desasters std::string im Grunde bis heute so. Das gilt auch für Interface-Typen, und C++ löst das heutzutage meist mit Concepts
      Alles Übrige sollte in wiederverwendbare, modulare Bibliotheken. Rust ist ziemlich gut darin, einen stabilen und gewissermaßen blessed Satz externer Bibliotheken bereitzustellen, daher ist der Druck viel geringer, nach dem Motto „Jedes in Rust geschriebene Spiel braucht diese Datenstruktur, also packen wir sie in die Standardbibliothek“ zu argumentieren. Wer Spiele entwickelt, kann einfach die benötigten Crates einbinden. C++ hat die Idee eines „guten empfohlenen Pakets für ein Problem, das viele, aber nicht die Mehrheit haben“ nie wirklich angenommen
  • Was mir Sorgen macht, ist, welche der derzeit hinzugefügten Dinge am Ende später wieder zurückgenommen werden. Contracts sind gerade erst in C++26 aufgenommen worden, und schon jetzt werden ernste Designfehler aufgezeigt
    Ich möchte „Komitee-Design“ nicht pauschal verurteilen. Ich denke, solche Gremien erfüllen einen wichtigen Zweck und haben eigene Stärken. Diese Stärke liegt aber nicht darin, völlig neue Features auf der grünen Wiese zu entwerfen
    WG21 und WG14 glänzen dann wirklich, wenn ein Designraum bereits einigermaßen erkundet ist und sie eine Funktion — möglichst mit mehreren existierenden Implementierungen — aufgreifen und zu einem Standardfeature machen, das von den meisten Nutzern und Implementierern akzeptiert werden kann. std::embed ist so ein Beispiel
    Umgekehrt läuft es oft wirklich schlecht, wenn man Dinge standardisiert, bevor überhaupt jemand sie vernünftig implementiert hat, etwa die im Artikel erwähnte GC-Erweiterung, std::memory_order_consume oder C++20-Module

    • C++ und Haskell wurden beide von Komitees entworfen, und doch sind die beiden Sprachen fast Gegensätze. Daran denke ich immer, wenn ich in Versuchung gerate zu glauben, „$X wurde von einem Komitee entworfen“ sage irgendetwas Wesentliches über $X aus
  • Früher war ich ziemlich schockiert, als mir klar wurde, dass C++ seine Standardbibliothek nicht versioniert. Ich hätte nicht gedacht, dass dieser Artikel genau diesen Punkt so treffend herausarbeitet.
    Interessant fand ich auch die Erwähnung, dass Go bei der Forward Compatibility ähnlich konservativ ist. Go scheint aber auch bei neu hinzukommenden Features ähnlich vorsichtig zu sein und hat dadurch wohl die meisten Probleme von C++ vermieden. Eine stabile ABI nicht zu haben, dürfte ebenfalls geholfen haben.
    Unter den populären Bibliotheken, die ich kenne, ist libcamera die einzige, die explizit eine C++-ABI exponiert, und das ist ziemlich lästig. Meiner Erfahrung nach exportieren auch C++-Bibliotheken ihre Symbole meist über eine C-ABI, was die Interoperabilität mit anderen Sprachen ebenfalls erleichtert. Vielleicht habe ich hier aber auch einen Trend verpasst.
    Und gibt es nicht auch gewisse quirks bei der ABI-Kompatibilität zwischen Clang und MSVC? Ich meine mich zu erinnern, dass Conan das Mischen von Compilern ausdrücklich discouragt oder verboten hat, daher frage ich mich, warum das C++-Komitee sich so sehr bemüht, ABI-Stabilität zu bewahren.

    • Das ist nicht ganz richtig. C++ versioniert seine Standardbibliothek nur nicht unabhängig von der Sprache.
      Hier gibt es zwei eng verwandte Dinge: die Spezifikation der Standardbibliothek und ihre Implementierung. Die Spezifikation gilt für eine vollständige Kombination aus Sprache und Bibliothek, und die Implementierung versucht normalerweise, mindestens eine oder mehrere Spezifikationsversionen zu unterstützen.
      Es gibt viele Bibliotheken, die C++-Interfaces exponieren, auch sehr große wie Qt.
      Das Problem ist, dass die abstrakte Maschine von C++ den Link-Prozess nicht definiert. Deshalb kann sie nicht definieren, wie dynamische Bibliotheken funktionieren. Das dynamische Linken von C++ auf UNIX-Systemen folgt dem C-Modell. Es tut so, als wäre es dynamisches Linken, und schiebt es den Loader-Problemen zu. Dadurch entstehen schreckliche Dinge wie copy relocation. Windows hat ein viel prinzipielleres Konzept davon, was eine Shared Library ist, aber deshalb können einige Idiome von C++-Bibliotheken unter UNIX auf Windows nicht funktionieren.
      Shared Libraries sind besonders problematisch bei Features wie C++-Templates. Damit man Templates mit benutzerdefinierten Typen instanziieren kann, muss die vollständige Definition im Header stehen, weil der Compiler die Grenzen von Compilation Units nicht überblicken kann. In Shared Libraries wird derselbe Code dann an mehreren Stellen instanziiert. Wenn Programm und Bibliothek dasselbe Template mit denselben Parametern instanziieren, haben beide eine Kopie, und Linker und Loader müssen dafür sorgen, dass im final geladenen Programm nur eine davon verwendet wird.
      Im Vergleich dazu sagt Swift ausdrücklich: „Shared Libraries existieren, und die Sprache stellt Konstrukte auf Sprachebene bereit, um sie darzustellen.“ Wenn man Generics über Shared-Library-Grenzen hinweg exponieren will, geht das, aber für alle externen Aufrufer werden sie auf eine Version mit dynamischem Dispatch heruntergebrochen. In C++ kann man das auch von Hand umsetzen. Man baut eine generische Version des Templates mit einem Wrapper zur Typlöschung und verwendet andere konkrete Instanziierungen explizit. Aber das ist schwierig und manuell. In Swift heißt es einfach: „So verhält es sich an einer Shared-Library-Grenze.“
      Dasselbe gilt für das Verbergen von Typen. C++ verwendet das pImpl-Pattern, um ein Public Interface zu bauen, das Verhalten über Bibliotheksgrenzen hinweg exponiert, die Implementierung aber verbirgt. Swift hat eine abstrakte Maschine, die weiß, wo Bibliotheksgrenzen sind, und sagt: „Die Größe eines Typs, der nicht ausdrücklich als ABI-stable gekennzeichnet ist, ist jenseits einer Shared-Library-Grenze keine Compile-Time-Konstante.“
      Es gibt noch eine weitere Form, in der der Standard die Realität verleugnet. Fast jede nicht triviale C++-Codebasis, an der ich gearbeitet habe, wurde mit -fno-rtti -fno-exceptions oder den entsprechenden Optionen von CL.EXE kompiliert. Der Standard erkennt das nicht einmal als Möglichkeit an. Die meisten Funktionen der Standardbibliothek erwarten für die Fehlerbehandlung weiterhin Exceptions, sodass bei einer Kompilierung mit -fno-exception einfach abort aufgerufen wird. Dadurch werden Standardbibliotheks-Komponenten, die dynamische Speicherallokation verwenden, für Embedded unbrauchbar. std::vector<T>::push_back kann das Programm zum Absturz bringen.
      Die Passage im Artikel, dass „das Komitee nicht nur schlechte Features nicht entfernen kann, sondern weiterhin neue Features hinzufügt, nach denen Praktiker gar nicht gefragt haben“, trifft zu 100 % darauf zu, wie Contracts entstanden sind. Verus zeigt, was ein gutes Contracts-System in einer Sprache ermöglichen kann, die auf eine ähnliche Umgebung wie C++ abzielt. P2900 Contracts ist eine Kombination miteinander kollidierender Anforderungen und verschlimmert jedes Problem, für das Contracts sonst passend sein könnten.
      Ich glaube auch nicht, dass die Schlussfolgerung stimmt, ein „C++ engineer“ werde viel besser bezahlt als ein „Engineer, der programmieren kann“. In der Praxis schreibt ohnehin niemand Code streng nach dem C++-Standard, sondern jeder arbeitet nach seiner bevorzugten internen subset-of-a-superset-Variante.
    • go vet hat hier ebenfalls Wert, weil es automatische Upgrades zur Verbesserung von APIs bereitstellt.
  • Seit letztem Jahr habe ich C++ fast komplett aufgegeben, zuerst zugunsten von Kotlin, dann von Swift. Im Unternehmen muss ich zwar noch C++ warten, aber neu geschriebener Code ist deutlich sauberer, prägnanter und sicherer. Ich nehme dafür Trade-offs bei Codegröße und vielleicht auch Performance in Kauf, aber es lohnt sich.

  • Ich hielt diesen Satz für falsch, weil ich mich daran erinnerte, dass sich die Semantik von Gos for-Schleifen auf eine Weise geändert hat, die Abwärtskompatibilität bricht: https://go.dev/blog/loopvar-preview
    Dann habe ich aber festgestellt, dass Go hier ähnlich wie Rust Editions vorgeht. Die Semantik ändert sich nur, wenn man Go-Version 1.22 oder höher deklariert. Vermutlich könnte man io/ioutil auf ähnliche Weise entfernen, aber offenbar wäre es nicht wert, dafür Code sogar über Editions-Grenzen hinweg zu brechen.

  • Wenn C++ all diese schlechten Ideen nicht tatsächlich ausprobiert und bewiesen hätte, dass es schlechte Ideen sind, gäbe es Rust in seiner heutigen Form vielleicht nicht. Big Thank You!

  • Ich hätte Interesse an einem Rust-artigen Ersatz für die C++-Standardbibliothek. Ich kenne rpp, das genau darauf abzielt: https://github.com/TheNumbat/rpp
    Gibt es noch andere Optionen? Ich meine nicht einfach andere Implementierungen der C++-Stdlib wie EASTL, sondern Bibliotheken, die sich stärker an Rust orientieren. Mir ist klar, dass manches wie std::initializer_list in die Syntax eingebaut ist, aber der Rest lässt sich komplett austauschen.