Pourrons-nous enfin utiliser les modules C++ en 2026 ? par Mathieu Ropert
Tous les 6 à 12 mois, j’essaie d’utiliser les modules C++, je me heurte à un obstacle, je m’en plains peut-être sur les réseaux sociaux, puis je passe à autre chose. Même après avoir regardé de nombreuses conférences sur le sujet, il y a toujours quelque chose qui vient tout gâcher. Ma plus grande réussite à ce jour a été de parvenir à utiliser le module VulkanHpp dans ma bibliothèque de rendu, après quoi tout a commencé à dérailler. Mais après avoir de nouveau fait quelques progrès la semaine dernière (et rencontré de nouveaux obstacles), j’ai le sentiment d’avoir suffisamment d’éléments pour dresser un bilan cohérent.
À titre de mise en garde, je tiens à préciser que j’ai partagé certaines de mes conclusions sur l’état actuel des modules avec d’autres programmeurs C++ et qu’ils n’étaient pas tous d’accord avec moi. Cependant, je pense que les modules souffrent d’un fort problème de « biais d’expert » qui fait que de nombreux contre-arguments ressemblent à « sur ma machine, ça marche » pour des personnes comme moi qui n’ont pas eu beaucoup d’expérience avec ce sujet et n’ont pas suivi de près le processus de normalisation. Je ne prétends pas être un expert en la matière, mais je m’y connais en systèmes de compilation et je pense avoir passé bien plus de temps à essayer de bidouiller les modules dans mes projets que le programmeur C++ moyen ; je pense donc que cet article peut parler au nom de l’utilisateur passionné moyen (ou plutôt de l’utilisateur en devenir).
Ah, et je me concentre principalement sur MSVC. Je mentionnerai peut-être brièvement Clang ou GCC, mais mon expérience se limite principalement à Windows.
Les aspects simples
Contrairement à ce que vous avez peut-être entendu, les cas d’utilisation simples sont assez faciles à mettre en œuvre, à condition de respecter un ensemble strict de contraintes. Par exemple, comme je l’ai mentionné précédemment, j’ai utilisé le module fourni par VulkanHpp dans ma bibliothèque de rendu et cela fonctionne très bien. Ou plus précisément, cela fonctionnait, jusqu’à ce qu’ils modifient quelque chose en amont, ce qui a entraîné un problème lié à l’ensemble de contraintes auquel j’ai fait allusion. Nous reviendrons sur les détails plus tard. En attendant, voici à quoi cela ressemble dans mon CMake :
1 2 3 4 5 6 7 8 9 10 11
| add_library( VulkanHppModule )
target_sources( VulkanHppModule PRIVATE
FILE_SET CXX_MODULES
BASE_DIRS ${Vulkan_INCLUDE_DIR}
FILES ${Vulkan_INCLUDE_DIR}/vulkan/vulkan.cppm
)
target_compile_definitions( VulkanHppModule PUBLIC
VULKAN_HPP_NO_SETTERS
VULKAN_HPP_NO_CONSTRUCTORS
)
target_link_libraries( VulkanHppModule PUBLIC Vulkan::Vulkan ) |
Je n’ai même pas eu à écrire ces lignes moi-même, elles figuraient dans la documentation du projet. Tout ce que j’ai vraiment dû personnaliser, ce sont les définitions de compilation si nécessaire (dans ce cas précis, j’ai désactivé les setters et les constructeurs pour m’appuyer à la place sur les initialiseurs désignés de C++ 20).
Et ça a fonctionné : il m’a suffi d’écrire import vulkan_hpp dans ma bibliothèque de rendu pour utiliser les liaisons C++ de Vulkan. Si je n’avais pas réussi à le faire fonctionner, je serais probablement revenu à l’API C de Vulkan avec mes propres wrappers RAII personnalisés, car les temps de compilation avec les #include standard étaient exécrables. Cela fonctionnait également de manière récursive (là encore, avec des limitations que j’expliquerai plus tard), ce qui signifie que ma bibliothèque de rendu pouvait inclure l’importation de VulkanHpp dans ses en-têtes publics et que cela se transmettait sans problème lorsqu’elle était intégrée à mes projets utilisant #include <renderer/renderer.h>.
Vous avez peut-être lu qu’il fallait un peu bricoler CMake pour faire fonctionner les modules, qu’il fallait utiliser des indicateurs ésotériques tels que CMAKE_CXX_SCAN_FOR_MODULES, CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP ou CMAKE_EXPERIMENTAL_CXX_MODULE_CMAKE_API, mais aucun d’entre eux n’est nécessaire pour le moment, à condition d’utiliser une version récente de CMake (idéalement la 4.x, mais les paramètres par défaut devraient fonctionner à partir de la version 3.28).
Et voilà : en peu de temps, j’avais remplacé les 9 secondes interminables qu’il fallait pour inclure VulkanHpp par un temps négligeable de quelques millisecondes. Je considère cela comme une belle victoire. Mais c’est là que les ennuis commencent…
IntelliNonSense
Voici une anecdote amusante : vous pouvez trouver des comptes-rendus de réunions du SG15 datant de 2019 dans lesquels Microsoft affirme que les modules fonctionnent parfaitement en interne pour l’équipe Edge. Et pourtant, si vous ouvrez un projet utilisant des modules avec Visual Studio 2026, vous êtes accueilli par ce message incroyable :
C++ IntelliSense support for C++20 Modules is currently experimental.
Eh oui. Ça fait 7 ans, et ils n’arrivent toujours pas à faire en sorte qu’IntelliSense analyse correctement les directives d’importation. Je sais que le serveur de langage est basé sur EDG et non sur VC++, mais franchement, je m’en fiche. C’est une entreprise valant près de 3 000 milliards de dollars au moment où j’écris ces lignes qui nous dit qu’elle n’arrive pas à faire fonctionner une fonctionnalité une décennie après avoir milité pour la normalisation des modules en s’appuyant sur sa propre réussite interne. Je ne sais pas s’ils ont exagéré leurs affirmations à l’époque, s’ils n’ont pas suffisamment financé l’équipe Visual Studio depuis, ou quoi d’autre, mais on ne peut pas me dire que huit ans n’ont pas suffi pour faire fonctionner la coloration syntaxique avec les modules. Et si c’est le cas, alors peut-être y avait-il quelque chose de profondément erroné dans leur proposition et le comité aurait dû demander à voir les justificatifs avant de voter pour.
Quoi qu’il en soit, voici comment résoudre le problème :
1 2 3 4 5 6
| #if defined( __INTELLISENSE__ )
#include <vulkan/vulkan.hpp>
#include <vulkan/vulkan_raii.hpp>
#else
import vulkan_hpp;
#endif |
Cela permet à votre compilateur (et au temps d’itération) de rester sur la voie rapide du module, et IntelliSense peut alors continuer à analyser les fichiers d’en-tête en arrière-plan, ce qui vous offre la mise en évidence et l’autocomplétion. Est-ce une astuce ? Absolument. Mais c’est une astuce que j’utilise depuis 6 mois et qui me permet de me concentrer sur autre chose.
Maintenant que cela est dit, nous pouvons aborder le véritable problème.
Les modules sont un système « tout ou rien »
J’ai laissé entendre dans les sections précédentes que les modules fonctionnent si l’on s’en tient à certaines contraintes strictes. Le problème, c’est que ces contraintes ne sont pas négligeables. En gros, les modules fonctionnent selon un principe « tout ou rien ». Si vous commencez à utiliser des bibliothèques via des directives d’importation, vous ne pouvez pas faire en sorte que la même unité de traduction les intègre via des #include. Et cela devient rapidement un problème.
Voici l’exemple le plus simple pour l’illustrer :
1 2 3 4 5 6 7 8
| // Works, obviously
#include <array>
// Works even if <array> is included before and part of the std module
import std;
// Error, will yield a million "xxx already declared" failures
#include <utility> |
En termes simples, une bibliothèque peut à la fois être importée et incluse, à condition que la directive #include apparaisse en premier et l’import en second. Je ne sais toujours pas si cela est imposé par la norme ou s’il s’agit d’une limitation d’implémentation, mais c’est quelque chose que j’ai observé directement sur MSVC et dont d’autres ont également fait mention.
Dans mon cas d’utilisation précédent, cela ne posait pas de problème, car VulkanHpp n’est importé que par ma bibliothèque de rendu, n’importe rien lui-même et n’est utilisé nulle part ailleurs dans mon arborescence de compilation. Malheureusement, les choses ont pris une mauvaise tournure lorsque la version récente a commencé à intégrer la bibliothèque standard via import std. Du coup, il y a soudainement une dépendance transitive qui importe une bibliothèque très courante ; je dois donc désormais m’assurer que ma directive import vulkan_hpp vient après tout autre #include de la bibliothèque standard. Et comme vulkan_hpp est utilisé publiquement dans ma bibliothèque de rendu, celle-ci doit désormais toujours être importée en dernier dans chaque unité de traduction. Sinon, j’obtiens un milliard d’erreurs de compilation liées à des redéclarations ou redéfinitions.
« Il suffit de passer aux modules »
La solution recommandée, m’a-t-on dit, consiste à tout transférer dans des modules. Ou du moins, si une bibliothèque commence à utiliser import std, à modifier toutes les autres bibliothèques que j’utilise pour qu’elles n’utilisent également que import std. Dans le cas de mon projet jouet, cela concernerait au moins TBB et fastgltf. Ironiquement, cela ne semble pas avoir d’impact sur les bibliothèques C++ qui s’appuient uniquement sur la bibliothèque standard C (je pense que ce serait le cas si j’utilisais import std.compat ?). C’est regrettable que cela donne raison aux auteurs de bibliothèques qui refusent d’utiliser la STL.
Notez que j’ai parlé de « modifier », et non pas simplement d’« activer un bouton ». Car bien que C++20 ait six ans, pratiquement aucune bibliothèque C++ ne propose de définition de module. Boost ne propose des modules que pour quelques bibliothèques triées sur le volet. Les affirmations que j’ai lues selon lesquelles Catch2 fournirait un module semblent n’avoir été qu’une hallucination de l’IA. La seule grande bibliothèque que j’ai pu trouver est fmt, qui est une bonne bibliothèque, mais honnêtement, si vous disposez de la prise en charge de C++20, vous avez de toute façon déjà <format> à votre disposition.
Et bien sûr, chaque bibliothèque qui décide de prendre en charge les modules doit proposer une forme de compilation double, car tous ses clients n’utilisent pas encore les modules. Et pour chacune de ses propres dépendances, elle doit décider si elle les intègre via #include, via import ou si elle laisse l’utilisateur les configurer (mon opinion actuelle est que la version module devrait toujours utiliser import et ne pas proposer d’option, afin d’éviter l’enfer combinatoire).
Prise en charge de la compilation double
J’ai ensuite essayé de prendre en charge la compilation double pour ma bibliothèque de rendu, et ce n’est pas tout à fait une mince affaire.
Tout d’abord, comme suggéré précédemment, il faut basculer des #include vers des import lors de la compilation/l’analyse en mode module. Cela implique généralement d’ajouter une définition et de faire quelques manipulations autour de chaque directive #include :
1 2 3 4 5 6 7
| #ifndef RENDERER_MODULE
#include <array>
#include <utility>
#include <vector>
#else
import std;
#endif |
Pour les bibliothèques qui ne comportent qu’un seul fichier d’en-tête, ce n’est pas si grave, mais pour les bibliothèques plus complexes composées de plusieurs fichiers .cpp et .h, cela s’apparente un peu plus à une chasse aux œufs de Pâques. Dans ma branche POC actuelle, j’ai fini par supprimer toutes les directives #include et les regrouper dans un seul fichier que je peux activer ou désactiver selon que l’on utilise ou non les modules. Cela ralentit la compilation sans modules, car désormais toutes mes unités de traduction chargent une multitude d’en-têtes dont elles n’ont pas personnellement besoin (je te regarde, <filesystem> 😠
.
Ensuite, il faut composer avec le fait que les directives de module ne peuvent pas être exclues via #ifdef. C’est une contrainte de conception. Je ne sais pas exactement pourquoi, mais cela constitue une erreur grave selon la norme. Ce qui signifie que si vous avez un fichier d’implémentation .cpp, vous ne pouvez pas utiliser #ifdef et ses dérivés pour le déclarer de manière conditionnelle comme faisant partie d’un module. Il reste donc trois options : un hack, un autre hack ou toujours compiler votre bibliothèque en tant que module.
Commençons par la première astuce. Je ne l’apprécie pas, mais elle montre en quelque sorte la futilité d’essayer de restreindre l’utilisation de #ifdef dans la spécification. En effet, cette restriction ne s’applique pas à #include. Nous pouvons donc simplement la contourner en dupliquant chaque fichier d’implémentation :
1 2 3 4
| // device_module.cpp
module renderer;
#define RENDERER_MODULE
#include <device.cpp> |
Cela implique d’utiliser un ensemble différent de fichiers .cpp, que vous compiliez ou non en tant que module, et d’avoir un fichier de liaison supplémentaire pour chaque fichier d’implémentation, mais cela fonctionne. Une autre suggestion, proposée par Daniela Engert, consistait à abandonner complètement la compilation séparée de tous les fichiers .cpp et, à la place, à les intégrer tous dans la section module :private; de la définition du module à l’aide de directives #include :
1 2 3 4 5 6 7 8 9 10
| export module renderer;
export {
#include <renderer/renderer.h>
}
module :private;
#include <renderer/bindless.cpp>
#include <renderer/buffer.cpp>
#include <renderer/command_buffer.cpp>
#include <renderer/device.cpp>
// ... |
Certains de mes lecteurs pourraient objecter : « mais cela placerait toute l’implémentation dans la même unité de traduction, comme dans les compilations Unity ». Ce serait exact. C’est pourquoi je préfère ne pas utiliser cette solution non plus. J’ai déjà eu affaire à des compilations Unity par le passé et je les considère toujours comme un « hack » qui va à l’encontre des attentes traditionnelles concernant les déclarations static et les espaces de namespace {}.
Des modules presque toujours ?
J’ai donc choisi de toujours compiler ma bibliothèque sous forme de module. De cette manière, je peux placer des déclarations de module dans mes fichiers .cpp sans problème. L’astuce consiste à utiliser extern "C++" de C++20. De la même manière que les noms déclarés avec extern "C" utilisent une liaison C rétrocompatible et un mangling de noms, le fait d’encadrer les déclarations export {} avec extern "C++" génère des symboles utilisant une ABI compatible avec les déclarations #include (par défaut, avec les modules, chaque symbole est préfixé par le nom de son module, ce qui rend impossible sa détection par l’éditeur de liens dans des contextes non modulaires).
1 2 3 4 5 6 7 8
| export module renderer;
// Don't mangle as a module for backward compatibility with non modules includes
extern "C++"
{
export {
#include "renderer/renderer.h"
}
} |
De cette manière, la bibliothèque n’a pas besoin d’être compilée différemment selon que les utilisateurs recourent à import ou à #include. Cela ne pose évidemment problème que pour les bibliothèques qui produisent des symboles exportés. Les bibliothèques composées uniquement d’en-têtes n’ont pas à s’en soucier.
Le fait de n’avoir qu’une seule version de compilation signifie que la bibliothèque n’utilise plus sa propre variante #include. Il est conseillé de conserver quelques tests utilisant la bibliothèque à la fois via le chemin import et via le chemin #include tant que vous prenez en charge les deux (ce qui, je suppose, prendra un certain temps compte tenu du rythme d’adoption des modules).
Alors, dois-je utiliser les modules ?
Le passage aux modules implique un coût initial important. Devoir faire passer toutes vos dépendances aux modules représente une charge de travail non négligeable et, malheureusement, le soutien des responsables de bibliothèques est pour l’instant limité. Même ceux qui déclarent utiliser les modules semblent, pour l’instant, recourir à des fourches de leurs bibliothèques tierces. Je ne sais pas s’ils n’ont pas eu envie de contribuer ou de maintenir des correctifs, ou s’ils ont soumis des correctifs qui ont été rejetés, mais cela n’est pas très encourageant. Les sondages de Meeting C++ ne montrent pas un taux d’adoption élevé pour une fonctionnalité vieille de six ans. Il s’agit peut-être d’un problème de l’œuf et de la poule (personne ne passe aux modules en raison du manque de prise en charge des bibliothèques, et les responsables de bibliothèques ne s’en préoccupent pas en raison du manque d’utilisateurs des modules).
J’envisage de proposer des correctifs pour les bibliothèques que j’utilise, mais j’avoue que même après avoir écrit cet article, je ressens encore un peu le syndrome de l’imposteur et je me demande si ma contribution serait utile. Il y a si peu d’expertise, d’expérience et de documentation sur les modules qu’il n’est pas évident de savoir ce qui constitue ou non une bonne pratique. J’ai compris le fonctionnement des nouveaux mots-clés principalement par essais et erreurs, ce qui me fait penser que la plupart des projets ne disposeront pas d’un relecteur qualifié pour évaluer la qualité d’un correctif proposé.
En attendant, la solution de facilité consiste à faire comme je l’ai fait au départ avec VulkanHpp et à limiter l’utilisation des modules aux bibliothèques qui sont lourdes à analyser mais faciles à placer en fin de chemin #include/import pour un gain rapide ; malheureusement, cela s’effondre rapidement à grande échelle en raison du facteur viral.
Addendum : Jens Weller m’a signalé l’existence de « Are We Modules Yet ? », un site web qui répertorie les projets proposant des modules. Curieusement, fastgltf fournit bien un module, mais celui-ci n’est ni compilé ni installé par vckpg, ce qui explique que je ne l’aie pas vu. Je pense que les bibliothèques devraient toujours ajouter les définitions de modules à leur liste d’installation plutôt que de les dissimuler derrière un paramètre de compilation, afin que cela ne devienne pas un problème pour le gestionnaire de paquets.
Licensed under CC BY-NC-SA 4.0
Source : Can we finally use C++ Modules in 2026?
Et vous ?
Quel est votre avis sur le sujet ?
Voir aussi :
C++26 : Concept et templates variables comme paramètre template, par Sandor Dargo
Le C++ doit être du C++ : Une opinion qui est le fruit de 23 années d'expérience en C++, par David Sankel
C++26 est prêt : la nouvelle norme apporte des améliorations en matière de sécurité mémoire et serait la version « la plus aboutie » depuis C++11. Elle introduit les contrats et la bibliothèque std::execution
Partager