J'aurais aimé t'aimer, C++

Un retour d'expérience ... contrasté

Bonne année 2025 à vous !

Comme vous le savez peut-être, je passe mes journées à faire de la chimie quantique. En pratique, il s’agit de lancer des calculs via des programmes bien particuliers, voir parfois de créer lesdits programmes. Que font ces programmes ? Basiquement, de l’algèbre linéaire sur des matrices (multiplications, résolutions de système d’équations, etc) de tailles relativement importantes. Comment on fait ça ? En essayant d’utiliser un maximum de puissance de calcul. On peut pour cela utiliser du multi-threading (plutôt du multi-coeurs, en fait), demander à plusieurs machines de travailler sur le même problème, voire utiliser un (ou plusieurs !) GPU(s).

De manière logique, là ou le mult-threading est de plus en plus courant dans les langages de programmation (et même pour les plus vieux, il y a moyen de faire quelque chose facilement avec OpenMP), le fait de synchroniser plusieurs machines en même temps ou d’utiliser des GPUs est moins répendu. Et ce qui l’est encore moins, c’est des bibliothèques d’algèbres linéaires qui permettent de faire cela (parce qu’en vrai, c’est pas facile à faire).

Bref, me voilà avec pour bonne résolution pour cette année 2025, celle de réécrire from scratch un programme en C++ afin d’utiliser une telle bibliothèque. Et … Je pense que ça mérite que je partage mon expérience.

Le contenu du billet, qui est un billet d’opinion, est un peu provocateur. N’en prenez pas ombrage : chaque langage possède ces qualités et ces défauts. En plus, malgré tout ce que je peux dire ici, je vais apprendre de mes erreurs et vraiment le coder en C++, ce programme ;)

Pourquoi ?

TL;DR: Vu que je fais du calcul intensif, j’ai pas beaucoup d’autres choix, et j’aime la POO.

Quand je vous ai dis plus haut, qu’il n’existait pas beaucoup de bibliothèques qui permettait de faire de l’algèbre linéaire, je ne vous ai malheureusement pas menti. Oui, il y a Eigen, mais elle ne fait pas du multi-noeuds.

Historiquement, tout commence avec une collection de routines écrites en Fortran 77 distribuées entre dévellopeurs, qui finira par s’appeler BLAS. Vu qu’il s’agit de routines relativement modulables et utiles dans le petit monde du calcul intensif, BLAS est devenu un standard de facto. On lui a ensuite accolé un petit copain, LAPACK (qui contient des routines plus évoluées), mais qui a également été victime de son succès. Ce qui signifie qu’un très grand nombre de bibliothèques d’algèbre linéaire "modernes" sont soit: a) des wrappers plus ou moins fancy autour de BLAS/LAPACK (c’est le cas de numpy en Python, dont on ne dira jamais assez combien elle est géniale), ou b) des réimplémentations pour des usages spécifiques, mais tout en conservant l’API (par exemple cuBLAS, qui permet de faire de telles opérations sur des GPUs NVIDIA).

Pour faire communiquer plusieurs machines en même temps, on utilise MPI, un protocole qui étant les fonctionnalités des sockets UNIX. La fusion de celui-ci avec LAPACK donne scaLAPACK, une bibliothèque qui, si elle est correctement utilisée, permet de réaliser un calcul en parallèle sur plusieurs milliers de machines en simultanés. Comme un super-calculateur n’est qu’un agrégat de plusieurs milliers de machines,1 ça tombe bien pour moi ;)

Vu que le nombre de bibliothèques et d’applications de calculs intensifs qui ne sont pas basées sur le couple BLAS/LAPACK est ridiculement faible, je me permets donc d’affirmer que même en 2024, le Fortran 77 est toujours utilisé et que votre vie en est impactée. Deux exemples : la météo (de la bonne mécanique des fluides qui tache bien) et l’IA (qui fait plein d’algèbre linéaire en sous-main). Et ce n’est que les deux premiers exemples qui me viennent en tête.

Le pire ? Bah c’est vieillot (et les fonctions prennent des tonnes d’arguments parce que les structures n’existaient pas en Fortran à l’époque) mais ça marche bien, en fait. La preuve ? Les applications sus-mentionnées.

Le défaut ? Bah à moins d’utiliser un wrapper (dont la qualité varie en fonction du langage), un code de calcul intensif, ça s’écrit en donc Fortran, en C, ou en C++ (le Fortran s’interfaçant assez bien avec les deux dernier, même si tout ce passe par référence, en Fortran, donc par pointeur dans les deux autres).2

En particulier, ça ne s’écrit pas en Python (à mon grand regret), ou le coup de l’interprétation reste prohibitif.3 Ça ne s’écrit pas non plus en Rust (alors que ça devrait), vu que les garanties du Rust sont plutôt inutiles quand le wrapper pointe vers quelque chose d’écrit dans un langage (le Fortran, donc) qui ne partage pas lesdites garanties. Et bizarrement, autant pour ré-implémenter le noyau Linux en Rust, il y a du monde, autant pour l’algèbre linéaire, y’a plus personne4 :pirate: Et malheureusement, ça ne s’écrit pas en Julia non plus, parce que le wrapper pour scaLAPACK y est très basique.

Problème ? J’aime bien la POO,5 et je trouve qu’une matrice, c’est quand même un très bel objet (au sens du "O" de POO). Donc autant je suis d’accord avec @Spacefox quand il nous dit dans son très bon billet que quand on a un marteau, tout ressemble à un clou, autant j’ai l’impression d’être dans un cas ou la POO s’applique bien (une matrice, c’est avant tout un grand tableau de nombres à gérer, donc le planquer dans un objet, c’est quand même confortable). Autrement dit, quand j’ai codé pour la première fois le fameux projet qui m’a ammené à écrire ce billet, je l’ai écrit en C et … J’ai fait de la POO en C. Rien de mal à ça, mais gérer la mémoire, c’est pas fun, en fait. Et je suis pas venu ici pour souffrir.6

Donc quand j’ai découvert qu’il existait finalement une vraie ré-implémentation de scaLAPACK en C++ qui apporte un coup de jeune au domaine (pas un bête wrapper), bah j’ai sauté sur l’occasion. Et c’était le début de mes ennuis.

Mon état d’esprit.

Mais avant de râler, voici une liste "défauts" (ou pas) que je ne vais pas traiter:

  1. L’absence de gestionnaire de packages: bien que la présence d’un gestionnaire de package est un confort certains pour installer des dépendances, la gestion desdites dépendances peut vite être un cauchemar. L’exemple pathologique, c’est npm (pour éviter de m’énerver, je vous renverrai vers un autre billet de @SpaceFox, auquel je n’ai pas grand chose de plus à ajouter). Par ailleurs, j’utilise Meson, un CMake en moins énervé et cryptique, qui permet dans une certaine mesure d’installer des dépendances.
  2. "La POO, c’est le mal". Plus spécifiquement, deux critiques inhérentes à son implémentation que j’ai pu voir à plusieurs endroits utilisées contre C++. En résumé: a) l’héritage n’est généralement pas une zero cost abstraction parce qu’elle oblige a maintenir une liste de fonctions à appeler en fonction des cas quand ce cas ne peut pas être déterminé à la compilation, et b) le fait que la gestion des exceptions introduits à la compilation des morceaux de code pas ouf pour les performances. Bien que ce soit des critiques pertinentes, elles me semblent liées à la POO dans C++ donc … Si je choisis la POO dans C++, je choisis de faire avec. De toute façon, je sais d’avance que mon bottleneck au niveau performances ne se situera pas là.
  3. Il n’y a pas de garbadge collector en C++: ça serait confortable, mais c’est pas indispensable. En tout cas, pas à mon niveau (par contre, j’imagine pas sur des énormes applications).

De manière générale, dans ce billet d’humeur, je souhaite adresser des choix de design spécifiques à C++ que je trouve critiquables.


  1. C’est un peu plus compliqué que ça, ne le dite pas à mon system engineer, mais vous avez l’idée ;)
  2. … Hum, j’oubliais: "Beuuurk, des pointeurs nus".
  3. Je comprends pas pourquoi une bonne partie de l’IA s’écrit en Python, à vrai dire.
  4. En vrai, Rust ou pas, réimplémenter correctement (sca)LAPACK sans tuer ces performances, c’est compliqué. C’est aussi ce qui explique que personne peu de monde s’y est frotté ;)
  5. Et globalement, je code en Python quand j’ai pas besoin de performances, donc j’ai facilement les réflexes de la POO.
  6. En vrai, avec un peu de rigueur, ça se fait très bien, c’est juste … Looooooooooong et chiaaaaaaaaaaaant!

C++ = C with classes (pun intended)

TL;DR: trop de rétro-compatibilité, les bonnes fonctionnalités sont inutilement compliquées, donc trop facile d’écrire du mauvais code.

Ce titre se réfère, ici à trois choses:

  1. Le fait qu’il fut un temps ou le C++ était réellement transpilé en C ;
  2. Instant nostalgie: le fait que comme un certain nombre de personnes de ma génération, j’ai appris le C++ avec le tuto de matéo21 sur feu le Site du Zéro. Ce tuto, dans sa première version, ne faisait pas grand chose pour te dissuader que le C++, c’était juste du C avec des classes (ça a ensuite été corrigé quand le tuto C et C++ ont été séparés, mais le mal était fait me concernant) ; et
  3. Ce dont je veux parler dans cette section, le fait que le C++ ne fait pas grand-chose pour te forcer à écrire du C++ et qu’il te laisse facilement écrire du mauvais C with classes. Tout tient à la volonté du développeur et aux commentaires du type "ton code est bien, mais enfin, tu n’utilises pas cette fonctionnalité de C++20 qui n’est même pas disponible dans ton compilateur, c’est donc du mauvais C++".1

Plus sérieusement, j’ai l’impression, mais je me trompe peut-être, que ce dernier point est dû à deux choses : a) C++ est maladivement rétro-compatible (même avec C, donc, jusqu’à un certain point), et b) C++ est devenu ultra expressif.

Rétro-compatibilité, quand tu nous tiens

Parlons tout d’abord du premier point.

Par exemple, la STL contient toujours un subset de la librairie standard C, qui pour ce que j’en ai vu, contient un joli namespace std et quelques surcharges, et c’est tout. Pas de C++ moderne, donc ? Et non : pour prendre un exemple qui me concerne directement, la norme C++ n’a seulement pensé à définir template<typename T> T sqrt(T nb) (en mieux, évidement) que depuis C++26 (source).2

Et pour du SIMD sur les opérations mathématiques ? Pareil, c’est seulement pour 2026 (je sais, il y a std::for_each, j’y viens). Mais bon, je peux pas leur en vouloir, les compilateurs n’utilisent toujours pas AVX par défaut ;)

Pour les nostalgiques du C (coucou @Taurre), ne vous inquiétez pas, FILE* est toujours là, prêt à être utilisé. Je le sais parce que tempfile() en renvoi toujours un et que personne chez C++ c’est dit que ça serait bien d’en créer une version moderne. StackOverflow ne propose pas franchement mieux (la réponse la plus votée propose quand même d’utiliser des file descriptors, quand t’en revient à appeler l’API UNIX, tu sens que t’as raté un truc, surtout quand d’autres langages, il y a au moins un wrapper).

Pire encore, dans un monde ou tu es caillassé en place publique pour utiliser des pointeurs nus (les fameux), new et malloc() sont toujours là. Des fois que tu aimerais te tirer des balles dans le pied (mais pas de kink shaming). Vous ne voulez pas de pointeurs nus ? Baaaah … ne permettez pas d’en faire par défaut (et, aller, prévoyez une espèce #pragma unsafe). Sincèrement, je préfère me faire engueuler par mon compilateur que par des devs sur un forum ;)

Bref, je trouve qu’il est beaucoup trop facile d’écrire du "mauvais C++", ce qui est étonnant dans un langage qui est à ce point obsédé par la safety au sens large du terme (à la Rust, mais sans s’en donner les moyens).

Je vais évidement éviter d’utiliser new. Ceci dit, l’alternative n’est pas tendre avec le dévellopeur. En fait, je trouve qu’il n’est pas simple d’écrire du bon C++, et c’est mon second point.

Et expressif, avec ça ?

Dire que C++ est verbeux par rapport à d’autres langages me semble raisonnable. On me répondra que cette verbosité se justifie par son expressivité, qui permet d’être très précis sur ce qu’on souhaite en tant que développeur. Et je suis d’accord. Par contre, j’ai l’impression que ça vient avec une autre règle, qui est que "le comportement par défaut n’est probablement pas le bon" (qui vient du fait que le comportement par défaut est rétro-compatible, donc probablement incorrect en C++ moderne). Et une troisième qui est basiquement "plus c’est long, plus c’est bon". Et là, à un moment, c’est comme le chocolat, ça devient écœurant.

Comparons les deux lignes suivantes:

int* A_new = new int; *A_new = 3;
auto A_unique = std::make_unique<int>(3);

Les deux lignes font la même chose, et la seconde est beaucoup plus explicite. Par contre, c’est déjà même plus long, et encore, j’ai utilisé auto. Et c’est long partout ou on ne peut pas deviner std::unique_ptr<int> à partir du conctexte : dans les membres d’une classe, dans les définitions des constructeurs et des fonctions fonctions, et même parfois à l’intérieur : auto n’as qu’une utilité restreinte (et c’est normal, d’ailleurs, je ne remet pas ça en question).

Toujours aussi long, le casting: (int) 3.4 devient static_cast<int>(3.4): plus d’expressivité, certes, mais plus long. Et quand tu commences à les enchainer, ça commence à déborder de ton écran: static_cast<double>(n) / static_cast<double>(N) * 100 (paye tes pourcentages) par ci, static_cast<int>(size) parce que ta bibliothèque prend des entiers mais que les containers te renvoient du size_t par là, etc, etc. Rendez-moi double(n) !

Et trop, ça devient trop : il y a aussi toute la classe des algorithmes, qui sous couvert d’exhaustivité prennent toujours des itérateurs, et donc 2 arguments par containers. Pourquoi ne pas avoir fait une surcharge pour pouvoir écrire std::sort(x) plutôt que std::sort(x.begin(), x.end()) ?3 Aaaaah, mais non, j’ai pas compris : on va plutôt les mettres dans une AUUUUTRE bibliothèque, std::ranges (celle qui est pas dans clang, là), SAAANS déprécier la première. D’ailleurs quand on cherche sur le sujet, on peut constater la détresse des développeurs de clang et de MSVC qui ne parviennent pas à implémenter ladite bibliothèque. Je vous ai dit que le C++, ça se méritais ?

Passons vite fait sur les #include: trop aussi. Des choses aussi utiles que vector, move ou make_unique/make_shared sont dans 3 includes différents. C’était un problème avec C, pourquoi chercher à le reproduire ? À quand un #include <stl>?4 Même boost ne pousse pas le vice aussi loin, c’est dire !

Et finalement, le pire pour moi au niveau expressivité, c’est la sémantique de mouvement. J’ai beau en comprendre l’intérêt, 4 constructeurs,5 sérieusement ? Des std::move partout, des std::swap, allez quoi ? Pourquoi je dois autaaaaant écrire pour faire les choses bien ? Dans une norme ou on a quand défini T&& x (ça commence à faire beaucoup de &), pourquoi il n’y a pas un sucre syntaxique pour std::move, par exemple ? Si le perfect forwarding c’est si bien, pourquoi est ce que j’ai l’impression de devoir le mériter ?

Je me permets une digression. Est-ce qu’une partie du problème ne viendrait pas que le C++ a gardé le choix du C de tout passer par valeur ? Du peu que j’ai pratiqué le Java (qui est aussi furieusement verbeux expressif), j’ai l’impression que la sémantique de mouvement était une non-question. Ou je me suis trompé ?

Et donc …

Est ce que les concepteurs de la norme sont payés plus si les fichiers sont plus longs ?

Je finirai sur un exemple qui résume toute ma frustration récente:

Typiquement, j’ai découvert au détour d’un commentaire que push_back() dans un std::vector, c’est dépassé; il faut utiliser emplace_back. Parce que sémantique de mouvement. Pourquoi ne pas avoir changé le comportement de push_back plutôt que de créer une nouvelle fonction ? Bonne question (et, comme d’habitude, la nouvelle fonction est plus longue à écrire). Pourquoi ne pas avoir déprécié push_back ? Bonne question.

Est ce que clang-tidy reporte un warning ? Peut-être. Pourquoi je dois utiliser clang-tidy pour appendre ça ? Je sais pas.

Moi, il y a quelques jours.

… Tout ça me laisse avec la très sincère impression qu’il faut écrire des kilomètres de code pour écrire du bon C++. Les cas nominaux ne sont déjà pas simples à écrire, mais le bon C++, ça se mérite.

Soit, mais la conséquence, c’est qu’il est très facile de générer de la dette technique en faisant des cochonneries par simplicité (genre jouer avec container.data() alors qu’il faudrait pas, ou à écrire des for(auto x : container) à la place de ces fameux std::ranges).6 Pourquoi, si ce n’est que pour des raisons de rétro-compatibilité, des choses correctes ne seraient pas simples à écrire ? (genre, make_shared ou make_unique par défaut, par exemple)

Pour parler de ce que je connais, autant on a pu critiquer le choix de Python de casser la rétro-compatibilité entre Python 2 et 3, autant ils ont ensuite appris de leurs erreurs et déprécient désormais de manière plus ou moins raisonnable fonctionnalités et libraires. Et d’ailleurs, en Python le fait de recevoir des warnings de dépréciation dépend de la librairie, pas de la bonne volonté du compilateur, ou pire, de celle du dévellopeur d’activer les bons flags. Encore une fois, je préfère me faire engeuler par mon compilateur qui de toute façon ne me donne pas le choix ou m’oblige à activer des flags indiquant clairement que je fais quelque chose de mal (un truc bieeeen long qui te fait sentir bien mal, genre -funsafe-very-unsafe-allow-new-but-scream-like-hell-when-see-one-because-its-unsafe), qu’un dévellopeur.

Je pense sincèrement que le C++ gagnerai à sérieusement déprécier (d’après ce post, ça a déjà été le cas, mais c’est mineur) puis a remettre du sucre syntaxique. Ou faire une Kotlin (donc pas un autre langage, mais une version du C++ ou les bonnes choses seraient le défaut, et les mauvaises difficiles à faire).

Bref, enfin avoir un C++ qui ne serait pas un C with classes and stuffs that are not mandatory anyway.


  1. Vécu avec clang-tidy qui propose des std::ranges quand on le pousse un peu trop, vu sur StackOverflow dans d’autres contextes.
  2. J’écris ce billet en 2025, rappelons-le.
  3. J’espère qu’à ce prix là, c’est vectorisé, d’ailleurs !
  4. Je parie que c’est à cause de l’usage des templates. J’y viens ;)
  5. Je sais qu’il ne faut pas les implémenter à chaque fois
  6. Et, me concernant, une certaine frustration de faire "pas assez bien".

Et puis viennent les templates

TL;DR: "Vous n’avez aucun pouvoir ici, Gandalf le gris." (Saroumane, dans La communauté de l’anneau [le film])

Aller : on est en 2025, le support de C++20 avance, les IDEs font de l’auto-complétions et se permettent même des suggestions de style (jusqu’ici, la majorité de mes std::move m’ont été suggéré, peut être que j’ai pas tout à fait compris la sémantique de mouvement, en fait). clang-tidy … Existe (j’y viens) et en cherchant bien, on finit par tomber sur des ressources pour apprendre le C++ vraiment moderne (malheureusement, le moderne dépendant de quand a été écrit la ressource, actuellement c’est C++20, mais beaucoup de résultats sont toujours à C++11 voire à C++0x). Donc bon, à condition de se souvenir dans quel include est quoi, il y a moyen de commencer à s’amuser. D’ailleurs, merci à @informaticienzero et @mehdidou99 pour leur tuto :)

Sauf que mon projet est un peu particulier, parce que j’aimerai bien pouvoir facilement passer de la double précision à la simple précision (GPUs, tout ça). Et donc là, sans crier gare, je me prends d’envie de faire de la programmation générique (aka mettre des template <typename output_type> où ça va bien). Et c’est là où je commence à courir après un poulet pour faire un sacrifice rituel au dieu de la santé mentale sincèrement à remettre mes choix en question. Comprenez: c’est à ce moment-là que j’ai réellement considéré d’écrire ce billet.

<exagération> Les templates réussissent là ou les autres ont échoué, en cassant deux choses qui fonctionnait très bien par ailleurs : la compilation, et l’héritage. Rien que ça. </exagération>

La compilation

La compilation, d’abord. On apprend très vite, en C (et en C++, parce que rétro-compatibilité) à séparer la définition (dans un header) et l’implémentation. Même si c’est pas forcément naturel (ça a moins de sens en Python, par exemple, même si ça se fait), on est récompensé par le fait que les implémentations sont généralement compilées une fois, et assemblées à la fin. Autrement dit, quand on a compilé une première fois son projet, le re-compiler après une modification est généralement assez rapide (sauf si c’est dans un header, mais vu qu’il s’agit de définitions, c’est plus rare).

Sauf que voilà, en C++ (et dans d’autres langages, genre Java), une template se résout à la compilation. La conséquence de ça, c’est que désormais, on ne peut plus séparer définition et implémentation, et tout (!) doit aller dans le header.1 Encore une fois, ça serait tout à fait normal dans d’autres langages, donc pourquoi pas.

Ou est mon problème dans tout ça ? On va faire une petite observation. Voici le temps de compilation du projet original, en C :

image.png
Dans cet exemple précis, j’utilise gcc. C’est de toute façon à peu près la même avec clang.

Vous aurez reconnu l’interface de GitHub Actions. Je ne triche pas : c’est bien le temps de compilation à froid, à partir des sources fraichement téléchargées. Il y a bien entendu une déviation standard, mais elle est de quelques secondes. Et maintenant, voici le temps de compilation, dans les mêmes conditions, du projet C++, où j’ai peut-être implémenté quelque chose comme 10% des fonctionnalités:

image.png
g++, cette fois. clang++ fait pareil, si jamais.

4 (quatre!) fois plus. Je vais me faire déchirer dans les commentaires, mais sincèrement, je triche pas : les dépendances sont à peu près les mêmes (les options de compilation aussi, autant que faire ce peux) et j’ai pas encore eu besoin de rajouter Boost, par exemple (mais vu les chiffres, je vais y réfléchir à deux fois). Et c’est pas juste à froid : bien entendu, certains fichiers .cpp sont toujours compilés, ce qui permet de gagner du temps, mais à chaaaaaaaaaque changement dans une template, le compilateur repasse sur touuuuuuut le projet ou presque, et même avec un PC pas dégeu du tout, je sens bien la différence.

On me répondra que c’est le prix à payer pour avoir un code safe et optimisé, ce qui n’est pas forcément le cas de mon code C. Ok … Mais quatre fois plus long ?

Le nécessaire xkcd de circonstance.

Et gare à l’overdose: ce faisant, le compilateur réaffiche les mêeeeemes warnings (parce que oui, j’ai activé des flags pour essayer d’améliorer mon code) sur les dépendances sur lesquelles je n’ai paaaaaaaaas la main, d’autant que dès qu’il s’agit de templates, gcc ce sens obligé d’être très verbeux explicite lui aussi. Par exemple, pour un paramètre non-utilisé dans une dépendance, 35 (!) lignes de warnings:

… Que voici pour la beauté du geste. C’est pas très intéréssant, par contre.
In file included from /opt/slate/include/blas/device_blas.hh:6,
                 from /opt/slate/include/blas.hh:74:
/opt/slate/include/blas/device.hh: In instantiation of ‘void blas::device_memcpy(T*, const T*, int64_t, Queue&) [with T = double; int64_t = long int]’:
/opt/slate/include/slate/Tile.hh:966:42:   required from ‘void slate::Tile<scalar_t>::copyData(slate::Tile<scalar_t>*, blas::Queue&, bool) const [with scalar_t = double]’
  966 |             blas::device_memcpy<scalar_t>(
      |             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
  967 |                 dst_tile->data_, data_, size(), queue );
      |                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/opt/slate/include/slate/BaseMatrix.hh:2497:27:   required from ‘void slate::BaseMatrix<scalar_t>::tileCopyDataLayout(slate::Tile<src_scalar_t>*, slate::Tile<src_scalar_t>*, blas::Layout, bool) [with scalar_t = double]’
 2497 |         src_tile->copyData( dst_tile, *queue, async );
      |         ~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~
/opt/slate/include/slate/BaseMatrix.hh:2704:9:   required from ‘void slate::BaseMatrix<scalar_t>::tileGet(int64_t, int64_t, int, slate::LayoutConvert, bool, bool, bool) [with scalar_t = double; int64_t = long int]’
 2704 |         tileCopyDataLayout( src_tile, dst_tile, target_layout, async );
      |         ^~~~~~~~~~~~~~~~~~
/opt/slate/include/slate/BaseMatrix.hh:2868:12:   required from ‘void slate::BaseMatrix<scalar_t>::tileGetForWriting(int64_t, int64_t, int, slate::LayoutConvert) [with scalar_t = double; int64_t = long int]’
 2868 |     tileGet(i, j, device, layout, true, false, false);
      |     ~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/opt/slate/include/slate/BaseMatrix.hh:396:26:   required from ‘void slate::BaseMatrix<scalar_t>::tileGetForWriting(int64_t, int64_t, slate::LayoutConvert) [with scalar_t = double; int64_t = long int]’
  396 |         tileGetForWriting( i, j, HostNum, layout );
      |         ~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~
../include/pstdlite/slate_utils.hpp:278:34:   required from ‘void pstdlite::slateutils::blow(slate::HermitianMatrix<scalar_t>&, slate::Matrix<scalar_t>&, bool, int) [with scalar_t = double]’
  278 |             dst.tileGetForWriting(i, j, slate::LayoutConvert::None);
      |             ~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
../tests/tests_system.cpp:50:19:   required from here
   50 |   slateutils::blow(S, work);
      |   ~~~~~~~~~~~~~~~~^~~~~~~~~
/opt/slate/include/blas/device.hh:580:14: warning: unused parameter ‘dst’ [-Wunused-parameter]
  580 |     T*       dst,
      |     ~~~~~~~~~^~~

Dans un mode idéal, ce fameux paramètre inutilisé faisant partie de l’implémentation d’une fonction, il aurait dû arrêter de m’embêter à partir du moment ou j’ai eu fini de compiler ladite dépendance. Mais non, il reste là pour me hanter continuellement.2

Ça donne envie d’activer les flags pour améliorer son code, hein ? :p

Et si j’ai le malheur de faire une erreur, je suis bon pour scroller frénétiquement, parce qu’elle est enfuie au milieu de … tout ça. Encore heureux que gcc la met en rouge !

Bon, soyons honnêtes 30 secondes : j’ai un peu exagéré, parce que clang est plus sobre3. N’empêche, je ne compte plus le nombre de device.hh:580:14: warning: unused parameter 'queue' [-Wunused-parameter] qui sont dans mon log de compilation, ce qui montre bien que les compilateurs repassent encore et encore sur les même headers, ad nauseam.

L’histoire d’être constructif : cher Père Noël, est ce qu’il y aurai moyen de rajouter un mécanisme qui pré-processerai les header un maximum, de manière à pouvoir malgré tout pouvoir résoudre la template tout en accélérant la compilation ?

… Interlude : clang-tidy

Ça va aller vite: clang-tidy est un linter qui permet de se concentrer sur l’amélioration du code. Il est très complet, mais affreusement lent.

Pourquoi ? Pareil, les headers de la STL qui contiennent des tonnes de templates, qu’il passe soigneusement en revue, des fois que les personnes chargées d’implémenter la STL soient eux aussi des mauvais programmeurs. Résultat, après 15 (!) bonnes secondes :

Suppressed 116461 warnings (116421 in non-user code, 39 NOLINT, 1 with check filters).
Use -header-filter=.* to display errors from all non-system headers. Use -system-headers to display errors from system headers as well.

Et ça, c’était UN fichier. En plus, il reporte une erreur parce qu’il trouve pas un header que le compilateur arrive manifestement à trouver. Quand je vous dit que le bon C++ ça se mérite.

En pratique, j’utilise donc cpplint qui implémente une partie des règles de Google. C’est toujours mieux que rien. Et de temps à autre, je lance clang-tidy et je vais prendre un café :magicien:

… Et l’héritage, donc.

Mais ce qui m’as achevé, c’est ce bout de code. Et plus particulièrement sa ligne 12:

template<typename T>
class A {
  protected:
    T _x;
    T get() { return _x; }
  public:
    A(T x): _x{x} {}
};

template<typename T>
class B: public A<T> {
  using A<T>::_x;
  public:
    B(T x): A<T>(x) {}
    void set(T y) { _x = y; }
    void eq(T y) { return y == A<T>::get(); }
};

Ce code ne compile pas sans la ligne 12. Je répète lentement : malgré que _x est protected (et … juste au dessus), IL. FAUT. QUE. JE. DÉCLARE. EXPLICITEMENT. Q’IL. EXISTE. DANS. LA. CLASSE. PARENTE. À ce niveau là, je me fout qu’il existe une règle qui justifie l’existence de ce truc.4 Tout l’intérêt de protected est de pouvoir accéder aux éléments de la classe parente à bas cout. Mais non, vous commencez à connaitre le refrain : c’est C++ et c’est une des feature les plus utile, ça serait donc dommage qu’elle soit facile à utiliser…

Sincèrement: soit je rajoute ce fameux using, soit je crée un setter dans la classe parente en plus du getter. Dans les deux cas, c’est du code qui serait inutile s’il ne s’agissait pas d’une template. Il ne s’agit pas d’être explicite ou de favoriser un bon comportement, là. Il s’agit de … Faire le travail du compilateur à sa place ?

Et quitte à finir en beauté: regardez le nombre de A<T>:: qu’il faut écrire dans cet exemple. Je vous promet qu’ils sont tous nécessaires (et que A:: tout seul ne fonctionne pas, mais ça à la limite c’est logique). Manifestement, déclarer public A<T> n’est pas suffisant pour qu’il soit évident que je ne parle pas de std::vector<T> ou de A<float> (à vrai dire, je suis pas assez versé en magie noire pour savoir qu’est ce qui serait valide à la place de A<T>:: dans cette exemple). Encore des trucs qui seraient inutiles sans templates.

Et donc ?

Franchement, j’ai beau me douter qu’il existe une justification à tout ça, je ne peux pas m’empêcher de penser que le C++ est fait pour ne pas simplifier la vie du développeur. Ce qui lui fait un point commun avec Java.5

Bon, évidement, j’ai exagéré: l’héritage n’est pas réellement cassé et le compilateur fini toujours bien par sortir un programme valide en sortie malgré qu’il tourne en rond sur les mêmes templates encore et encore.

Mais est ce que tout ça en vaut la peine ? Franchement, pour une classe qui en pratique ne sera utilisé qu’avec float et double, je suis à deux doigts d’utiliser des #define. Mais bon, c’est comme tout le reste, je garde la foi :ange:


  1. Sauf si on fait des déclarations explicites en début de .cpp. Vu mon cas d’usage, il est probable que je me tourne vers ça.
  2. À ce niveau là, c’est du même niveau que dependabot qui t’annonce qu’une dépendance d’une de tes dépendances contient une faille de sécurité. Dans les deux cas, ça me fait une belle jambe. Du coup, ça m’en fait deux. Soit.
  3. je passe de l’un à l’autre quand GitHub Actions me reporte une erreur spécifique à l’un ou l’autre. Oui, ça arrive, clang++ a tendance à plus coller à la norme, là ou gcc aime bien rajouter ces trucs et être plus permissif.
  4. C’est comme le 49.3, la règle existe mais c’est une mauvaise règle, voilà.
  5. En vrai, je crois pas me souvenir que les templates en Java étaient aussi pénibles. Mais c’est loin.

Et c’est tout le temps comme ça. Plus je tente des trucs, plus je découvre à quel point mes pré-conceptions du C++ sont à l’opposé de ce qui fait le bon C++, et à quel point des choses qui sont simples sur le papier (ou, pour ce que ça vaut, d’autres langages) sont inutilement compliquées à mettre en pratique. C’est un peu décourageant. D’ailleurs, à force de naviguer dans les messages d’erreurs et les suggestions, j’ai fini par tomber sur la FQA, qui est une réponse point par point à (une ancienne version de) la FAQ C++. C’est disponible ici. Certaines critiques sont légitimes, d’autres le sont un peu moins (et tombent parfois dans "la POO, c’est mal" que je mentionnais plus haut). Certaines sont également datées.

Au delà de ça, tout n’est quand même pas toujours difficile en C++, et je retrouve quand même certains patterns que j’ai déjà éprouvés. Juste plus long.

Dit autrement, il est toujours facile de faire "du mauvais X en X" (ou X est le langage de votre choix), mais certains langages forcent quand même plus la main au développeur, par exemple en proposant du sucre syntaxique qui aide à faire les choses bien simplement. Un exemple (controversé), c’est les list-comprehension en Python. Un autre (tout aussi controversé), c’est l'autoboxing en Java. Etc. En C++, le seul truc qui simplifie vraiment les choses, c’est auto, à tel point que pour une fois c’est C qui c’est inspiré de C++ et pas l’inverse :p (promis, après ça j’arrête le sarcasme)

Par contre, le message de ce billet n’est pas de dire "X est mieux que C++" (d’ailleurs, si je cite Python, c’est juste parce que Python est le langage que je maitrise le plus, Python a évidemment bien des défauts et je pourrai écrire un billet similaire dessus). Chaque langage possède ces qualités et ces défauts, et c’est en âme et conscience que j’ai choisi C++ (voir partie 1). Ça ne sert d’ailleurs à rien de dire "t’aurais dû coder en X" : pareil, j’ai fait mon choix et j’ai déjà expliqué pourquoi je n’en changerai pas.

Pour le reste, j’espère sincèrement que ça va s’arranger. Pour C++ et pour moi :)

D’ailleurs, si vous avez des conseils, je suis preneur. Sincèrement :)

PS: ce billet a été écrit sur plusieurs jours afin d’éviter que je ne sois trop dans l’émotionnel.

PPS: Des remarques très intéréssantes m’ont été faites dans les commentaires, que je vous invite à lire également :)


Avant de vous quitter, il faut que j’adresse un dernier point, que je ne sais pas comment exprimer convenablement. Donc respirez un grand coup avec moi, et allons-y : la communauté C++ me semble un peu malsaine. C’est hautement subjectif, totalement basée sur mon expérience personnelle, et évidement, ça ne concerne pas tous les développeurs. Et c’est probablement pas uniquement dans la communauté C++.

Néanmoins, ça va des infos officielles (celles qui sont écrites en partie par les créateurs du langage EDIT: pas tout à fait, merci pour l’info dans les commentaires) de C++ qui trollent ouvertement:1

C.12: Don’t make data members const or references in a copyable or movable type Reason

const and reference data members are not useful in a copyable or movable type, and make such types difficult to use by making them at least partly uncopyable/unmovable for subtle reasons.

La règle C.12 des "C++ core guidelines". Le gras est de moi.

Why am I getting errors when my template-derived-class uses a nested type it inherits from its template-base-class? (…) This might hurt your head; better if you sit down.

La "C++ Super-FAQ", plus particulièrement . Le gras est toujours de moi.

(merci de me rappeler que le C++ c’est compliqué, j’avais pas remarqué. M’enfin si même les concepteurs le disent …)

… Et donc, vu que c’est compliqué et que personne est d’accord, ça va jusqu’aux développeurs eux-mêmes, qui partent en débat interminables dès qu’il y a une question sur "pourquoi ça marche pas" ou "pourquoi il ne faudrait pas faire comme ça ?" (celle sur lesquelles on tombe quand on se pose une question légitime sur le langage, donc). Un exemple parmi tant d’autres : une discussion sur reddit à propos de la règle C.12 que je mentionne ci-dessus. Après avoir lu un certain nombre de commentaires, je ne sais toujours pas quelle est la bonne solution.

Mais le pire, c’est que souvent ça s’échauffe (parce qu’il s’agit limite de dogmatisme) et que ça fini par se mettre dessus plus ou moins gentiment:

Congratulations, you’ve wasted everyone’s time with your extra steps. This data is const FOR YOU, for a specific use case, but not mine. I need to transform it, adapt it, and convert the buffer name to 1337 c0d3.

Ce commentaire dans la discussion sus-nommée. Le gras est de moi.

Et c’est très souvent comme ça. Même la FQA que je cite plus haut tombe parfois (souvent ?) dans ce travers.

Sincèrement, je viens de passer 10 jours à réapprendre le C++, et je ne compte plus le nombre de commentaires désobligeant que j’ai pu lire au cours de mes lectures (avec, évidement, des "pointeurs nus? Beurk" à toutes les sauces, en français ou en anglais2). Calmez-vous, les gens, on est là pour apprendre <3

Voilà. C’est pas un morceau qui m’a fait plaisir à écrire, mais il fallait que j’évacue ça. Cœurs sur vous, les devs C++ :)


  1. La FAQ a un ton très bizarre, d’ailleurs, qui passe du très formel à l’étrangement famillier. C’est … Particulier pour un truc qui se veux officiel.
  2. Je pense que c’est devenu une sorte de running gag, à force, m’enfin quand même.

11 commentaires

Hello,

Petit pinaillage, pour rajouter une couche de "c’est pourtant simple en fait!" emplace_back(), ce n’est pas une question de déplacement (move), c’est une question de construction placée (in-place). Plutôt que de construire et copier (push_back, C++98), ou construire et déplacer (push_back de rvalue en C++11), on donne les paramètres de construction à emplace_back qui va les utiliser pour faire une construction placée directement.

Si on lui passe une rvalue d’un objet déjà construit, ça va faire comme avec push_back: la construction placée se fera avec le constructeur de déplacement. L’intérêt est limité.

La notion de construction placée n’est pas un sujet à destination des débutants. On rentre dans les micro-optims à tendance obfuscatoire. Mais maintenant on a un outil visible qui participe à cela.

IL. FAUT. QUE. JE. DÉCLARE. EXPLICITEMENT. Q’IL. EXISTE. DANS. LA. CLASSE. PARENTE.

this->_x dans ces cas. J’ai vu il y a encore peu des propositions pour autoriser à chercher dans la classe non spécialisée — comme ce que VC++6 (ou 5?) faisait ("incorrectement") en 1995.

Mais… on ne t’a pas encore taquiné avec "mais c’est quoi ces données protégées?" :) (-> Mieux vaut mettre les données en privé quitte à offrir des services protégés à destination des classes enfants. Question de design OO, aucune spécificité avec le C++ — et s’il n’y a pas d’invariant, on peut envisager d’oublier les restrictions de visibilité)

Franchement, pour une classe qui en pratique ne sera utilisé qu’avec float et double

Le conseil (de génie logiciel) récurrent: aller au plus simple. Écrire les tests et la classe pour un seul type, avec un gros using real_type = double; /* par exemple */ au tout début, et la rendre générique si le besoin survient véritablement.

propos de la règle C.12 que je mentionne ci-dessus. Après avoir lu un certain nombre de commentaires, je ne sais toujours pas quelle est la bonne solution.

Mon avis: oublie les données membres const. C’est une transposition de pratiques Java où cette (bonne — en Java) habitude s’est développée pour définir les classes de valeur. Le C++, qui est nativement orienté valeur, attend que l’on puisse directement a = b. En Java, ça agit sur des références. En C++ sur les valeurs. Bref. Très peu de raisons de s’en servir. La bonne pratique C++ va plutôt consister à flagger toutes les fonctions membres comme const à la place.

NB: il m’arrive de temps à autre de définir const des données membres non statiques, mais j’ai toujours une arrière goût de bancalité. Il n’est pas rare que je revienne en arrière sur ce choix.

Même la FQA que je cite plus haut tombe parfois (souvent ?) dans ce travers.

Je n’ai jamais gardé un bon souvenir de la FQA.

La FAQ a un ton très bizarre, d’ailleurs, qui passe du très formel à l’étrangement familier. C’est … Particulier pour un truc qui se veux officiel.

J’ai du mal à la considérer officielle. Elle part de la FAQ maintenue il y a fort longtemps par Marshall C. Cline. Avec des morceaux qui venaient très certainement de comp.lang.c++(.moderated). Bref, très certainement écrite à plein de mains, sur 3 décennies. Carrément écrite par et pour des gen X dont la sensibilité est peu exacerbée.

(Après j’imagine que tu as du voir passer plein de drama sur r/cpp ces derniers mois sur ces sujets.)

A ce propos, elle n’est plus trop à jour, mais je trouvais que l’on avait fait du bon boulot à l’époque pour la FAQ C++ de developpez. Le ton? Pas trop fait attention. Ecrite à plein de mains aussi.

Bref, bienvenu parmi nous. Bon courage. Et n’hésite pas à venir poser des questions.

EDIT:

Je me permets une digression. Est-ce qu’une partie du problème ne viendrait pas que le C++ a gardé le choix du C de tout passer par valeur ? Du peu que j’ai pratiqué le Java (qui est aussi furieusement verbeux expressif), j’ai l’impression que la sémantique de mouvement était une non-question. Ou je me suis trompé ?

Même Java et Python passent tout par valeur. Sauf que ce sont des références qui sont passées par valeur et que du coup, chut faut pas le dire — enfin, une très ancienne doc de Java le disait, mais cela a été retiré. Un test simple pour le vérifier: écrire une fonction de swap qui échange 2 entiers passés par paramètres.

Mais après, le choix de tout passer par valeur est un choix assumé. Particulièrement aujourd’hui.

C’était très intéressant – et c’est encore un billet qui me convainc que je ne devrais jamais toucher à C++, même avec un bâton.

Pour apporter mes 2 centimes sur les sujets que je maitrise (principalemenent Java donc) :

Je me permets une digression. Est-ce qu’une partie du problème ne viendrait pas que le C++ a gardé le choix du C de tout passer par valeur ? Du peu que j’ai pratiqué le Java (qui est aussi furieusement verbeux expressif), j’ai l’impression que la sémantique de mouvement était une non-question. Ou je me suis trompé ?

Je n’avais jamais entendu parler de « sémantique de mouvement » avant aujourd’hui. D’après ce que j’ai compris du cours que tu as lié, c’est principalement lié à de la manipulation directe de références, ce qui ne se fait pas en Java (et qui d’ailleurs est virtuellement impossible dans le langage). Donc, c’est une non-question, mais pas spécialement pour les raisons que tu évoques.

Par ailleurs, Java reste assez verbeux, la grosse différence que je vois avec C++ c’est que Java évolue vers une plus grande efficience et n’hésite pas à ajouter du sucre syntaxique (surtout depuis ~10 ans) ; et cet allègement est très largement supporté par la communauté et les frameworks (exemple : le fameux TrucSuperLongFactory est devenu rare).

Ou faire une Kotlin (donc pas un autre langage, mais une version du C++ ou les bonnes choses seraient le défaut, et les mauvaises difficiles à faire).

Kotlin est un autre langage ; d’ailleurs il compile vers de la JVM, mais aussi du natif (via LLVM), du JS (ES5 par défaut) et en alpha du WASM.

Il a évolué en parallèle de Java, et possède maintenant presque le défaut inverse : c’est devenu facile d’écrire du code Kotlin cryptique parce que trop symbolique et trop surchargé en sucre.

Franchement, j’ai beau me douter qu’il existe une justification à tout ça, je ne peux pas m’empêcher de penser que le C++ est fait pour ne pas simplifier la vie du développeur. Ce qui lui fait un point commun avec Java.

Oh, les templates (« Generics ») Java on plein d’inconvénients, dont la plupart sont des effets de bord du type erasure – et il n’y a à ma connaissance aucun projet pour l’éliminer tellement ça casserait tout. Sur ce point, Java a le même problème que C++ à vouloir garder à tout prix une rétrocompatibilité quasi absolue, et donc à trainer des décennies des boulets historiques.

Un autre (tout aussi controversé), c’est l’autoboxing en Java.

La fonctionnalité n’est plus du tout controversée en fait (en tous cas pas par l’industrie), au point où à un moment il était question de _supprimer la possibilité de manipulation directe des types non-objets de la JVM (ce qui poserait d’autres problèmes et serait une sacrée rupture de rétrocompatibilité !). Par contre il faut faire gaffe aux NPE cachés et éviter Boolean (l’objet derrière boolean) parce qu’il peut être null, et donc devient un ternaire.


En fait en Java cette histoire de passage par valeur ou par référence est un non-sujet, parce qu’en fait on a pas la main dessus (et je suppose que c’est pour ça que ça a été retiré – même si en réalité pas grand-monde ne lit les docs officielles) : tout est passé par valeur. L’astuce c’est que Java a la sale habitude d’exposer dans le langage des mécanismes internes à la JVM, à savoir :

  1. Il existe des types « primitifs » qui sont ceux gérés directement par la JVM: byte short int long, float double, char et boolean (oui y’en a que 8, et tous les numériques sont signés). On les passe par valeur et il n’y a pas de surprise.
  2. Les objets et les tableaux (i.e. l’immense majorité de ce qu’on manipule au quotidien) sont passés par valeur aussi. Sauf que : quand on manipule des objets ou tableaux en Java, les variables sont en réalité des références sur les objets et tableaux, qui elles seront passées par valeur (ce qui apporte une certaine confusion. Le contenu de ces objets et tableaux peut donc se faire modifier par effet de bord au passage d’une méthode, mais jamais la référence elle-même. Concrètement, après un appel de méthode, un objet qui lui est passé en paramètre peut avoir été modifié (c’est l’usage pour sort(...) d’ailleurs) ; mais ça sera toujours le même objet.

Il n’y a pas de manipulation directe des références possible en Java ; et le seul « pointeur » donc il est question c’est dans l’infâme NullPointerException. D’autre part, en Java moderne, on évite absolument de modifier un objet reçu en paramètre d’une fonction sans très bonne raison de le faire (au point qu’une erreur classique de débutant est de faire var sorted = Collections.sort(elements); et de s’étonner que ça plante, parce que l’API standard de tri est de celles qui modifie directement les objets qu’on lui passe. On trouve aussi pas mal d’API de ce style :

public Object modify(Object param) {
    param.variousCallsChangingInternalState();
    return param;
}
// Appelé ainsi :
a = modify(a);
// ou encore :
b = modify(a); // et dans ce cas : a == b

Le return param; est techniquement inutile, parce que l’objet est directement modifié, mais ça fait du code moins « surprenant » et plus lisible dans la meta de l’écriture de code Java.

Juste une remarque générale. Tu n’as pas dit avec quel support tu apprends le C++. Si c’est des ressources en ligne, c’est généralement le bordel. Je te conseille les livres "Tour of C++" puis "Professional C++".

+1 -0

Salut,

Je vais reprendre certains points que je trouve étrange et ajouter quelques explications sur d’autres.

Pour commencer, je pense qu’il y a confusion entre classe et POO. Ce n’est pas parce qu’on fait une classe que cela devient automatiquement un objet dans le sens POO. Pour expliquer ce point il faut que je définisse ce qu’est la POO pour moi: essentiellement respecter le principe de substitution (le L de SOLID)1, c’est-à-dire pouvoir ré-implémenter les comportements et finalement se baser sur une interface. En C++, cela se traduit par le mot clef virtual.

En python/java, le comportement de virtual est celui par défaut et on utilisera final pour l’interdire.

Ensuite on sépare les classes en 2 grandes catégories: classe de valeur et classe d’entité.

Les premières sont des classes qui manipulent des valeurs, elles sont copiables, déplaçables, ré-assignable, en gros, pas de contrainte particulière. Mais surtout, il n’y a pas d’héritage parce que cela ne fonctionne pas bien du tout avec la copie: object slicing. Éventuellement un héritage privé, au pire protégé, mais cela devient un détail d’implémentation.

Typiquement, une matrice est dans ce genre de cas.

La seconde catégorie mériterait d’être séparé en 2, mais elle concerne des objets qu’on ne veut absolument pas copier soit à cause du slicing (quand il y a héritage) ou pour des raisons d’unicité (les value-entity comme std::unique_ptr qui elles peuvent être déplaçables).

Du coup, pour venir sur ce point

l’héritage n’est généralement pas une zero cost abstraction […]

Ce n’est vrai que si on utilise le mot clef virtual qui va créer une table d’indirection (la vtable) pour appeler la bonne implémentation de fonction selon le type de la classe.

Mais dans ton contexte, faire des fonctions virtuelles n’a pas probablement de sens.

le fait que la gestion des exceptions introduits à la compilation des morceaux de code pas ouf pour les performances.

C’est plus ou moins neutre et il est d’avis que s’il n’y a pas d’exception, le coût est nul ou quasi nul. Mais on oublie vite que sans exception, il faut faire les vérifications de retour à la main (plein d’if partout) et en réalité un code avec exception peut être plus rapide pour cette raison (il y a des vidéos de la cppcon sur le sujet).

Par contre, il y a un coût important si l’exception est attrapée (et uniquement dans ce cas) dans un contexte multi-threadé parce que les implémentations actuelles font un lock pour vérifier le type. Donc tous les threads qui se retrouvent en erreur sont synchro.

Par de rapport entre exception et POO.

Il n’y a pas de garbadge collector en C++: ça serait confortable, mais c’est pas indispensable. En tout cas, pas à mon niveau (par contre, j’imagine pas sur des énormes applications).

Quand on a du passage par valeur et du déterminisme de destruction (RAII de son vrai nom), il n’y a pas besoin de GC puisqu’on sait exactement quand une valeur est détruite et quand il faut la libérer.

C’est pour ça qu’on attache l’allocation dynamique à un pointeur intelligent: forcer le déterminisme de destruction pour qu’il s’applique aussi sur le référé.

99.99% du temps, cela suffit, il n’y a que quelque rare cas, quand les données sont partagées dans tous les sens, de manière cyclique et sans point déterminant pour les détruire qu’un GC devient vraiment pratique.

Pire encore, dans un monde ou tu es caillassé en place publique pour utiliser des pointeurs nus (les fameux), new et malloc() sont toujours là. Des fois que tu aimerais te tirer des balles dans le pied (mais pas de kink shaming). Vous ne voulez pas de pointeurs nus ? Baaaah … ne permettez pas d’en faire par défaut

Je suis plutôt d’accord, mais en réalité c’est assez compliqué. Comme tu le dis, le C++ est maladivement rétro-compatible (il y a des guerres récurrentes sur ça au comité de normalisation), mais du coup si new ne retourne plus de pointeur nu, mais par exemple un std::unique_ptr, par quoi on remplace si on veut utiliser l’ancien défaut ? Et que faire de operator new qui retourne un void * qui n’a du coup aucune info pour libérer la mémoire ?

En vrai l’expression new (donc pas l’opérateur) pourrait retourner directement un std::unique_ptr, mais il y a 3 problèmes:

  • Bien que C++ soit le langage et la bibliothèque standard, certains environnements ne disposent pas de la STL (ex: arduino)
  • Le système d’include fait qu’un fichier .cpp et tous les .h qui en dépendent doivent être compatibles. S’il y a un changement sur le retour de new, plein de code cassent et les raisons principales de refus au niveau du comité de standardisation sont: trop cher de tout recompiler et trop cher de faire un changement (cas des certifications). Et peut-être même "ça casse l’ABI"2 (phrase magique pour couper court à toute amélioration).
  • Le C++ se veut en bonne partie compatible avec le C et toute la bibliothèque est disponible. C’est dans le standard, se repose sur telle version du C. Changer malloc va être un problème. Mais bon, on peut le laisser, c’est tellement plus pénible que new que j’espère personne ne l’utilise en C++ sans bonne raison.

Il y avait des propositions d’isolation de feature du genre epoch en rust ou ton pragma unsafe, mais ça n’a pas trop pris à cause des problèmes d’ABI. Peut-être que le jour où les modules se seront démocratisés il y aura des changements.

Et finalement, le pire pour moi au niveau expressivité, c’est la sémantique de mouvement. […] Pourquoi je dois autaaaaant écrire pour faire les choses bien ?

Il n’y a souvent pas besoin de faire grand-chose lorsqu’on déclare les constructeur operateur = à part définir des fonctions à =default / delete. On peut grandement réduire le besoin de réimplémenter les fonctions spéciales en faisant en sorte que les membres soit des types qui implémentent eux-mêmes ces opérateurs.

Du coup, quelques classes hyper rudimentaires qui ne font rien et qui les implémentent pour que toutes les autres les utilisent et n’ait pas besoin de les redéfinir.

La compilation: 13s en C, 1m3s en C++

Il y a des chances que cela vienne de scaLAPACK, parce qu’avec 24 lignes pour meson, ça ne va pas faire beaucoup de .cpp compilé. Je n’ai pas regardé, mais si c’est une ré-implémentation de LAPACK, il y a de forte chance qu’il y ait ce qu’on appelle des expression-templates qui permettent d’optimiser pas mal, mais qui ont un coût important à la compilation.

Si tu es curieux, il y a des options comme -ftime-trace avec clang qui va créer un fichier json contenant le temps de compilation de chaque phase / fichier. Il peut être visualisé sur www.speedscope.app ou chrome://tracing avec chrome / chromium.

À titre indicatif, le plus gros fichier de la STL est <functional> qui prend (sur ma machine) 1/4 de seconde. Il possède beaucoup de dépendance vers d’autre en-tête de la STL et même si c’est un peu lent, c’est loin de prendre plusieurs secondes.

Le système d’include est quand même très désuet, mais les modules (on va dire le nouveau système) ne fonctionne pas encore très bien.

le compilateur réaffiche les mêeeeemes warnings […] sur les dépendances sur lesquelles je n’ai paaaaaaaaas la main

Problème de configuration.

Il y a 2 catégories d’en-têtes: système et autre. Système veut dire pas à nous, donc les warnings ne sont pas affichés. Au niveau du compilateur il y a 2 options pour indiquer les dossiers: -isystem et -I. Au niveau de meson il faut faire include_directories(..., is_system: true).

regardez le nombre de A<T>:: qu’il faut écrire dans cet exemple

Lmghs à répondu this->_x, je vais expliquer pourquoi et comment fonctionnent les classes templates.

Sur les template, tout le code va être déduit le plus tard possible, c’est-à-dire quand on l’utilise. Mais tout ce qui ne dépend pas d’un type template doit être résolu. Par résoudre, j’entends par là que c’est accessible et que dans le cas d’une fonction, un prototype existe. C’est important, parce que même si un meilleur prototype existe après (qui ne fait pas de conversion / upcast par exemple) il ne sera pas utilisé.

Donc quand le compilateur voit get(), cela veut dire qu’une fonction get() doit exister dans le scope de la classe, de ses parents non template ou en fonction libre avant. Lorsque B<T>::eq() est utilisé, get() est déjà résolu et celui de A<T> ne peut donc pas être utilisé.

this-> ou A<T>:: est un moyen pour dire au compilateur que l’identifiant est dépendant d’un type template et le résoudre plus tard.

Et c’est aussi pour ça qu’on se retrouve à mettre typename devant les sous-type template, et obj.template foo<T>() pour les fonctions template d’un objet template lorsqu’on veut indiquer des paramètres template (pas assez de template dans cette phrase).

Dernière chose, comme les templates sont résolus le plus tardivement possible, on peut avoir des fonctions qui ne peuvent pas compiler, mais qui ne donnent pas d’erreur du moment qu’elles ne sont pas utilisées.


  1. Ce qui est probablement très réducteur pour certain. Et je considère aussi que SOLID n’est pas propre à la POO.
  2. Application Binary Interface. On peut voir cela comme une API, mais sur le code compilé. À savoir, il y a eu un gros changement d’ABI en 2011 concernant std::string pour C++11. Pendant des mois, énormément de personnes avaient des problèmes pour lancer tel ou tel programme / lib sur linux, car soit ils étaient sur l’ancienne ABI, soit la nouvelle. Et comme les programme font référence à libstdc++.so les 2 versions ne peuvent pas coexister en même temps. Je pense que cela à traumatisé tout le monde, plus personne ne veut cela.

Je rigole souvent au boulot en disant que le C++ serait un excellent langage si on en supprimait 80% des features, malheureusement, personne n’est d’accord sur lesquels 20% sont à garder.

De manière plus sérieuse, il y a effectivement un problème avec la rétro-compatibilité qui fait que, mis à part quand quelque chose est universellement reconnu comme une erreur (comme std::auto_ptr), c’est pratiquement impossible à supprimer. C’est d’autant plus un problème que ça ralentis beaucoup le développement de nouvelles features vu que personne n’a envie d’ajouter une mauvaise feature qui sera forcé de rester dans le langage pendant 10 ou 20 ans.

Pour moi, le plus gros point fort de C++ c’est le RAII1. En contre-partie, le langage est forcé de donner les outils/concepts nécessaires pour utiliser le RAII sans impacter les performances et ces trucs sont loin d’être simple: le perfect-forwarding, le move semantic, les r-value references, RVO, NRVO, placement new, etc…

Et c’est un peu le problème que C++ a à l’heure actuelle: il est principalement utilisé dans des contextes où les performances sont suffisamment importantes pour ne pas utiliser un autre langage, mais ils essayent quand même de faire quelque chose où c’est plus difficile de faire de la merde qu’avec C. Le langage a des bases qui datent d’il y a au moins 40 ans (et plus si on compte aussi tout ce qui a été copié du C), mais il est principalement développé par des grosses boîtes qui ont des milliards de lignes de code. Donc la rétro-compatibilité est très difficilement négociable.

Dans les conseils que je peux te donner:

  • utilise la version du compilateur la plus récente que tu peux utiliser, surtout si tu tentes d’utiliser des features un peu récentes. Non seulement les messages d’erreur seront probablement meilleurs, mais ça te permettra probablement d’éviter de perdre trop de temps à cause de potentiels bugs dans le compilateur2.
  • fait relire ton code par une (ou plusieurs) personnes qui connaissent très bien le C++, en se focalisant principalement sur les trucs qui peuvent être simplifiés ou les trucs qui peuvent facilement amener à des comportement indéterminés (et donc sans pinailler sur des détails comme auto x{1}; vs int x = 1;). C’est probablement le meilleur moyen d’apprendre le langage.
  • <opinion>

    C++ n’est pas forcément un langage super bien adapté pour de la POO de fanatique. Entre faire de l’héritage et faire autre chose, l’autre solution est "mieux" au moins 80% du temps. C’est parfaitement normal de se retrouver avec un gros projet C++ qui ne contient pas le moindre protected

    </opinion>


  1. le fait que le destructeur d’un objet est garanti d’être appelé quand l’objet en question sort de la pile.
  2. d’expérience, la majorité des bugs font que le compilateur n’accepte pas du code parfaitement valide d’après le standard. T’as aussi des cas où le compilateur va segfault pendant la compilation. C’est très rare qu’un bug fasse que le code généré ne corresponde pas au code que tu as écris.

C’est très rare qu’un bug fasse que le code généré ne corresponde pas au code que tu as écris.

Hum… C’est fréquent que le code asm généré ne corresponde pas au code C++ (et C aussi) écrit. Cf la classique boucle qui somme les entiers de 1 à n qui sera remplacée par une multiplication de n par n+1 plus une magouille sur les bits pour diviser par deux.

Le résultat observable d’un code C++ non buggué, doit être conforme à ce que le code C++ exprime — la fameuse règle du "as-if". Et on est d’accord, il est rare qu’un compilo aie des bugs qui invalide ça.

Pour expliquer ce point il faut que je définisse ce qu’est la POO pour moi: essentiellement respecter le principe de substitution (le L de SOLID)

Je prends 2 trucs:

  • tout le sucre syntaxique qui permet de fournir des boites noires qui font abstraction de détails et posent des capsules protectrices autour des invariants de nos objets. Pas forcément propre à l’OO. Mais les langages OO facilitent ça — plus ou moins (cf Python où l’encapsulation relève plus d’un contrat de confiance)
  • et comme toi: le sous-typage qui apporte la substituabilité, et qui ne marche bien que si on respecte le LSP.

+1 aussi à "le prix d’appel de fonctions virtuelles est un non sujet quand on a besoin de résoudre des points de variation dynamique".

Ce qui me fait revenir sur ce passage de Pierre_24

Bien que ce soit des critiques pertinentes [prix du polymorphisme et prix des exceptions], elles me semblent liées à la POO dans C++ donc

La critique du prix d’appel des fonctions virtuelles et une critique de dévs C qui compare le coût d’appel entre une fonction virtuelle et une qui ne l’est pas. Le sur-coût ne serait pas meilleur dans les autres langages -> pas propre au C++.

Mais… après je suis d’accord avec jo_link_noir: ce n’est pas la bonne question. Il faut comparer un appel de fonction virtuelle avec une série de if, ou un switch: soit un mécanisme qui permet de choisir qui on appelle vraiment — généralement non extensible (cf. l’OCP). Il faut aussi prendre en compte la prédictibilité de la branche à suivre, et l’éparpillement en cache des objets parcourus (choses qui peuvent être fortement mitigées par des choses comme boost.poly_collection).

Typiquement, une matrice est dans ce genre de cas [i.e. valeur].

Pour le coup, j’ai vu beaucoup d’implémentations de matrices qui, malgré la nature "valeur" de la classe, reposaient sur de l’héritage. Je n’ai pas vraiment réfléchi si c’est vraiment ce qu’il y a de mieux en termes de design.

C’est parfaitement normal de se retrouver avec un gros projet C++ qui ne contient pas le moindre protected

<analogie foireuse>La visibilité protégée, c’est un peu comme les poêles avec un revêtement anti-adhésif de type téflon. C’est plus facile de prime abord, mais ce n’est pas un bon réflexe</>

NB: c’est pareil dans tous les autres langages OO qui offrent un moyen aux classes filles de fragiliser les invariants des classes parentes. -> bonne pratique qui n’est pas propre au C++.

Merci pour vos commentaires. Merci aussi aux différentes personnes qui m’ont signalé des erreurs en MP, une nouvelle version est désormais disponible. Par contre, je vais laisser les critiques originales telles quelles et y répondre ici, afin de conserver l’historique de la discussion (j’ai quand même ajouté une note en fin de billet qui dit que les commentaires sont intéressants à lire).

Pour répondre à peu tout le monde :


Merci Spacefox et lmghs de m’avoir rappelé qu’en Java et en Python, c’était un passage de référence par valeur, et pas un passage par référence. Ça m’était totalement sorti de la tête parce que ça ne change pas grand chose dans la plupart des cas :-°


La notion de construction placée n’est pas un sujet à destination des débutants. On rentre dans les micro-optims à tendance obfuscatoire. Mais maintenant on a un outil visible qui participe à cela.

lmghs

Je vois :)

J’ai du mal à la considérer officielle. Elle part de la FAQ maintenue il y a fort longtemps par Marshall C. Cline. Avec des morceaux qui venaient très certainement de comp.lang.c++(.moderated). Bref, très certainement écrite à plein de mains, sur 3 décennies. Carrément écrite par et pour des gen X dont la sensibilité est peu exacerbée.

lmghs

Je vois. J’ai été eu par l’intro qui dit:

What’s “Super” about this FAQ? In part it’s because this is a merger of two great FAQs: Marshall Cline’s C++ FAQs, and Bjarne Stroustrup’s C++ FAQ.

(et le fait que le site s’appelle "isocpp", ou qu’on y trouve les fameuses "C++ core guidelines", qui pour le coup m’ont l’air plus officielles). En effet, le fait qu’il s’agisse de la fusion de deux FAQs ne veut pas forcément dire que l’un a relu la partie de l’autre :)


Kotlin est un autre langage ; d’ailleurs il compile vers de la JVM, mais aussi du natif (via LLVM), du JS (ES5 par défaut) et en alpha du WASM.

Spacefox.

Whoops, mon mauvais. J’ai lu la phrase "dérivé du Java" de manière un peu trop littérale. Pour le coup, je confesse ma complète ignorance: je n’ai jamais écrit la moindre ligne de Kotlin, et je sais qu’il existe … Grace à toi :-°


Juste une remarque générale. Tu n’as pas dit avec quel support tu apprends le C++. Si c’est des ressources en ligne, c’est généralement le bordel. Je te conseille les livres "Tour of C++" puis "Professional C++".

gbdivers

Touché, comme disent nos copains outre-atlantique. Je suis parti du tuto C++ d’ici, et puis j’ai itéré sur les messages d’erreurs et recommandations que j’obtenais.

Par contre, je n’ai rien contre les livres (si ce n’est que faire un CTRL+F y est plus compliqué), mais je suis très étonné qu’en 2025, il n’existe justement pas de bonne ressources en ligne, même en anglais. Je suis tombé sur plusieurs articles de blogs sur des points précis, mais effectivement, à part ça, c’est le bordel, comme tu dis. Curieux.


[héritage != zero cost abstraction], [gestion des exceptions], [absence de GC]

jo_link_noir

Merci pour les précisions :)

Néanmoins, j’avais bien dit que c’était des non-problèmes : j’ai choisi C++, donc d’embrasser tout ces mécanismes internes et ces abstractions (qui, effectivement, vont au delà de l’OO, et j’ai fait le raccourci trop facile "exceptions = OO"). Typiquement, comme tu le dis très bien, je fais du RAII, donc la question du GC ne se pose pas trop. Et pour le coup, je n’ai pas à me plaindre, parce que ça fonctionne bien.


[la sémantique de mouvement]

jo_link_noir

À la lecture de ton commentaire et des autres, je me rends compte que j’ai oublié d’expliquer pourquoi cette fameuse sémantique de mouvement (et, par ailleurs, les pointeurs intelligents) est tellement importante pour moi dans le contexte de ce projet.

Quand je dis en introduction que je dois gérer de grosses matrices, je ne rigole pas. Typiquement, je vise une application dans laquelle mes matrices font quelques centaines de Gio (quelque chose comme 50 000x50 000 nombres flottants en simple précision, dont je ne stockerai que la moitié parce que la matrice est symétrique, mais soit). Bien entendu, la matrice est distribuée, mais disons que je ne peux pas me permettre trop de copies, même temporaires, parce que je chatouille quand même gentiment la limite de mémoire des machines que j’utilise. Évidement, c’est tout l’enjeu du projet.

Donc autant en C, j’avais assez facile de faire un malloc puis à me promener des pointeurs, ici je dois réaliser la même chose à peu près sans pointeurs, donc grâce à la sémantique de mouvement et au perfect forwarding, parce que je suis typiquement dans le cas d’usage ou je ne peux pas me permettre des copies (même si les concepteurs du langage n’avaient probablement pas mon cas d’usage en tête quand ils ont pensé la fonctionnalité :p ).

Mais pourquoi "sans pointeurs" ? Eh bien parce que …

[la matrice comme exemple d’objet]

jo_link_noir et lmghs

OK, là aussi je vous dois des explications:

  1. J’ai pris cet exemple parce qu’il est parlant, mais en fait, j’ai pas la main dessus parce que j’utilise les objets proposés par ma libraire d’algèbre linéaire (qui, notez bien, n’implémente pas de constructeur de mouvement :'( ).
  2. Pour aller dans le sens de lmghs, ils utilisent l’héritage pour gérer le fait qu’une matrice symétrique est dérivée d’une matrice générale. Il faudrait que j’y réfléchisse à tête reposée, mais j’ai l’impression que le principe de Liskov est respecté, et que donc c’est valide (même s’il y a probablement moyen de trouver des corner case avec le fait qu’une matrice symétrique est forcément carrée, mais voilà).
  3. Vu que la matrice doit gérer sa distribution sur les différentes machines, c’est quand même un peu plus qu’une simple data class. Mais la remarque de jo_link_noir reste pertinente.

Il y a des chances que cela vienne de scaLAPACK, parce qu’avec 24 lignes pour meson, ça ne va pas faire beaucoup de .cpp compilé. Je n’ai pas regardé, mais si c’est une ré-implémentation de LAPACK, il y a de forte chance qu’il y ait ce qu’on appelle des expression-templates qui permettent d’optimiser pas mal, mais qui ont un coût important à la compilation.

jo_link_noir

La remarque est valide, il y a de grande chance que beaucoup de choses viennent de ça (pas sûr qu’ils utilisent des mécanismes aussi poussés, ceci dit, c’est plein de bonne volonté, mais je n’ai pas l’impression que les développeurs soient forcément au courant des évolutions du C++ non plus).

Ceci dit, mon point reste: "templates = c’est long à compiler" ;)


(…) Bien que C++ soit le langage et la bibliothèque standard, certains environnements ne disposent pas de la STL (ex: arduino)

jo_link_noir

Ça, c’est un argument que je vois souvent et que je ne comprends pas.

Ou plutôt que je comprends mais que je ne partage pas. Je me doute bien que le but, et l’exemple de l’arduino est parlant, est d’avoir un langage dont les dépendances sont minimales voire non-existentes. Dans d’autres cas, c’est aussi de pouvoir utiliser des STL alternatives. Sauf que tout dépend ou on place le curseur : si demain le comité de normalisation décide que unique_ptr et shared_ptr (typiquement) sont des trucs inhérents au langage, bah ça n’en fait plus une dépendance, mais simplement un truc qui fait partie de C++.

Et pour le coup, je suis carément team "on déprécie new et on replace par new_unique et new_shared" :p


(…) Il y a 2 catégories d’en-têtes: système et autre. Système veut dire pas à nous, donc les warnings ne sont pas affichés. Au niveau du compilateur il y a 2 options pour indiquer les dossiers: -isystem et -I. Au niveau de meson il faut faire include_directories(..., is_system: true).

Merci pour le tip. Ceci dit, il va falloir que j’investigue : par flemme, j’ai utilisé dependency() (ce qui va finir par me poser des problèmes parce que ça suppose qu’il y ait pkg-config ou cmake dans l’environement ou je compile), et donc je m’attendrai que Meson les considère comme system (ce qu’elles sont, j’ai installé la librairie au niveau du système). Par contre, je pense que ça va m’être utile sur une autre dépendance, qui elle est gérée via un subproject() et qui a aussi tendance à faire râler les compilateurs (fmt, pour pas la citer, le support de std::format est pas encore bon).


Et comme les programme font référence à libstdc++.so les 2 versions ne peuvent pas coexister en même temps.

jo_link_noir

Je veux même pas savoir pourquoi personne ne c’est dit qu’utiliser semver pour ce genre de cas serai une bonne idée. Ou même, chaipa, libstdc++03.so et libstdc++11.so :-°


Lmghs à répondu this->_x, je vais expliquer pourquoi et comment fonctionnent les classes templates (…).

jo_link_noir

Ok, merci pour la précision (en même temps, je me doutais bien qu’il existait une bonne raison, c’est juste que c’est vraiment pas pratique). Je pense que je vais rester sur this->x_ (qui est moins chargé en contexte), pour le coup.


[le protected]

lmghs, jo_link_noir, Berdes

Le truc mériterai un topic à lui tout seul (peut être que j’en ferai un, d’ailleurs), mais en fait, l’exemple que je donne est une simplification de celui-ci:

template<typename T>
class BaseA {
  // invariant: v.size() > 0. Et encore, c'est même pas forcément important.
  protected:
    std::vector<T> v;
};

template<typename T>
class A1: public BaseA<T> {
  // invariant: v.size() == A.side()
  private:
    Matrix<T> A;
}

template<typename T>
class A2: public BaseA<T> {
  // invariant: v.size() == B.side() == C.side()
  private:
    Matrix<T> B;
    Matrix<T> C;
}

Donc le protected ne vient pas vraiment d’une volonté d’affaiblir l’invariant de la classe parente (que du contraire), mais simplement de factoriser un petit peu, parce que la gestion de v est réellement équivalent dans A1 et A2, mais que j’ai quand même besoin d’y accéder (en lecture) à bas cout dans les classes filles, et que je m’étais dit qu’un get() ne ferait qu’alourdir le truc.

Pareil, faudrait que je me pose pour voir ce que Liskov en dit, mais je pense que je suis dans le bon.


Et c’est un peu le problème que C++ a à l’heure actuelle: il est principalement utilisé dans des contextes où les performances sont suffisamment importantes pour ne pas utiliser un autre langage, mais ils essayent quand même de faire quelque chose où c’est plus difficile de faire de la merde qu’avec C.

Berdes

Je suis team "plus (+) forcer la main au dévellopeurs", comme vous aurez pu le constater ^^

C++ n’est pas forcément un langage super bien adapté pour de la POO de fanatique.

Berdes

De fait. Ceci dit, on a quand même réussi à invoquer deux fois Liskov en 6 commentaires :p

Entre faire de l’héritage et faire autre chose, l’autre solution est "mieux" au moins 80% du temps.

Berdes

Il est bien possible que ce que je cherche à faire plus haut soit en effet de la composition. Pareil, il faut que j’y réfléchisse.

EDIT: je sais pas écrire, c’est dingue.

+2 -0

Petite note rapide à propos de new: il y a des cas où son utilisation est necessaire. Typiquement, quand tu as un objet dont le constructeur est privé et qui expose une ou plusieurs méthodes statique pour la construction, si ces méthodes veulent retourner un unique_ptr, make_unique ne peut pas être utilisé parce que le constructeur n’est pas visible. Tu te retrouves alors à écrire un simple std::unique_ptr<MyObject>(new MyObject (...)) pour obtenir ton pointeur.

Les methodes statiques en remplacement de constructeurs, ça arrive couramment quand tu utilise quelque chose comme std::expected pour gérer les erreurs et que la construction de l’objet peut fail.

Plein de choses intéressantes.

Je vais surtout rebondir sur la partie multi-noeuds.

Intuitivement, 50k^2 de floats (9Go, non?), ca ne me parait pas si gros que ça pour justifier du multi-noeuds — d’autant que tu parles de les déplacer, ce que je comprends en "c’est bien en RAM", ou alors il y a des handles MPI dont la possession est déplacée?. Il n’est pas rare aujourd’hui que mon parallélisme soit à deux niveaux. Une chaine d’images (volumineuses) sur une machine, et chaque image est découpée en tuiles (une tuile à la fois chargée en mémoire), et les calculs sur la tuile sont dispatchés dans plusieurs threads.

Du coup, j’évite le côté compliqué du multi-nœuds, pour donner à chaque nœud une seule donnée. Mais c’est aussi parce que j’ai plusieurs grosses données relativement indépendantes. Rien à voir avec les éléments finis titanesques de la météo — où c’est une fois un truc très gros (ou le contraire selon comme on regarde la chose) à utiliser simultanément avec les voisins

(A propos de multi-nœuds, HPX m’intrigue beaucoup. Pour son approche C++ — je soupçonne fortement que HPX a influencé les évolutions MT de la lib standard du C++11. Et pour le fait que ce ne soit pas restreint aux problèmes réguliers — comme MPI peut l’être)

Bref. Tout ça pour dire, qu’un truc intéressant avec Eigen, Blaze et Armadillo (3 lib matricielles capables de dispatcher à la MKL (si on est sur intel) IIRC): du fait qu’elles reposent sur des expressions templates, déplacer n’est plus vraiment un problème: on n’est pas à renvoyer des matrices par valeur des diverses opérations; mais à la place, un A * x + B sera reconnu pour appeler la bonne fonction SAXPY au moment de l'"affectation" — je mets des guillemets, il y a probablement plus de subtilités que ça.

EDIT: je sais pas écrire, c’est dingue.

ha ha. Il n’est pas rare que je passe une heure à me relire et me corriger. Et j’ai vu encore après qu’il y avait de jolis fautes d’accord dans mon premier message, un mot qui manque… normal quoi.

Petite note rapide à propos de new: il y a des cas où son utilisation est nécessaire. […]

Il y a d’autres techniques avec des clés privés https://godbolt.org/z/Yz3GKT17b

Je vais faire un message court, tu as deja assez a lire avec les autres messages.

(et le fait que le site s’appelle "isocpp", ou qu’on y trouve les fameuses "C++ core guidelines", qui pour le coup m’ont l’air plus officielles)

Je sais pas si tu as vu/compris le fonctionnement du comité C++, mais c’est un peu le bordel. Pour simplifier, le C++ est un langage standardisé par l’ISO. Le comité C++ ne décide pas en fait du langage C++, il propose un document, qui est soumis au vote a l’ISO. N’importe quel membre de n’importe quel pays qui vote contre, le document est rejeté. C’est pour cela qu’il est tres difficile de supprimer des choses dans le C++.

Perso, j’attend plus des compilateurs que du comité pour "supprimer" des choses du langage (= ajouter un warning quand on utilise un truc qu’il faut pas).

Par contre, je n’ai rien contre les livres (si ce n’est que faire un CTRL+F y est plus compliqué), mais je suis très étonné qu’en 2025, il n’existe justement pas de bonne ressources en ligne, même en anglais. Je suis tombé sur plusieurs articles de blogs sur des points précis, mais effectivement, à part ça, c’est le bordel, comme tu dis. Curieux.

Comme dit par les autres, personne n’est d’accord sur le C++. En particulier sur ce que doit contenir un cours, l’approche pédagogique, etc. Il y a un groupe de travail sur ce sujet, mais ils produisent (a mon avis) des documents pas tres utilisables. Donc difficile d’avoir un travail collaboratif qui permettrait d’avoir un cours officiel sur le C++, comme on a par exemple avec Rust.

j’avais assez facile de faire un malloc puis à me promener des pointeurs, ici je dois réaliser la même chose à peu près sans pointeurs, donc grâce à la sémantique de mouvement et au perfect forwarding, parce que je suis typiquement dans le cas d’usage ou je ne peux pas me permettre des copie

Je connais pas ton code, mais attention, un déplacement peut devenir une copie ! Le déplacement dit juste au compilateur "je n’ai plus besoin de l’objet à l’endroit initial, si tu peux optimiser, va y". Mais si tu "déplace" un bloc mémoire a un autre endroit (par exemple une matrice qui serait sur la Pile), alors il n’y a pas d’autres solution que de copier les données à la nouvelle adresse mémoire.

mais j’ai l’impression que le principe de Liskov est respecté, et que donc c’est valide

Remarque general. Liskov, c’est de la conception. C’est pour essayer d’avoir du code plus maintenable. Mais toi, ton objectif, c’est la performance a priori. Il est parfois nécessaire/bénéfique de CHOISIR d’avoir un code moins maintenable mais plus performant. Tant que tu fais ce choix en connaissance de cause.

Et plus generalement, quand on te dit "il faut faire ceci, cela, mais pas ceci, cela", ca n’a pas forcément du sens. Dans ton contexte. Il est important, a mon avis, de comprendre pourquoi on fait ces choix, les implications positives et négatives, et choisir ce qu’il y a de mieux (ou moins pire, ou qui n’impacte pas) selon ton propre contexte.

+5 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte