A quoi sert if constexpr en C++ 17 ?

WOCinTech Chat Stock Images @ Microsoft NYC

C++ 17 a fourni un lot de nouveautés, dont une très utile lors de la compilation, if constexpr, pour faciliter la vie des développeurs. Elle offre plus de possibilités que l’utilisation des #if et autres #ifdef.

Template variadique utilisant une liste d’initialisation avec inférence de type

Prenons l’exemple d’une fonction template qui fournit un vecteur à partir d’un nombre variable de paramètres. Elle peut s’écrire ainsi :

template <typename T1,typename... T> vector<T1> init(T... args) {
    vector<T1> v;
    for (auto x : { args... }) {
        v.push_back(x * x);
    }
    return v;
}

Cette fonction peut être utilisée ainsi, sans aucun problème :

    auto v1 = init<int>(1, 2, 3);

Malheureusement, si on appelle la fonction sans aucun paramètre, le compilateur signale une erreur sur la 3ième ligne de la fonction.

auto v2 = init<int>();

Que se passe t-il ? Lors de l’instanciation du template, le compilateur génère une ligne de la forme :

for (auto x : { })

C’est une instruction invalide, puisqu’auto n’est pas capable de déterminer un type à partir d’une liste vide.
Pour éviter ce problème, on pense immédiatement à effectuer un test sur le nombre d’arguments effectivement passés en utilisant sizeof…(). Mais comment ?

Utilisation de #if ou de if ?

Un #if ne sera pas utilisable, car le passage par le préprocesseur intervient avant la génération du code du template par le compilateur.
Et si on emploie une instruction if classique, la compilation échoue toujours.

template <typename T1,typename... T> vector<T1> init(T... args) {
    vector<T1> v;
    if (sizeof...(args) > 0) {
        for (auto x : { args... }) {
            v.push_back(x * x);
        }
    }
    return v;
}

Le développeur sait que cette condition est suffisante et que le code ne sera jamais appelé. Mais pas le compilateur, qui génère toujours l’instruction avec la liste vide lors de l’instanciation du template.

Utilisation de if constrexpr

if constexpr permet de résoudre le problème. Cette instruction permet de définir des conditions évaluées à la compilation. Dans le cas d’un template, elle permet donc d’inclure ou d’exclure des lignes générées par le compilateur. Une réécriture de la fonction init permet donc d’éviter les lignes posant problème.

template <typename T1,typename... T> vector<T1> init(T... args) {
    vector<T1> v;
    if constexpr (sizeof...(args) > 0) {
        for (auto x : { args... }) {
            v.push_back(x * x);
        }
    }
    return v;
}

L’utilisation de init sans paramètres ne pose maintenant plus aucun problème de compilation et la fonction init peut être appelée avec ou sans paramètre.

Cet exemple provient des formations C++ moderne que j’anime régulièrement.