1 points par GN⁺ 18 시간 전 | 2 commentaires | Partager sur WhatsApp
  • 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, ou std::aligned_storage, tous accompagnés d’articles de dépréciation ou de suppression ; std::function s’inscrit lui aussi dans un cycle de remplacement long de 15 ans, désormais entouré par std::move_only_function, std::copyable_function et std::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::deque et std::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::map et std::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_function classe std::function comme « Legacy. Avoid in new code. »
  • std::function est 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() de std::function pré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_function s’inscrit dans le flux de P0288R9 pour C++23, std::copyable_function dans P2548R6 pour C++26, et std::function_ref dans P0792R14 pour C++26

Les fonctionnalités standard officiellement annulées

  • std::auto_ptr a é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> et std::random_shuffle
  • std::random_shuffle a été remplacé par std::shuffle parce qu’il dépendait de std::rand et 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’alias throw() a été supprimé en C++20 par P1152
  • std::iterator a é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 typedefs
  • std::aligned_storage et std::aligned_union sont arrivés avec C++11 puis ont été dépréciés en C++23 par P1413R3, notamment à cause du boilerplate typename ::type, de reinterpret_cast, du comportement indéfini quand Len == 0 et de l’absence de constexpr
  • std::not1, std::not2, unary_negate et binary_negate ont été dépréciés en C++17 puis supprimés en C++20, remplacés par std::not_fn de P0005
  • L’interface de garbage collection de C++11 autour de std::declare_reachable a é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::regex est 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 PCRE2
  • std::async a 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 de std::format avec P0645 en C++20 puis de std::print et std::println avec P2093 en C++23, il n’est toujours pas déprécié
  • std::list figure parmi les cas où Bjarne Stroustrup a montré, dans sa keynote GoingNative 2012, que même sur des charges avec insertions au milieu, std::vector gagnait ; son billet suivant, Are lists evil?, répond à peu près « oui »
  • std::deque apparaî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 break
  • std::valarray a é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 ordinaires
  • std::vector<bool> a été analysé de façon emblématique par Howard Hinnant dans On vector<bool> ; le stockage bit-packed est utile en soi, mais le fait de l’avoir nommé comme une spécialisation de std::vector en fait un piège pour le code générique lorsque T = bool
  • volatile a é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_map est, 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_map et ankerl::unordered_dense suivent des directions similaires ; côté Rust, HashMap utilise par défaut dans la bibliothèque standard un portage de SwissTable via hashbrown
  • std::map et std::set reposent sur des red-black trees à base de nœuds, ce qui impose une allocation heap par nœud et du pointer chasing à chaque parcours ; Abseil btree_map et Rust BTreeMap évitent ces coûts grâce à une base B-tree
  • C++23 a bien ajouté std::flat_map et std::flat_set via P0429R9, mais sans pouvoir modifier la conception par défaut de std::unordered_map, std::map et std::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::list côté C++ à HashMap + BTreeMap + VecDeque cô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::vector apporte environ 70x, celui de std::unordered_map à flat_hash_map 3 à 5x, et celui de std::map à btree_map 1.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::simd est 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::simd dans 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=native pouvait faire mieux que std::simd
  • std::simd compile 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::regex traîne des problèmes connus depuis 15 ans, std::deque a 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é distutils en 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::uninitialized a été remplacé par MaybeUninit, std::error::Error::description par source, et la macro try! 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/ioutil sans 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::valarray ou le défaut de const-correctness de std::function sont 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 de std::unordered_map vers l’open addressing, ainsi que toute modification structurelle de std::list, std::map ou std::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::string ou la composition de std::regex_traits se 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 dans Cargo.toml, un utilisateur Java choisit une version de JDK, et un utilisateur C# un TFM comme net6.0 ou net8.0 ; mais il n’existe pas de Cargo.toml pour std:: en C++
  • -std=c++26 permet de choisir quels headers et quelles règles de langage utiliser, mais ne fournit ni un autre std::string, ni un std::unordered_map repensé
  • 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

 
dieafterwork 3 시간 전

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 Drop s’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 de std::thread::scoped
    Un remplaçant assurant la même chose de façon sound est apparu ensuite
    std::mem::uninitialized a été déprécié et est maintenant considéré comme unsound. Les types Range existants devraient être lentement remplacés par de nouveaux types presque identiques afin de corriger des problèmes d’API relativement mineurs. std::error::Error::description a é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 de Display
    C’est assez étonnant quand on pense que std est restée stable pendant 11 ans, et que le reste de std existe 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 soit

    • Je n’avais réussi à n’avoir absolument jamais entendu parler de Leakpocalypse : faultlore (2015)
    • Le problème du trait Iterator qui 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 f32 et f64 n’implémentent pas Cmp et proposent à la place la méthode f32::total_cmp est aussi un point pénible sur lequel les nouveaux ingénieurs butent souvent, et il faut alors soupirer puis expliquer le contexte
      Le 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

    • Dans certains cas, oui. On pourrait faire en sorte que 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 Option de fournir de façon ergonomique une approche « vérifier puis utiliser ». C++ pourrait aussi fournir une version de std::option où 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 ici
      Je 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 tromper
    • Je pense que c’est pareil en C. Beaucoup des problèmes du C viennent du fait que sa stdlib est médiocre
      Rien 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
    • Dans mon entreprise, on utilise une implémentation de 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 bornes
      Travailler 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 on choisit d’utiliser moins de templates pour des abstractions simples, est-ce qu’on ne perd pas en performances ?
  • 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’utiliser reinterpret_cast juste 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 à puces

    • Ce billet ne donne pas l’impression d’avoir été écrit par un LLM
  • J’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 simple cargo add ajoute encore 130 méthodes via le trait itertools
    Ce 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éclamer

    • L’un des problèmes de C++ est qu’il n’existe pas de critère officiel et documenté pour décider si une fonctionnalité doit relever du langage ou de la bibliothèque standard
      En 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::string est un désastre. Cela s’applique aussi aux types d’interface, et aujourd’hui C++ traite surtout cela via les concepts
      Le 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::embed en est un bon exemple
    En 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_consume ou les modules de C++20, cela a vraiment tendance à mal tourner

    • C++ et Haskell ont tous deux été conçus par comité, mais les deux langages sont presque à l’opposé l’un de l’autre. J’essaie de m’en souvenir chaque fois que j’ai envie de penser que « $X a été conçu par comité » implique forcément quelque chose à propos de $X
  • J’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

    • Ce n’est pas tout à fait exact. C++ ne versionne simplement pas la bibliothèque standard indépendamment du langage
      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 pImpl pour 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-exceptions ou 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 simplement abort. 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_back peut faire crasher le programme
      Le 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 vet a aussi de la valeur ici. Il fournit des mises à niveau automatiques pour améliorer les API
  • J’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 for en Go avait changé en cassant la compatibilité descendante : https://go.dev/blog/loopvar-preview
    Mais 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/ioutil de la même manière, mais cela ne semble probablement pas assez utile pour casser du code au-delà d’une frontière d’edition

  • Si 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_list sont intégrées à la syntaxe, mais tout le reste peut être changé