🔝 Retour au Sommaire
Chapitre 3 — Types, Variables et Opérateurs · Section 3.5 · Sous-section 3 sur 3
Prérequis : 3.5.2 —constexpr: Calcul à la compilation
Nous avons vu que constexpr est opportuniste : une fonction constexpr est évaluée à la compilation si le contexte le permet, mais elle peut aussi être appelée au runtime avec des arguments non constants. Cette flexibilité est un atout dans la plupart des cas. Mais elle pose un problème : comment garantir qu'un calcul est toujours effectué à la compilation, sans exception ?
C'est le rôle de consteval, introduit en C++20. Une fonction consteval — aussi appelée immediate function — doit être évaluée à la compilation. Si un appel ne peut pas être résolu au compile-time (parce qu'un argument n'est pas une expression constante, par exemple), le compilateur ne se rabat pas silencieusement sur le runtime : il émet une erreur de compilation.
consteval transforme une intention (« ce calcul devrait être fait à la compilation ») en garantie (« ce calcul sera fait à la compilation, ou le programme ne compilera pas »).
consteval int square(int n) {
return n * n;
}La syntaxe est identique à constexpr — seul le mot-clé change. La différence est entièrement sémantique : consteval interdit tout appel au runtime.
Comparons les deux qualificateurs sur le même exemple :
constexpr int square_cx(int n) { return n * n; }
consteval int square_ce(int n) { return n * n; }
// --- Contexte compile-time : les deux fonctionnent ---
constexpr int a = square_cx(5); // ✅ Compile-time → 25
constexpr int b = square_ce(5); // ✅ Compile-time → 25
// --- Contexte runtime : seul constexpr fonctionne ---
int runtime_val = get_user_input();
int c = square_cx(runtime_val); // ✅ constexpr accepte le runtime
// int d = square_ce(runtime_val); // ❌ Erreur de compilation :
// l'argument n'est pas une expression constanteL'erreur sur square_ce(runtime_val) n'est pas un bug — c'est le comportement voulu. consteval exprime une intention forte : « cette fonction n'a de sens qu'à la compilation, et l'appeler au runtime serait une erreur de conception ».
Une conséquence directe de cette garantie est qu'une fonction consteval ne génère aucun code dans le binaire final. Elle n'existe que dans le compilateur, pendant la phase de compilation. Son résultat est injecté directement dans le code machine comme une constante. Il est impossible de prendre l'adresse d'une fonction consteval ou de la passer comme callback — elle n'a tout simplement pas d'existence runtime.
consteval int cube(int n) { return n * n * n; }
// auto ptr = &cube; // ❌ Erreur : impossible de prendre l'adresse d'une immediate functionL'un des usages les plus naturels de consteval est de forcer la génération de données au compile-time. Avec constexpr, le compilateur peut évaluer au runtime dans certains contextes. Avec consteval, la table est garantie d'être dans le segment de données du binaire :
consteval std::array<uint8_t, 256> generate_crc_table() {
std::array<uint8_t, 256> table{};
for (int i = 0; i < 256; ++i) {
uint8_t crc = static_cast<uint8_t>(i);
for (int j = 0; j < 8; ++j) {
if (crc & 1)
crc = (crc >> 1) ^ 0xA7;
else
crc >>= 1;
}
table[i] = crc;
}
return table;
}
constexpr auto crc_table = generate_crc_table(); // Garanti compile-timeSi quelqu'un modifie ultérieurement le code pour appeler generate_crc_table() dans un contexte runtime (par inadvertance ou lors d'un refactoring), le compilateur refusera — la garantie est préservée.
Convertir des chaînes de caractères en identifiants numériques à la compilation est un pattern courant dans les moteurs de jeux, les systèmes d'événements et les frameworks de sérialisation :
consteval uint32_t string_hash(const char* str) {
uint32_t hash = 2166136261u; // FNV-1a offset basis
while (*str) {
hash ^= static_cast<uint32_t>(*str++);
hash *= 16777619u; // FNV-1a prime
}
return hash;
}
// Les hachages sont résolus à la compilation — aucun calcul au runtime
constexpr auto EVENT_CLICK = string_hash("mouse_click");
constexpr auto EVENT_KEYDOWN = string_hash("key_down");
constexpr auto EVENT_RESIZE = string_hash("window_resize");
void handle_event(uint32_t event_id) {
switch (event_id) {
case string_hash("mouse_click"): // ✅ Compile-time — utilisable dans un case
process_click();
break;
case string_hash("key_down"):
process_keydown();
break;
}
}Avec constexpr, un développeur pourrait accidentellement passer une chaîne construite au runtime à string_hash — le code compilerait et fonctionnerait, mais le hachage serait calculé à chaque appel. Avec consteval, cette erreur est impossible : toute chaîne non constante provoque une erreur de compilation.
consteval permet de rejeter des valeurs invalides avant même l'exécution du programme :
consteval int checked_port(int port) {
if (port < 0 || port > 65535) {
throw "Port hors de la plage valide [0, 65535]"; // Provoque une erreur de compilation
}
return port;
}
constexpr auto http_port = checked_port(80); // ✅ Valide
constexpr auto https_port = checked_port(443); // ✅ Valide
// constexpr auto bad_port = checked_port(99999); // ❌ Erreur de compilationLe throw dans une fonction consteval ne lève jamais réellement d'exception — il n'y a pas de runtime. Si la branche contenant le throw est atteinte pendant l'évaluation compile-time, le compilateur considère l'évaluation comme échouée et produit une erreur. C'est un mécanisme élégant pour encoder des préconditions vérifiables à la compilation.
consteval uint32_t rgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a = 255) {
return (static_cast<uint32_t>(a) << 24)
| (static_cast<uint32_t>(r) << 16)
| (static_cast<uint32_t>(g) << 8)
| static_cast<uint32_t>(b);
}
// Palette résolue à la compilation — zéro instruction au runtime
constexpr auto COLOR_RED = rgba(255, 0, 0);
constexpr auto COLOR_GREEN = rgba(0, 255, 0);
constexpr auto COLOR_BLUE = rgba(0, 0, 255);
constexpr auto COLOR_OVERLAY = rgba(0, 0, 0, 128); // Noir semi-transparent Ici, consteval garantit que les couleurs ne seront jamais calculées au runtime, même si le code évolue. Avec constexpr, la garantie dépendrait du contexte d'appel.
C++23 a introduit if consteval, une construction qui permet de détecter à l'intérieur d'une fonction si l'évaluation a lieu à la compilation ou au runtime. C'est l'équivalent, pour les fonctions, de ce que if constexpr est pour les types :
constexpr double fast_sqrt(double x) {
if consteval {
// Branche compile-time : algorithme lent mais précis
// (pas d'accès aux instructions matérielles à la compilation)
double guess = x / 2.0;
for (int i = 0; i < 100; ++i) {
guess = (guess + x / guess) / 2.0; // Méthode de Newton
}
return guess;
} else {
// Branche runtime : utilise l'instruction matérielle
return std::sqrt(x);
}
}
constexpr double compile_time_result = fast_sqrt(2.0); // Branche compile-time
double runtime_result = fast_sqrt(get_value()); // Branche runtime (std::sqrt) if consteval est particulièrement utile dans les fonctions constexpr qui doivent choisir entre un algorithme adapté à la compilation (sans I/O, sans intrinsics matérielles) et un algorithme optimisé pour le runtime. Sans cette construction, il fallait soit renoncer à constexpr, soit utiliser le même algorithme dans les deux contextes.
if consteval {
// Exécuté si l'évaluation est compile-time
} else {
// Exécuté si l'évaluation est runtime
}
if !consteval {
// Exécuté si l'évaluation est runtime
}Notez l'absence de parenthèses — c'est if consteval, pas if (consteval). La forme inversée if !consteval est un raccourci pour if consteval {} else { ... }.
Les fonctions consteval et constexpr peuvent s'appeler mutuellement, avec des règles précises.
Une fonction constexpr peut appeler une fonction consteval, mais uniquement dans un contexte compile-time. Si la fonction constexpr est évaluée au runtime, l'appel à la fonction consteval provoque une erreur :
consteval int compile_only(int n) { return n * 10; }
constexpr int flexible(int n) {
return compile_only(n); // ✅ si flexible() est évalué au compile-time
// ❌ si flexible() est évalué au runtime
}
constexpr int a = flexible(5); // ✅ Compile-time → compile_only(5) est valide
// int b = flexible(get_input()); // ❌ Runtime → compile_only() ne peut pas être appeléC'est un point de vigilance : ajouter un appel à une fonction consteval dans une fonction constexpr peut casser les appels runtime existants de cette dernière. Le compilateur le signalera.
Dans l'autre sens, il n'y a aucune restriction. Une fonction consteval peut librement appeler des fonctions constexpr, car elle est elle-même toujours évaluée au compile-time :
constexpr int double_it(int n) { return n * 2; }
consteval int quadruple(int n) {
return double_it(double_it(n)); // ✅ Toujours valide
}
constexpr int result = quadruple(3); // 12La question « dois-je utiliser constexpr ou consteval ? » revient fréquemment. Voici les critères de décision :
Utilisez constexpr quand la fonction est utile à la fois au compile-time et au runtime. C'est le cas le plus fréquent — une fonction de calcul pur qui peut être appelée dans les deux contextes :
constexpr int abs(int n) { return n < 0 ? -n : n; }
// Utile à la compilation ET à l'exécutionUtilisez consteval quand l'évaluation runtime serait une erreur ou un gaspillage. Trois critères signalent le besoin de consteval :
-
Le résultat n'a de sens qu'à la compilation — hachage de chaînes littérales, génération d'identifiants symboliques, construction de tables qui doivent être dans le segment
.rodata. -
L'évaluation runtime serait un bug de performance — un calcul coûteux (table de CRC, précalcul d'un sinus) que vous ne voulez absolument pas effectuer au démarrage du programme.
-
La fonction encode des préconditions de compilation — validation de ports, de plages, de formats. Si la valeur n'est pas connue à la compilation, la validation n'a pas lieu d'être dans cette fonction.
| Critère | constexpr |
consteval |
|---|---|---|
| Appel au runtime | ✅ Autorisé | ❌ Interdit |
| Appel au compile-time | ✅ Autorisé | ✅ Obligatoire |
| Génère du code dans le binaire | Potentiellement | Jamais |
| Adresse de la fonction | Possible | Impossible |
| Cas d'usage principal | Fonctions pures flexibles | Calculs qui doivent être compile-time |
Si vous hésitez, commencez par constexpr. C'est le choix le plus flexible et le moins contraignant. Vous pourrez toujours passer à consteval plus tard si vous constatez que la fonction ne devrait jamais être appelée au runtime. L'inverse — transformer un consteval en constexpr — est aussi possible sans casser les appels compile-time existants, mais pourrait introduire des appels runtime inattendus.
Les fonctions consteval sont soumises aux mêmes restrictions que les fonctions constexpr en contexte compile-time (pas d'assembleur inline, pas d'I/O, pas de reinterpret_cast, mémoire dynamique libérée avant la fin de l'évaluation). Elles ont en outre quelques restrictions supplémentaires :
- Impossible de prendre leur adresse. Pas de pointeur de fonction
consteval, pas de passage en callback. - Impossible de les appeler via un pointeur de fonction. Même si vous stockez l'adresse d'une manière détournée, l'appel sera refusé.
- Incompatibles avec certains patterns de dispatch. Si votre architecture repose sur des tableaux de pointeurs de fonctions ou des callbacks polymorphiques,
constevaln'est pas applicable.
Ces restrictions ne sont pas des bugs — elles sont la conséquence logique du fait que les fonctions consteval n'existent pas au runtime.
Pour clore ce chapitre sur les types, variables et opérateurs, voici une vue d'ensemble des trois qualificateurs d'immutabilité et de calcul à la compilation :
Immutabilité Évaluation compile-time
──────────── ───────────────────────
const ✅ Oui ❌ Pas garanti
constexpr ✅ Oui (implicite) ⚡ Si possible (dual)
consteval ✅ Oui (implicite) ✅ Obligatoire
const est votre outil quotidien — appliquez-le à toute variable qui ne doit pas changer, quelle que soit l'origine de sa valeur.
constexpr est votre outil de performance et d'expressivité — appliquez-le aux constantes dont la valeur est connue à la compilation et aux fonctions pures qui bénéficient d'une évaluation anticipée.
consteval est votre outil de garantie — appliquez-le quand l'évaluation au runtime serait une erreur de conception, un bug de performance ou une violation de vos invariants d'architecture.
Ensemble, ces trois qualificateurs vous donnent un contrôle fin sur le moment et la manière dont les calculs sont effectués, de l'exécution pure jusqu'à la compilation pure. C'est l'un des atouts distinctifs du C++ par rapport aux langages qui n'offrent aucun contrôle sur la frontière compile-time / runtime.