🔝 Retour au Sommaire
Chapitre 3 — Types, Variables et Opérateurs · Section 3.1 · Sous-section 2 sur 3
Prérequis : 3.1.1 —auto: Déduction automatique du type
Là où auto demande au compilateur « déduis le type de cette variable à partir de son initialiseur », decltype pose une question différente : « quel est le type exact de cette expression ? ». Et surtout, decltype pose cette question sans évaluer l'expression — il l'analyse uniquement du point de vue du système de types.
Cette distinction peut sembler subtile, mais elle a des conséquences pratiques importantes. auto applique les règles de déduction de template et retire certains qualificateurs (comme const et & de haut niveau). decltype, lui, préserve le type exact de l'expression, y compris les références et les qualificateurs. C'est un outil de précision, indispensable dès que l'on travaille avec des templates, des types de retour complexes ou de la métaprogrammation.
decltype s'utilise avec une expression entre parenthèses :
int x = 42;
decltype(x) y = 10; // y est de type int (le type déclaré de x)
const int cx = 100;
decltype(cx) z = 50; // z est de type const int Contrairement à auto, decltype préserve le const de haut niveau. Dans l'exemple ci-dessus, z est bien const int, alors que auto z = cx; aurait déduit un simple int.
decltype peut également s'appliquer à des expressions plus complexes :
int a = 1, b = 2;
decltype(a + b) sum = 0; // int (le résultat de int + int est int)
std::vector<double> v = {1.0, 2.0, 3.0};
decltype(v.size()) n = 0; // std::vector<double>::size_type (alias de std::size_t) L'expression passée à decltype n'est jamais évaluée. Elle sert uniquement à déterminer le type au moment de la compilation. Vous pouvez donc écrire des expressions qui seraient invalides à l'exécution sans provoquer d'erreur, tant que le type est déductible :
int* ptr = nullptr;
decltype(*ptr) ref = x; // int& — *ptr est une lvalue de type int
// Pas de déréférencement réel de nullptrLe comportement de decltype dépend de la nature de l'expression qui lui est passée. Il y a exactement deux cas à distinguer, et les maîtriser suffit à prédire le résultat dans toutes les situations.
Si l'expression est un nom de variable simple (un identifiant sans parenthèses supplémentaires), decltype retourne le type déclaré de cette variable, tel quel :
int x = 42;
const int cx = 10;
int& rx = x;
decltype(x) // → int (type déclaré de x)
decltype(cx) // → const int (type déclaré de cx)
decltype(rx) // → int& (type déclaré de rx) C'est direct et intuitif : decltype vous donne exactement le type que vous avez écrit dans la déclaration de la variable.
Si l'expression est autre chose qu'un simple identifiant — un appel de fonction, une opération arithmétique, un accès membre, ou même un identifiant entouré de parenthèses supplémentaires — alors decltype analyse la catégorie de valeur de l'expression :
- Si l'expression est une lvalue (elle a une adresse, on peut la placer à gauche d'un
=),decltyperetourne une référence sur le type :T&. - Si l'expression est une xvalue (une rvalue « expirante », typiquement le résultat de
std::move),decltyperetourneT&&. - Si l'expression est une prvalue (une rvalue « pure », comme un littéral ou le résultat d'une opération arithmétique),
decltyperetourneT(sans référence).
Voyons cela en pratique :
int x = 42;
// Expressions lvalue → T&
decltype((x)) // → int& ⚠️ Les parenthèses font de x une expression, pas un identifiant
decltype(*&x) // → int& (déréférencement d'un pointeur = lvalue)
// Expressions prvalue → T
decltype(42) // → int (littéral = prvalue)
decltype(x + 1) // → int (résultat arithmétique = prvalue)
// Expression xvalue → T&&
decltype(std::move(x)) // → int&& (std::move produit une xvalue)La différence entre decltype(x) et decltype((x)) est le piège le plus fréquent de decltype :
int x = 42;
decltype(x) // → int (règle 1 : identifiant → type déclaré)
decltype((x)) // → int& (règle 2 : expression lvalue → référence) x seul est un identifiant : la règle 1 s'applique. (x) est une expression parenthésée : elle n'est plus un simple identifiant, la règle 2 s'applique, et comme x est une lvalue, le résultat est int&.
Cette distinction a des conséquences concrètes, notamment avec decltype(auto) que nous verrons plus loin dans cette section.
| Expression | Catégorie | decltype(...) |
|---|---|---|
x (variable int) |
identifiant | int |
cx (variable const int) |
identifiant | const int |
rx (variable int&) |
identifiant | int& |
(x) |
lvalue | int& |
*ptr |
lvalue | int& |
obj.member (non-identifiant dans un contexte template) |
lvalue | int& |
42 |
prvalue | int |
x + 1 |
prvalue | int |
std::move(x) |
xvalue | int&& |
decltype est particulièrement utile pour capturer le type de retour d'une fonction sans l'appeler :
int compute();
double measure(int sensor_id);
decltype(compute()) // → int
decltype(measure(0)) // → double L'appel de fonction à l'intérieur de decltype n'est pas exécuté — seul le type de retour est extrait. Vous pouvez passer n'importe quelle valeur comme argument ; elle sert uniquement à la résolution de surcharge.
C'est dans les templates que cette capacité devient indispensable. Quand le type de retour dépend des types des paramètres, decltype permet de l'exprimer :
// C++11 : trailing return type avec decltype
template <typename T, typename U>
auto add(T a, U b) -> decltype(a + b) {
return a + b;
}
// add(1, 2.5) → decltype(int + double) → double
// add(1, 2) → decltype(int + int) → intSans decltype, il serait impossible d'écrire cette fonction de manière générique en C++11. Le type de a + b dépend des règles de promotion arithmétique et ne peut pas être exprimé par un simple paramètre de template.
💡 Depuis C++14, la déduction automatique du type de retour (
autosans trailing return type) rend cette syntaxe souvent inutile pour les fonctions simples. Elle reste néanmoins nécessaire lorsque le type de retour doit être exposé dans une déclaration forward ou dans un concept.
C++14 a introduit decltype(auto), une forme hybride qui combine la commodité de auto (déduction à partir de l'initialiseur) avec les règles de decltype (préservation exacte du type, y compris les références).
Considérons une fonction qui encapsule l'accès à un élément d'un conteneur :
std::vector<int> values = {10, 20, 30};
// Avec auto : retourne une copie (auto retire la référence)
auto get_first_v1() {
return values[0]; // Type de retour déduit : int (copie)
}
// Avec decltype(auto) : retourne exactement ce que retourne l'expression
decltype(auto) get_first_v2() {
return values[0]; // Type de retour déduit : int& (référence)
}values[0] retourne un int& (une référence vers l'élément). Avec auto, la référence est retirée et la fonction retourne une copie. Avec decltype(auto), le type est préservé tel quel : la fonction retourne une référence.
decltype(auto) fonctionne aussi pour les variables :
int x = 42;
const int& crx = x;
auto a = crx; // int — auto retire const et &
decltype(auto) b = crx; // const int& — decltype(auto) préserve tout Rappelez-vous la différence entre decltype(x) et decltype((x)). Ce piège se retrouve directement dans decltype(auto) :
int x = 42;
decltype(auto) f1() {
return x; // decltype(x) → int — retourne une copie ✅
}
decltype(auto) f2() {
return (x); // decltype((x)) → int& — retourne une référence ⚠️
}La fonction f2 retourne une référence vers une variable locale si x est locale, ce qui constitue un dangling reference — un bug grave. Les parenthèses supplémentaires, qui semblent anodines, changent complètement la sémantique. C'est une raison pour laquelle decltype(auto) doit être utilisé avec prudence et en connaissance de cause.
decltype(auto) est principalement utile dans deux contextes :
Le perfect forwarding du type de retour. Quand vous écrivez une fonction wrapper qui doit retourner exactement ce que la fonction interne retourne, sans altérer le type :
template <typename F, typename... Args>
decltype(auto) transparent_call(F&& func, Args&&... args) {
return std::forward<F>(func)(std::forward<Args>(args)...);
}Si la fonction interne retourne une référence, le wrapper retourne une référence. Si elle retourne une valeur, le wrapper retourne une valeur. Le type est transmis fidèlement.
La capture fidèle d'un type dans les templates. Dans du code générique où la distinction entre T, T& et const T& a une importance sémantique, decltype(auto) garantit que rien n'est perdu.
Pour le code applicatif courant (en dehors des templates et des wrappers), auto suffit dans la grande majorité des cas.
std::vector<int> data = {1, 2, 3, 4, 5};
decltype(data) backup = data; // backup est std::vector<int> C'est utile quand le type est long à écrire ou susceptible de changer. Si data devient un std::deque<int>, backup suit automatiquement.
template <typename Container>
class Wrapper {
public:
using value_type = decltype(*std::declval<Container>().begin());
// Extrait le type retourné par le déréférencement d'un itérateur
};std::declval<T>() est un utilitaire qui « fabrique » une valeur de type T dans un contexte non évalué (comme decltype). Combiné avec decltype, il permet d'inspecter les types de retour de méthodes sans avoir besoin d'une instance réelle de l'objet.
template <typename Func>
void execute_and_log(Func func) {
using ReturnType = decltype(func());
if constexpr (std::is_void_v<ReturnType>) {
func();
std::print("Fonction exécutée (void)\n");
} else {
ReturnType result = func();
std::print("Résultat : {}\n", result);
}
}Ici, decltype(func()) extrait le type de retour du callable sans l'invoquer, ce qui permet d'adapter le comportement de la fonction via if constexpr selon que le retour est void ou non.
| Aspect | auto |
decltype(expr) |
|---|---|---|
| Source de la déduction | Initialiseur de la variable | Expression explicite |
| Règles appliquées | Déduction de template (paramètre par valeur) | Analyse du type exact de l'expression |
const de haut niveau |
Retiré | Préservé |
| Références | Retirées (sauf auto&, auto&&) |
Préservées selon la catégorie de valeur |
| Évaluation | L'initialiseur est évalué | L'expression n'est pas évaluée |
| Usage principal | Variables locales, boucles, lambdas | Templates, alias de types, types de retour complexes |
La règle de choix est simple : utilisez auto pour la plupart des déclarations de variables. Réservez decltype aux situations où vous avez besoin du type exact d'une expression, notamment dans le code générique.
decltype est l'outil de réflexion de type (type introspection) du C++ à la compilation. Il répond à la question « quel type a cette expression ? » avec une fidélité absolue, en préservant les qualificateurs et les références que auto retire par défaut. Combiné avec decltype(auto) (C++14), il permet de propager fidèlement les types à travers des couches d'abstraction.
Pour le développeur débutant, decltype sera moins fréquemment utilisé que auto — et c'est normal. Mais dès que vous aborderez les templates (chapitre 16) ou la métaprogrammation, il deviendra un compagnon indispensable. L'important à ce stade est de comprendre ses deux règles fondamentales (identifiant vs expression) et le piège des parenthèses.