La bibliothèque standard C++ se rétracte d’elle-même depuis 15 ans, et les preuves sont publiques
(hftuniversity.com)- La bibliothèque standard C++ a, depuis C++11, répété le même schéma : soit déprécier officiellement des conceptions ratées, soit les laisser en place à côté de nouveaux remplaçants, ce qui oblige les développeurs à savoir à quelle époque appartient la « couche qu’il ne faut pas utiliser »
- La couche des retraits officiels comprend des éléments comme
std::auto_ptr, les spécifications d’exception dynamiques, l’interface de garbage collection de C++11, oustd::aligned_storage, tous accompagnés d’articles de dépréciation ou de suppression ;std::functions’inscrit lui aussi dans un cycle de remplacement long de 15 ans, désormais entouré parstd::move_only_function,std::copyable_functionetstd::function_ref - La couche des contournements officieux regroupe
std::regex, trop lent,std::async, dont le destructeur peut bloquer et créer des pièges de deadlock, ainsi que<iostream>,std::list,std::dequeetstd::vector<bool>, toujours présents dans la norme mais souvent évités en code de production - Le problème des conteneurs par défaut est particulièrement visible avec
std::unordered_map,std::mapetstd::list: sur le même benchmark de charge, le P99 d’une implémentation C++ naïve atteint 302,653 cycles, contre 5,177 cycles pour l’implémentation Rust naïve, soit un écart de 58x - Le choix de la stabilité ABI constitue la différence centrale : alors que d’autres langages réduisent leurs erreurs via des suppressions, des éditions ou des transitions de version majeure, C++ conserve de fait ses mauvais choix par défaut presque pour toujours dans
std::
Point de départ : le verdict « legacy » sur std::function
- Le tableau de référence rapide de Sandor Dargo sur
std::copyable_functionclassestd::functioncomme « Legacy. Avoid in new code. » std::functionest arrivé avec C++11, tandis que son remplaçant le plus récent,std::copyable_function, entre avec C++26 ; la recommandation autour de la nouvelle fonctionnalité est moins « utilisez-la si vous avez besoin d’un callable copiable » que « n’utilisez plus l’ancienne »- Le
const operator()destd::functionprésente un défaut de const-correctness, puisqu’il peut appeler un callable non const, et ce défaut ne peut plus être corrigé sans casser l’ABI - En réponse à ce défaut,
std::move_only_functions’inscrit dans le flux de P0288R9 pour C++23,std::copyable_functiondans P2548R6 pour C++26, etstd::function_refdans P0792R14 pour C++26
Les fonctionnalités standard officiellement annulées
std::auto_ptra été déprécié en C++11 parce que sa sémantique de copie-déplacement cassait le code générique et les conteneurs standard, puis supprimé en C++17 via N4190 ; le même article a aussi supprimé les adaptateurs C++98 de<functional>etstd::random_shufflestd::random_shufflea été remplacé parstd::shuffleparce qu’il dépendait destd::randet d’un état global- Les spécifications d’exception dynamiques
throw(X, Y)ont été dépréciées en C++11 puis supprimées en C++17 par P0003R5, et l’aliasthrow()a été supprimé en C++20 par P1152 std::iteratora été déprécié en C++17 via P0174R2, et sa suppression en C++26 est poussée par P3365R1 ; l’alternative consiste à définir directement les cinq typedefsstd::aligned_storageetstd::aligned_unionsont arrivés avec C++11 puis ont été dépréciés en C++23 par P1413R3, notamment à cause du boilerplatetypename ::type, dereinterpret_cast, du comportement indéfini quandLen == 0et de l’absence deconstexprstd::not1,std::not2,unary_negateetbinary_negateont été dépréciés en C++17 puis supprimés en C++20, remplacés parstd::not_fnde P0005- L’interface de garbage collection de C++11 autour de
std::declare_reachablea été supprimée en C++23 par P2186R2, les principales implémentations n’ayant jamais fourni de véritable garbage collector - Les TS Concepts, Modules, Coroutines, Reflection, Executors et Networking ont eux aussi été repensés, remplacés ou retardés avant intégration ; Reflection évolue désormais vers P2996, et Executors vers le modèle sender/receiver de P2300
Des fonctionnalités toujours dans la norme mais évitées sur le terrain
std::regexest arrivé avec C++11, mais P1844R1 conserve la trace d’un constat du comité : ses performances sont « très mauvaises par rapport aux autres solutions disponibles » ; la filière de remplacement passe par CTRE et P1433R0, et hors standard par Boost.Regex, RE2 et PCRE2std::asynca un destructeur de future retournée qui bloque jusqu’à la fin de la tâche asynchrone, et N3679 documente les pièges de deadlock qui en découlent<iostream>est lent, lié aux locales, non thread-safe pour le formatting et célèbre pour ses messages d’erreur détestables ; pourtant, même après l’arrivée destd::formatavec P0645 en C++20 puis destd::printetstd::printlnavec P2093 en C++23, il n’est toujours pas dépréciéstd::listfigure parmi les cas où Bjarne Stroustrup a montré, dans sa keynote GoingNative 2012, que même sur des charges avec insertions au milieu,std::vectorgagnait ; son billet suivant, Are lists evil?, répond à peu près « oui »std::dequeapparaît dans l’issue publique microsoft/STL#147 du Microsoft STL, où il est indiqué que la taille de bloc imposée par la norme est trop petite et qu’une refonte majeure des performances sera nécessaire lors du prochain ABI breakstd::valarraya été introduit en 1998 comme conteneur numérique, mais les optimisations par expression templates ne se sont jamais concrétisées et, d’après cppreference, les implémentations ne semblent pas lui accorder de traitement spécial au-delà de celui des conteneurs ordinairesstd::vector<bool>a été analysé de façon emblématique par Howard Hinnant dans Onvector<bool>; le stockage bit-packed est utile en soi, mais le fait de l’avoir nommé comme une spécialisation destd::vectoren fait un piège pour le code générique lorsqueT = boolvolatilea été déprécié dans les opérations composées ainsi qu’en position paramètre/retour par P1152R4 en C++20, partiellement rétabli en C++23 par P2327R1, puis doit encore être révisé en C++26 via P2866R0
Des conteneurs par défaut impossibles à corriger à cause de l’ABI
std::unordered_mapest, du fait de sa spécification C++11 sur les buckets et la stabilité des itérateurs, pratiquement incompatible avec l’open addressing ; la structure SwissTable de Google est présentée comme offrant environ 3x de performances face àstd::unordered_map- Folly F14, Boost
unordered_flat_mapetankerl::unordered_densesuivent des directions similaires ; côté Rust,HashMaputilise par défaut dans la bibliothèque standard un portage de SwissTable via hashbrown std::mapetstd::setreposent sur des red-black trees à base de nœuds, ce qui impose une allocation heap par nœud et du pointer chasing à chaque parcours ; Abseilbtree_mapet RustBTreeMapévitent ces coûts grâce à une base B-tree- C++23 a bien ajouté
std::flat_mapetstd::flat_setvia P0429R9, mais sans pouvoir modifier la conception par défaut destd::unordered_map,std::mapetstd::list - Le benchmark de carnet d’ordres multi-symboles compare, à charge identique, seed identique et cœur isolé identique,
std::unordered_map+std::map+std::listcôté C++ àHashMap+BTreeMap+VecDequecôté Rust
| Implémentation | P99 cycles |
|---|---|
C++ naïf (unordered_map + map + list) |
302,653 |
C++ étape 1 (flat_hash_map + map + deque) |
9,951 |
C++ étape 2 (flat_hash_map + btree_map + deque) |
9,114 |
C++ étape 3 (flat_hash_map + btree_map + vector) |
4,268 |
Rust naïf (HashMap + BTreeMap + VecDeque) |
5,177 |
- Le simple passage de
std::listàstd::vectorapporte environ 70x, celui destd::unordered_mapàflat_hash_map3 à 5x, et celui destd::mapàbtree_map1.09x, soit un effet dans le bruit de mesure - Le point de la comparaison n’est pas de dire que le langage Rust est 58x plus rapide que C++, mais que la bibliothèque standard Rust a choisi de meilleurs défauts, tandis que la bibliothèque standard C++ ne peut plus corriger ses trois défauts structurels à cause de l’ABI
Le problème du Vasa et l’accumulation de fonctionnalités
- Dans le document WG21 P0977R0 de 2018, « Remember the Vasa! », Bjarne Stroustrup prend pour métaphore le naufrage du navire de guerre suédois Vasa en 1628 et estime que le comité a « environ 150 cuisiniers » qui traitent insuffisamment l’impact global des fonctionnalités sur le système dans son ensemble
std::simdest présenté dans std::simd Is a Solution to the Wrong Problem comme un exemple typique du même schéma : une fonctionnalité initiée par Matthias Kretz à partir de la bibliothèque Vc, passée par P0214, Parallelism TS 2 et P1928 avant d’entrer dans C++26- Au moment de l’entrée de
std::simddans la norme, l’écosystème hors standard proposait déjà Google Highway, ISPC, EVE, xsimd et SIMDe, tandis que les auto-vectorizers de GCC et Clang s’étaient améliorés au point qu’une boucle scalaire compilée avec-O3 -march=nativepouvait faire mieux questd::simd std::simdcompile 10x plus lentement qu’un code scalaire équivalent, se montre plus lent que l’auto-vectorizer qu’il prétend remplacer, et ne sait pas représenter les vecteurs à largeur scalable d’ARM SVE ni le runtime dispatch- Les trois implémentations libstdc++, libc++ et MSVC STL sont chacune maintenues par de petites équipes d’ingénieurs ; chaque nouvelle fonctionnalité standard ajoute de nouvelles lignes à la matrice de tests, de nouveaux bugs de conformité, de nouvelles interactions entre fonctionnalités et de nouveaux tickets de bug que le prochain mainteneur héritera
std::regextraîne des problèmes connus depuis 15 ans,std::dequea un ticket qui appelle une refonte, et les modules C++20 sont décrits comme n’étant toujours pas propres sur l’ensemble des trois implémentations, six ans après leur standardisation- Dans les faits, la connaissance opérationnelle du standard C++ moderne se concentre chez une petite minorité d’experts à plein temps, capables d’identifier les époques des mauvaises couches, les contournements tiers, les différences entre les trois implémentations de bibliothèques standard et l’écart entre théorie et pratique
Ce qui distingue les autres langages : non pas l’erreur, mais le taux de conservation
- Python a supprimé plus de 20 modules de sa bibliothèque standard avec la PEP 594, a retiré
distutilsen Python 3.12 via PEP 632, et PEP 387 explicite la possibilité de raccourcir le cycle de dépréciation pour les fonctionnalités dangereuses ou cassées - Java a traité l’Applet API selon une trajectoire de huit ans : dépréciation en Java 9, suppression prévue en Java 17, puis retrait effectif avec JEP 504 ; Nashorn a été supprimé en Java 15 via JEP 372
- Java SecurityManager a été placé en dépréciation pour suppression via JEP 411 puis définitivement désactivé par JEP 486, tandis que JEP 398 traite de la trajectoire de suppression de l’Applet API
- Rust propose des éditions 2015, 2018, 2021 et 2024 sélectionnables par crate dans
Cargo.toml;mem::uninitializeda été remplacé parMaybeUninit,std::error::Error::descriptionparsource, et la macrotry!par l’opérateur? - C# a accepté une transition de version majeure de .NET Framework vers .NET Core, en abandonnant BinaryFormatter, AppDomains, Remoting, Code Access Security, le serveur WCF et WebForms
- JavaScript, en raison de la contrainte de compatibilité web, supprime très peu, mais les cancelable promises ont été retirées au Stage 1, SIMD.js a été abandonné au profit de WebAssembly SIMD, et Go, du fait de sa promesse de compatibilité Go 1, s’est contenté de déprécier
io/ioutilsans le supprimer - Ce qui distingue C++, ce n’est pas le fait d’avoir commis des erreurs, mais son taux de conservation : des éléments comme
std::regex,std::unordered_map,std::vector<bool>,std::valarrayou le défaut de const-correctness destd::functionsont presque impossibles à éliminer
La stabilité ABI comme mécanisme de conservation permanente
- P1863R1 « ABI - Now or Never » posait la question de savoir s’il fallait accepter un ABI break pour C++23 ou choisir une stabilité ABI permanente ; le comité a de fait choisi cette seconde voie
- Ce choix rend très difficile toute correction profonde de
std::regex, toute transition destd::unordered_mapvers l’open addressing, ainsi que toute modification structurelle destd::list,std::mapoustd::deque - L’ABI de la bibliothèque standard C++ est imposée par l’éditeur de liens dynamique : des objets compilés avec une version de libstdc++ doivent pouvoir être liés avec des objets compilés avec une autre, si bien que des détails comme le layout de
std::stringou la composition destd::regex_traitsse figent dans les binaires distribués - Cette contrainte est explicitée dans des documents comme la politique ABI de libstdc++ et l’Itanium C++ ABI
- Un utilisateur Python choisit
python==3.12, un utilisateur Rust choisit une édition dansCargo.toml, un utilisateur Java choisit une version de JDK, et un utilisateur C# un TFM commenet6.0ounet8.0; mais il n’existe pas deCargo.tomlpourstd::en C++ -std=c++26permet de choisir quels headers et quelles règles de langage utiliser, mais ne fournit ni un autrestd::string, ni unstd::unordered_maprepensé- En conséquence, la bibliothèque standard C++ expédiée en production en 2026 continue, par conception et par contrainte, à embarquer les mauvais choix par défaut acceptés par le comité depuis 1998
- Les codebases C++ modernes de sociétés de trading tier-one, de moteurs de recherche ou de navigateurs reposent donc largement sur des bibliothèques non standard comme Boost, Abseil, Folly, EASTL, Chromium
//base, des conteneurs maison, des allocators personnalisés, CTRE, Outcome et diverses bibliothèques de coroutines
2 commentaires
Le texte original est très copieux, et en le lisant jusqu’au bout, on ressent quand même assez fortement une tonalité de fervent partisan de Rust.
Cela dit, j’y ai aussi appris beaucoup de choses que j’ignorais. Merci pour ce bon article.
Avis sur Lobste.rs
En repensant à la question de savoir s’il y a eu un churn comparable dans l’écosystème Rust, il semble qu’il n’y ait eu que quelques gros cas
Lors de Leakpocalypse, on en est arrivé à la conclusion qu’on ne pouvait pas compter sur le fait que le destructeur
Drops’exécute toujours pour préserver les invariants de sûreté, et il n’y a pratiquement pas eu de changements d’API réels, à part la suppression destd::thread::scopedUn remplaçant assurant la même chose de façon sound est apparu ensuite
std::mem::uninitializeda été déprécié et est maintenant considéré comme unsound. Les typesRangeexistants devraient être lentement remplacés par de nouveaux types presque identiques afin de corriger des problèmes d’API relativement mineurs.std::error::Error::descriptiona été déprécié parce que la plupart des types d’erreur n’ont pas envie de stocker une chaîne, et il existe un remplaçant direct avec l’implémentation deDisplayC’est assez étonnant quand on pense que
stdest restée stable pendant 11 ans, et que le reste destdexiste toujours, fonctionne toujours, et que 98 % est encore considéré comme du Rust idiomatique. En revanche, la bibliothèque standard C++ semble être dans une position dangereuse, beaucoup trop prompte à ajouter des fonctionnalités, et étonnamment conservatrice dès qu’il s’agit de déprécier quoi que ce soitIteratorqui emprunte son propre contenu me revient aussi à l’esprit. C’est un problème chronique qui revient sans cesse dans les discussions Rust sous la forme « pourquoi est-ce qu’on ne peut pas utiliser ça, et pourquoi faut-il un contournement ? »De la même manière, le fait que
f32etf64n’implémentent pasCmpet proposent à la place la méthodef32::total_cmpest aussi un point pénible sur lequel les nouveaux ingénieurs butent souvent, et il faut alors soupirer puis expliquer le contexteLe système de formatage des panic n’est pas terrible non plus, et on voit beaucoup de billets de blog expliquant que le panic handler par défaut utilise le formatage, qu’il est difficile à désactiver, et qu’il prend une part non négligeable de la taille du binaire
Personnellement, je pense que la conception vieillissante de la bibliothèque standard nuit fortement à la popularité et à l’utilisabilité de C++
Beaucoup de problèmes qu’on attribue au langage lui-même devraient en réalité être imputés à la bibliothèque standard
Par exemple, dire que « C++ compile lentement » n’est pas exact. L’utilisation des fonctionnalités de C++ n’est pas intrinsèquement lente ; ce qui ralentit, c’est le gonflement massif des headers et des dépendances, ainsi que la bibliothèque standard qui abuse des templates même pour des abstractions simples
Dire que « C++ n’est pas sûr » est aussi vrai en partie, mais la conception de la bibliothèque standard aggrave le problème. Il n’y a aucune raison de ne pas appliquer à une nouvelle bibliothèque standard les patterns plus sûrs utilisés dans la conception d’API Rust. Bien sûr, l’un des grands atouts de C++ est la rétrocompatibilité, donc c’est un problème extrêmement complexe
vec[idx]lance une exception ou fasse un abort au lieu d’un accès hors limites / comportement indéfini. Mais à cause des différences du langage, il y a aussi beaucoup de cas où il est bien plus difficile de construire des API sûres en C++Rust a par défaut des déplacements destructifs, mais pas C++. Donc les API de smart pointers ne peuvent qu’être unsafe, ou au minimum surprenantes et propices aux crashs. Par exemple, en faisant abort du programme lors d’un accès à un smart pointer après déplacement
Rust a des annotations de durée de vie, C++ n’en a pas. Donc Rust peut empêcher des choses comme l’invalidation d’itérateur dans la conception de ses API d’itération, alors qu’en C++ c’est en pratique très difficile. Rust a aussi le pattern matching, ce qui permet à des API comme
Optionde fournir de façon ergonomique une approche « vérifier puis utiliser ». C++ pourrait aussi fournir une version destd::optionoù l’accès à une valeur vide ne produit pas d’UB, mais ce serait bien plus pénible à utiliser que le C++ actuel ou Rust. L’opérateur?de Rust aide aussi énormément iciJe sais qu’on peut greffer à C++ quelque chose qui ressemble au pattern matching avec des ensembles d’overloads comme
std::variant, mais à mon avis c’est bien plus difficile à utiliser et plus facile de s’y tromperRien qu’avec une bibliothèque moderne dotée de bibliothèques de chaînes et de tableaux, de quelques conteneurs génériques, et d’un support natif des allocators, on pourrait rendre le C bien plus ergonomique et plus facile à utiliser. Bien sûr, certains défauts du langage ne disparaîtront pas juste en remplaçant la bibliothèque, mais on peut tout de même aller assez loin
Quand on regarde des bases de code C modernes, elles utilisent largement des bibliothèques maison pour les allocators, les chaînes, les vecteurs, les tables de hachage et les opérations sur le système de fichiers, et si on a déjà de l’expérience en C ou en gestion manuelle des ressources, ce n’est pas difficile à reproduire
slice<T, N>capable de représenter un « pointeur vers exactement N octets » ou un « pointeur vers un nombre arbitraire d’octets »Il y a
head(n),tail(n),slice(start, end)et l’opérateur d’indexation, et tout fait des vérifications de bornesTravailler avec ce genre d’abstractions est vraiment agréable, mais pour obtenir un langage moderne et raisonnablement sûr, il faut en pratique porter vers C++ les bibliothèques standard de Rust et Zig. Malgré tout, cela finit quand même par valoir l’effort fourni
Si quelqu’un va écrire un billet comme ça, j’aimerais vraiment qu’il l’écrive lui-même. Même si la liste a peut-être été faite à la main, la donner à un LLM puis afficher le résultat sur une page web pour que des humains le lisent, c’est d’une impolitesse extrême. Si je dois encore voir une phrase disant qu’un « ingénieur en poste » apprend à éviter la « fonctionnalité X » dès son « premier jour », je vais devenir fou
Ce qui est gênant, c’est qu’il y aurait vraiment énormément à dire ici, et pourtant au final ça ne dit rien. S’il y avait une raison à la création de ce billet, j’aimerais qu’on dise cette raison. Il a dû y avoir un aspect de C++ qui a mis l’auteur en colère, et une fonctionnalité qui l’a déconcerté. Si ces fonctionnalités sont mauvaises, ce n’est pas seulement à cause d’un échec de conception objectif, mais aussi à cause de leurs effets sur nous
Est-ce qu’il ou elle s’est déjà fait reprendre sur Slack pour avoir utilisé
std::iterator? Est-ce qu’il ou elle a déjà évité d’utiliserreinterpret_castjuste parce que ses 16 lettres risquaient de dégrader un peu le formatage des lignes ? Si ce genre d’histoires arrivait sur Lobsters, ce serait bien mieux. Et s’il n’y a pas ce genre d’histoire, alors il ne faut pas en inventer, ni laisser un GPU produire dix fois la même phrase via de la multiplication de matrices. Il suffit d’annoter les points qui méritent un commentaire, et d’écrire le reste sous forme de tableau et de listes à pucesJ’utilise C++ depuis 20 ans et je l’utilise encore, mais je suis largement d’accord avec cet article. Ce qui est vraiment bien avec Rust aujourd’hui, plus encore que la sûreté mémoire, c’est l’excellente bibliothèque standard et l’écosystème de packages
Un exemple représentatif est la bibliothèque ranges. Cela fait 6 ans qu’elle a été standardisée, et pourtant les principales bibliothèques standard ne l’ont toujours pas complètement implémentée ; et même lorsqu’elles le sont, il n’y a que quelques combinateurs. L’équivalent en Rust, les méthodes de
Iterator, en compte 76, et un simplecargo addajoute encore 130 méthodes via le traititertoolsCe qui me manque aussi vraiment, c’est le pattern matching. Cela permettrait de rendre ergonomiques des types union comme
std::variant. Une proposition est en discussion, mais ce n’est toujours pas dans C++26, et c’est regrettable. En revanche, contracts et executors vont y entrer, alors que franchement je n’ai jamais vu qui que ce soit autour de moi les réclamerEn général, voici le critère que j’applique. Si une fonctionnalité prend en charge un cas d’usage souhaitable et ne peut pas être exprimée dans la bibliothèque standard, alors elle doit entrer dans le langage. Si possible, il faut décomposer la fonctionnalité voulue en éléments minimaux et indépendants qui peuvent aussi servir à d’autres fins
Les fonctionnalités utilisées dans presque toutes les bases de code devraient entrer dans la bibliothèque standard. Si un type est couramment utilisé comme interface entre bibliothèques, il devrait entrer dans la bibliothèque standard. On ne veut pas que chaque bibliothèque définisse son propre type tuple ou sa propre chaîne de caractères. En C++, pour le premier cas, c’était de fait la situation jusqu’à C++11, et pour le second, c’est encore le cas parce que
std::stringest un désastre. Cela s’applique aussi aux types d’interface, et aujourd’hui C++ traite surtout cela via les conceptsLe reste devrait aller dans des bibliothèques modulaires réutilisables. Rust est assez bon pour disposer d’un ensemble stable et béni de bibliothèques externes, donc la pression du type « tous les jeux écrits en Rust ont besoin de cette structure de données, mettons-la dans la bibliothèque standard » est bien plus faible. Les développeurs de jeux n’ont qu’à importer les crates nécessaires. C++ n’a jamais vraiment intégré l’idée de « bons packages à recommander pour des problèmes que beaucoup, mais pas la majorité, rencontrent »
Ce qui m’inquiète, c’est de savoir lesquels des ajouts en cours finiront par être retirés plus tard. Contracts vient à peine d’entrer dans C++26 que de graves défauts de conception sont déjà signalés
Je n’ai pas envie de condamner en bloc la « conception par comité ». Je pense que ce genre d’organisations remplit un rôle important et possède des forces propres. Mais cette force ne réside pas dans la conception de fonctionnalités entièrement nouvelles sur une page blanche
L’endroit où WG21 et WG14 excellent vraiment, c’est lorsqu’ils prennent des fonctionnalités dont l’espace de conception a déjà été en partie exploré, avec si possible plusieurs implémentations existantes, pour en faire des fonctionnalités standard acceptables pour la plupart des utilisateurs et des implémenteurs.
std::embeden est un bon exempleEn revanche, quand on standardise avant même que quelqu’un ait correctement réussi à implémenter la chose, comme pour l’extension GC mentionnée dans l’article,
std::memory_order_consumeou les modules de C++20, cela a vraiment tendance à mal tournerJ’ai été assez choqué quand j’ai réalisé il y a quelque temps que C++ ne versionne pas sa bibliothèque standard. Je ne m’attendais pas à ce que cet article pointe exactement ce point
Il est aussi intéressant qu’il mentionne que Go est d’une prudence similaire sur la compatibilité ascendante. Mais Go est aussi assez conservateur sur les fonctionnalités ajoutées, ce qui semble lui avoir permis d’éviter la plupart des problèmes de C++. L’absence d’un ABI stable a probablement aussi aidé
Parmi les bibliothèques populaires que je connais, la seule qui expose explicitement un ABI C++ est libcamera, et c’est plutôt pénible. D’après mon expérience, les bibliothèques C++ exportent en général leurs symboles via un ABI C, ce qui facilite aussi l’interopérabilité avec d’autres langages. Il est possible que quelque chose m’échappe
Et il n’y a pas aussi des quirks dans la compatibilité ABI entre Clang et MSVC ? Je me souviens que Conan déconseillait explicitement, voire interdisait, de mélanger les compilateurs, donc je me demande pourquoi le comité C++ s’efforce autant de préserver la stabilité de l’ABI
Il y a ici deux choses étroitement liées : la spécification de la bibliothèque standard et son implémentation. La spécification porte sur la combinaison complète langage+bibliothèque, et l’implémentation essaie généralement de prendre en charge au moins une ou plusieurs versions de cette spécification
Il existe beaucoup de bibliothèques qui exposent des interfaces C++, y compris de très grosses comme Qt
Le problème, c’est que la machine abstraite de C++ ne définit pas le processus d’édition de liens. Elle ne peut donc pas définir le fonctionnement des bibliothèques dynamiques. Le linkage dynamique C++ sur les systèmes UNIX suit le modèle du C. Cela fait semblant d’être du linkage dynamique puis rejette la responsabilité sur des problèmes de loader. C’est ce qui produit des horreurs comme la copy relocation. Windows a une conception bien plus rigoureuse de ce qu’est une bibliothèque partagée, mais du coup certains idiomes des bibliothèques C++ sur UNIX ne peuvent pas fonctionner sur Windows
Les bibliothèques partagées posent de gros problèmes avec des fonctionnalités comme les templates C++. Si vous voulez pouvoir instancier un template avec un type utilisateur, la définition complète doit être dans le header, car le compilateur ne peut pas voir au-delà des frontières d’une compilation unit. Dans une bibliothèque partagée, le même code est instancié à plusieurs endroits. Si le programme et la bibliothèque instancient le même template avec les mêmes paramètres, ils ont tous deux leur propre copie, et le linker puis le loader doivent faire en sorte qu’une seule soit utilisée dans le programme final chargé
En comparaison, Swift dit explicitement : « les bibliothèques partagées existent, et le langage expose des constructions de niveau langage pour les représenter ». Si vous voulez exposer des génériques au-delà d’une frontière de bibliothèque partagée, c’est possible, mais pour tous les appelants externes cela est abaissé vers une version à répartition dynamique. On peut aussi l’implémenter à la main en C++. Il suffit de créer une version générique du template avec un wrapper d’effacement de type, puis d’écrire explicitement d’autres instanciations concrètes. Mais c’est difficile et manuel. En Swift, c’est simplement : « voilà ce qui se passe à la frontière d’une bibliothèque partagée »
C’est pareil pour la dissimulation des types. En C++, on utilise le pattern
pImplpour créer une interface publique qui expose le comportement au-delà des frontières de bibliothèque tout en cachant l’implémentation. Swift a une machine abstraite qui sait où se trouvent les frontières de bibliothèque, et dit : « la taille d’un type qui n’est pas explicitement déclaré ABI-stable n’est pas une constante de compilation au-delà de la frontière d’une bibliothèque partagée »C’est aussi une autre manière dont le standard nie la réalité. Presque toutes les bases de code C++ non triviales sur lesquelles j’ai travaillé étaient compilées avec
-fno-rtti -fno-exceptionsou les options équivalentes de CL.EXE. Le standard ne reconnaît pas cela comme une possibilité. La plupart des fonctions de la bibliothèque standard continuent pourtant de supposer les exceptions pour le signalement des erreurs, donc si on compile avec-fno-exception, elles appellent simplementabort. Cela rend inutilisables en embarqué les éléments de la bibliothèque standard qui font de l’allocation mémoire dynamique.std::vector<T>::push_backpeut faire crasher le programmeLe passage de l’article disant que « le comité est incapable de supprimer les mauvaises fonctionnalités et continue en plus d’ajouter de nouvelles fonctionnalités que les ingénieurs de terrain n’ont pas demandées » correspond à 100 % à la manière dont les contracts ont été introduits. Verus montre ce qu’un bon système de contracts peut rendre possible dans un langage destiné à des environnements similaires à ceux de C++. Les contracts P2900 sont une combinaison d’exigences contradictoires, si bien qu’ils aggravent tous les problèmes pour lesquels les contracts pourraient autrement être pertinents
Je ne pense pas que la conclusion selon laquelle un « ingénieur C++ » serait payé bien plus qu’un « ingénieur capable de programmer » soit vraie. En pratique, personne n’écrit du code en suivant le standard C++ à la lettre ; chacun écrit selon son propre subset-of-a-superset interne favori
go veta aussi de la valeur ici. Il fournit des mises à niveau automatiques pour améliorer les APIJ’ai quasiment abandonné C++ depuis l’an dernier : je suis d’abord passé à Kotlin, puis à Swift. Je dois encore maintenir du C++ au travail, mais le nouveau code qu’on écrit est bien plus propre, concis et sûr. Il y a un tradeoff sur la taille du code et peut-être sur les performances, mais ça en vaut la peine
Je pensais que cette phrase était fausse, parce que je me souvenais que la sémantique des boucles
foren Go avait changé en cassant la compatibilité descendante : https://go.dev/blog/loopvar-previewMais j’ai découvert que Go utilise ici une approche similaire aux editions de Rust. Il faut déclarer une version Go 1.22 ou supérieure pour que la sémantique change. On pourrait sans doute supprimer
io/ioutilde la même manière, mais cela ne semble probablement pas assez utile pour casser du code au-delà d’une frontière d’editionSi C++ n’avait pas réellement essayé toutes ces mauvaises idées et prouvé qu’il s’agissait de mauvaises idées, Rust n’existerait peut-être pas sous sa forme actuelle. Big Thank You!
Je suis intéressé par un remplaçant de la bibliothèque standard façon Rust pour C++. Je connais rpp, qui vise cet objectif : https://github.com/TheNumbat/rpp
Y a-t-il d’autres options ? Je ne parle pas d’autres implémentations de la stdlib C++ comme EASTL, mais de bibliothèques qui suivent Rust de plus près. Je sais que certaines choses comme
std::initializer_listsont intégrées à la syntaxe, mais tout le reste peut être changé