Fonctionnalités C++ modernes – decltype et std::declval

Arne Mertz

Traducteur

Antoine Besnier

decltype et std::declval sont deux fonctionnalités qui vont main dans la main et sont très utiles en métaprogrammation avec des templates et en association avec l’utilisation du mécanisme de déduction de type grâce à auto, par exemple dans des expressions lambda génériques.

Comme beaucoup de fonctionnalités des templates (dont les lambdas génériques font grossièrement partie), decltype et std::declval sont majoritairement utilisés pour le développement de bibliothèques. Cela ne signifie pas que ces fonctionnalités ne soient pas intéressantes ou utiles pour le développement d’applications. Après tout, il arrive à tout le monde d’avoir occasionnellement à écrire sa propre bibliothèque d’utilitaires.

Pour réagir au contenu de ce tutoriel, un espace de dialogue vous est proposé sur le forum. Commentez

- Titre : Fonctionnalités C++ modernes – decltype et std::declval

- Auteur : Arne Mertz

- Traducteur : Antoine Besnier

- Parution : 10 janvier 2019

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2019 Arne Mertz. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. decltype

- I-A. Que retourne decltype ?

- I-B. Cas d’application

- I-C. decltype n’exécute rien

- II. std::declval

- III. Conclusion

- IV. Remerciements

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.