Die C++-Standardbibliothek nimmt sich seit 15 Jahren selbst zurück – und die Belege sind öffentlich
(hftuniversity.com)- 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 undstd::aligned_storage, also Elemente mit Deprecation- oder Removal-Papers; auchstd::functionbefindet sich in einem 15-jährigen Ersatzpfad, flankiert vonstd::move_only_function,std::copyable_functionundstd::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::dequeundstd::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::mapundstd::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_functionordnetstd::functionals „Legacy. Avoid in new code.“ ein std::functionwurde in C++11 aufgenommen, der neueste Ersatz-Wrapperstd::copyable_functionkommt 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::functionhat beiconst 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_functionin C++23 mit P0288R9,std::copyable_functionin C++26 mit P2548R6 undstd::function_refin C++26 mit P0792R14 in derselben Entwicklungslinie
Standard-Features, die offiziell zurückgenommen wurden
std::auto_ptrwurde 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>sowiestd::random_shufflestd::random_shufflewurde durchstd::shuffleersetzt, weil es vonstd::randund globalem Zustand abhing- Dynamische Ausnahmespezifikationen
throw(X, Y)wurden in C++11 zur Deprecation vorgesehen und in C++17 mit P0003R5 entfernt; das Aliasthrow()wurde in C++20 per P1152 entfernt std::iteratorwurde 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 definierenstd::aligned_storageundstd::aligned_unionwurden in C++11 eingeführt und in C++23 mit P1413R3 zur Deprecation vorgesehen; als Probleme geltentypename ::type-Boilerplate,reinterpret_cast, undefiniertes Verhalten beiLen == 0und fehlendes constexprstd::not1,std::not2,unary_negateundbinary_negatewurden in C++17 zur Deprecation vorgesehen, in C++20 entfernt und durchstd::not_fnaus P0005 ersetzt- Die C++11-Garbage-Collection-Schnittstellen rund um
std::declare_reachablewurden 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::regexkam 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::asyncblockiert 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 nachstd::formataus C++20 per P0645 sowiestd::printundstd::printlnaus C++23 per P2093 nicht zur Deprecation vorgesehenstd::listist eines der Beispiele, bei denen Bjarne Stroustrup in der GoingNative-Keynote 2012 zeigte, dass sogar Workloads mit Einfügungen in der Mitte vonstd::vectorgewonnen werden; auch der spätere Beitrag Are lists evil? kommt praktisch zu „ja“std::dequeist 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ärestd::valarraywurde 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 besitzenstd::vector<bool>ist klassisch analysiert in Howard Hinnants Onvector<bool>; bitgepackte Speicherung an sich ist nützlich, aber die Benennung alsstd::vector-Spezialisierung macht sie zur Falle, weil generischer Code beiT = boolfalsch reagieren kannvolatilewurde 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_mapverbietet 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überstd::unordered_mapangegeben- Folly F14, Boost
unordered_flat_mapundankerl::unordered_densesind ähnliche Alternativen; RustsHashMapverwendet den hashbrown-SwissTable-Port als Standardbibliotheks-Default std::mapundstd::setbasieren als node-basierte red-black trees auf Heap-Allokationen pro Knoten und Pointer-Chasing bei jeder Traversierung; Abseilsbtree_mapund RustsBTreeMapumgehen dieselben Probleme mit einem B-Tree-Ansatz- C++23 fügte mit P0429R9
std::flat_mapundstd::flat_sethinzu, konnte aber die grundlegenden Designs vonstd::unordered_map,std::mapundstd::listselbst 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::listgegen Rust mitHashMap+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::listzustd::vectorbringt etwa 70x, der Wechsel vonstd::unordered_mapzuflat_hash_map3–5x, und der Wechsel vonstd::mapzubtree_map1,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::simdwird 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::simdstandardisiert 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=nativebesser abschnitten alsstd::simd std::simdkompiliert 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::regexträgt seit 15 Jahren bekannte Probleme mit sich,std::dequehat 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
distutilsin 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::uninitializedwurde durchMaybeUninit,std::error::Error::descriptiondurchsourceund das Makrotry!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/ioutilnur 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::valarrayund der const-correctness-Fehler vonstd::functionlassen 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 vonstd::unordered_mapzu Open Addressing und strukturelle Änderungen anstd::list,std::mapundstd::dequeschwierig - 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::stringoder die Zusammensetzung vonstd::regex_traitsin 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 inCargo.toml, Java-Nutzer die JDK-Version und C#-Nutzer ein TFM wienet6.0odernet8.0, aber C++ hat keinCargo.tomlfürstd:: -std=c++26wählt aus, welche Header und Sprachregeln verwendet werden, liefert aber weder ein anderesstd::stringnoch eine neu entworfenestd::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
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 vonstd::thread::scoped. Später kam ein Ersatz hinzu, der dasselbe sound ermöglichtstd::mem::uninitializedwurde deprecated und gilt inzwischen als unsound. Die bestehendenRange-Typen sollen nach und nach durch fast identische neue Typen ersetzt werden, um relativ kleine API-Probleme zu beheben.std::error::Error::descriptionwurde deprecated, weil die meisten Fehlertypen keinen String speichern wollen, und es gibt mit derDisplay-Implementierung einen direkten ErsatzWenn man bedenkt, dass
stdseit 11 Jahren stabil ist, ist das ziemlich überraschend, und der Rest vonstdexistiert 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ändenIterator-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?“ auftauchtEbenso ist es ein lästiger Punkt, über den neue Engineers oft stolpern, dass
f32undf64nichtCmpimplementieren und stattdessen die Methodef32::total_cmphaben, sodass man seufzend den Hintergrund erklären mussAuch 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
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 bauenRust 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
Optionergonomisch einen „prüfen und direkt benutzen“-Ansatz anbieten können. C++ könnte zwar auch eine Version vonstd::optionbereitstellen, 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 enormIch weiß, dass man C++ mit Overload-Sets wie bei
std::variantetwas hinzufügen kann, das Pattern Matching ähnelt, aber ich halte das für deutlich schwerer zu verwenden und fehleranfälligerSchon 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
slice<T, N>, die „ein Pointer auf genau N Bytes“ oder „ein Pointer auf eine beliebige Anzahl Bytes“ ausdrücken kannEs gibt
head(n),tail(n),slice(start, end)und den Index-Operator, und alles macht GrenzprüfungenMit 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
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::iteratorbenutzt hat und dafür in Slack auseinandergenommen wurde, oder ob jemand auf einen Cast verzichtet hat, weilreinterpret_cast16 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 PointsIch 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 einmalcargo addkommen über dasitertools-Trait noch 130 weitere dazuWas ich außerdem wirklich vermisse, ist Pattern Matching. Damit ließen sich Union-Typen wie
std::variantergonomisch 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ätteIm 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::stringim Grunde bis heute so. Das gilt auch für Interface-Typen, und C++ löst das heutzutage meist mit ConceptsAlles Ü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::embedist so ein BeispielUmgekehrt 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_consumeoder C++20-ModuleFrü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.
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-exceptionsoder 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-exceptioneinfachabortaufgerufen wird. Dadurch werden Standardbibliotheks-Komponenten, die dynamische Speicherallokation verwenden, für Embedded unbrauchbar.std::vector<T>::push_backkann 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 vethat 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-previewDann 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/ioutilauf ä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_listin die Syntax eingebaut ist, aber der Rest lässt sich komplett austauschen.