I. decltype▲
Le mot-clé decltype
, introduit dans C++11, renvoie essentiellement le type d’une expression ou d’une entité. Pour être conforme aux autres conventions de nommage, il aurait probablement dû s'appeler typeof, mais de nombreux compilateurs avaient déjà introduits des extensions portant ce nom. Le nom decltype
a donc été utilisé pour éviter des incompatibilités.
Donc, decltype
« retourne » un type. Il peut donc être utilisé là où l’on a besoin :
struct
X {
int
i;
double
bar(short
);
}
;
X x;
decltype
(x) y; //y has type X;
std::
vector<
decltype
(x.i)>
vi; //vector<int>
using
memberFunctionPointer =
decltype
(&
X::
bar); //double X::(*)(short)
auto
lam =
[&
]() ->
decltype
(y) {
return
y; }
; //decltype(y) is const X&
I-A. Que retourne decltype ?▲
Mais quel type renvoie decltype
exactement ? Nous allons simplifier un peu la description. Si vous souhaitez plus de précisions, consultez la documentation de référence (https://en.cppreference.com/w/cpp/language/decltype).
Si ce que nous passons à decltype
est le nom d’une variable (decltype
(x) dans l’exemple précédent), ou d'une fonction, ou le membre d’un objet (decltype
x.i), alors il retourne le type de ce que l’on a passé. Comme montré dans l’exemple decltype
(y) ci-dessus, cela peut aussi inclure les spécificateurs de référence &
, const
et volatile
.
Un cas particulier à noter concerne l’utilisation des liaisons structurées (structured bindings) du C++17. Si le nom que l’on passe à decltype
est celui d’une variable définie dans une liaison structurée, alors c'est le type de l’élément lié qui est retourné. Par exemple :
std::
pair<
int
volatile
&&
, double
&>
f(int
);
auto
const
&
[a, b] =
f(22
);
Bien que le type de a soit int
const
volatile
&
, decltype
(a) renverra int
volatile
&&
, car c’est le type du premier élément de la valeur de retour de f. De même, decltype
(b) renverra double
&
, et non pas double
const
&
.
Si l’expression passée à decltype
n’est pas un nom ou une expression d’accès à un membre, le type retourné dépendra de la catégorie de valeur de l’expression. Disons que, si le type de l’expression e est E, alors decltype
(e) sera :
- E, si e est une prvalue ;
- E
&
, si e est une lvalue ; - E
&&
, si e est une xvalue.
Par exemple, decltype
(&
X::
bar) ci-dessus n’est qu’un pointeur vers une fonction membre, et non une référence, car l’opérateur address-of retourne une prvalue.
Ces règles peuvent sembler compliquées, mais elles ne font, pour l'essentiel, que ce que l’on peut attendre intuitivement – à l’exception mentionnée de l’utilisation des liaisons structurées, et le fait que mettre une expression entre parenthèses en fait un lvalue. Cela signifie que quand x est une variable de type X, alors decltype
((x)) retournera X&
tandis que decltype
(x) retourne X.
I-B. Cas d’application▲
Un des exemples typiques de l’utilisation de decltype
en C++11 était de déterminer le type de retour d’une fonction template qui retourne une expression qui dépend des paramètres templates. Un exemple typique est la simple addition : additionner deux valeurs de types potentiellement différents peut produire un résultat de n’importe quel type, en particulier si vous jouez avec des surcharges d’opérateurs.
Par exemple, ajouter un int
à un char
const
*
retourne un char
const
*
. Ajouter un std::
string à un char
const
*
donnera un std::
string. Additionner un Sucre et un ReservoirEssence vous retournera probablement un Moteur volatile
.
template
<
class
T, class
U>
auto
add(T const
&
t, U const
&
u) ->
decltype
(t+
u) {
return
t+
u;
}
Heureusement, en C++14, il existe un mécanisme de déduction du type de retour des fonctions, ce qui permet de laisser au compilateur faire ce travail, et de ne plus avoir à utiliser decltype
.
Mais, également en C++14, nous avons vu arriver les lambdas génériques. Ce sont grosso modo des lambdas avec un opérateur d’appel template, mais sans avoir besoin d'en déclarer les paramètres. Travailler avec le type de ce qui peut être passé en paramètre à la lambda demande l’utilisation de decltype
:
auto
make_multiples =
[](auto
const
&
x, std::
size_t n) {
return
std::
vector<
std::
decay_t<
decltype
(x)>>
(n, x);
}
;
Ici, std::
decay_t va retirer le const
&
du type renvoyé par decltype
, car decltype
(x) ne renverrait pas ce qui aurait été T dans un template, mais ce qu’aurait été T const
&
.
I-C. decltype n’exécute rien▲
L’expression que nous passons à decltype
n’est pas exécutée. Ce qui signifie que ça ne coûte rien à l'exécution et qu'il n'y a pas d'effets de bord. Par exemple, decltype
(std::
cout <<
"Hello world!
\n
"
) retournera std::
ostream&
, mais aucun caractère ne s’affichera dans la console.
Lorsque nous appelons des fonctions, les types impliqués, principalement les types de retour, doivent être définis. Il est toutefois possible de déclarer une fonction avec un type de retour incomplet, en utilisant des déclarations anticipées (forward declaration). decltype
est cohérent de ce point de vue en ce qu’il peut être utilisé sur de telles fonctions sans avoir besoin de définir le type de retour. Après tout, nous savons que ce type existe, et c’est tout qui importe au compilateur.
class
Foo; //déclaration anticipée
Foo f(int
); //ok. Foo est toujours incomplet
using
f_result =
decltype
(f(11
)); //f_result est Foo
II. std::declval▲
Dans certains cas, les objets nécessaires que nous voulons passer à une expression pour l’évaluer avec decltype
ne sont pas accessibles ou disponibles. Peut-être ne serons nous-même pas en mesure de créer ces objets, par exemple parce que leurs classes n’ont que des constructeurs privés ou protégés.
Considérons le dernier exemple. decltype
(f(11
)) dit « Quel type vais-je récupérer quand j’appelle f avec 11 ? ». Cela veut vraiment dire : « Quel type vais-je récupérer quand j’appelle f avec n’importe quel entier ? ». Dans le cas d’un entier, nous pouvons utiliser un initialisé par défaut. Mais le constructeur par défaut n’est pas toujours accessible.
Dans ce cas, std::
declval peut nous aider. Il s’agit simplement d’une fonction template qui retourne une rvalue reference de ce que nous lui passons en paramètre. Ainsi, nous n'avons pas besoin de déclarer une fonction mal nommée, juste pour avoir quelque chose d’utilisable avec decltype
: decltype
(f(std::
declval<
int
>
())) suffit.
C’est particulièrement appréciable dans un contexte de template si la valeur que vous cherchez à obtenir dépend des paramètres du template. Regardez ci-dessous le genre d’alias que l’on peut obtenir lors de l’addition de deux types :
template
<
typename
T, typename
U>
using
sum_t =
decltype
(std::
declval<
T>
() +
std::
declval<
U>
());
Il faut le lire comme « sum_t est le type que j’obtiens lorsque j’additionne un truc T avec un machin U »/ Notez également que ni T ni U n'ont besoin d’être intégralement définis lors de l’instanciation du template, car l’expression à l’intérieur de decltype
n’est jamais évaluée.
III. Conclusion▲
C’était un sujet un peu technique, et si ce n’est pas votre travail d’écrire des bibliothèques génériques ou du code utilisant extensivement des templates, il est probable que vous n’ayez pas à vous en servir. Cependant, il est fort possible que vous le croisiez de temps et temps, et pour les adeptes des templates, ces deux fonctionnalités sont du pain bénit.
IV. Remerciements▲
Nous remercions Arne Mertz qui nous a aimablement autorisé à traduire ce tutoriel. Nos remerciements également à nouanda pour la traduction, Winjerome et Lolo78 pour la relecture technique et la mise au gabarit DVP, Jacques Jean pour la correction orthographique.