Comprendre enable_if

Cet article porte sur un outil standard depuis C++ 11, mais qui a été mis de l'avant par les gens de Boost (voir ceci pour des détails) et pouvait par conséquent être implémenté avec C++ 03.

Pour bien saisir les subtilités dans ce qui suit, mieux vaut vous familiariser tout d'abord avec les templatesSFINAE, la programmation générique et les traits.

Le trait enable_if est une technique permettant de contrôler l'application de SFINAE. L'idée générale va comme suit :

Si nous souhaitons par exemple qu'une fonction f() ne soit utilisable que pour un type T qui soit un POD (Plain Old Datatype), donc un type tel que ceux que l'on retrouve en langage C. On peut exprimer cette contrainte à l'aide du trait enable_if (et du trait std::is_pod<T>::value) de l'une des deux manières suivantes (et il y en a d'autres, si jamais vous souhaitez explorer) :

enable_if pour type retourné enable_if pour paramètre silencieux
#include <vector>
#include <type_traits>
template <class T>
   typename std::enable_if<std::is_pod<T>::value>::type f(T) {
      // ...
   }
int main() {
   using std::vector;
   vector<int> v;
   f(3);
   // f(v); // serait illégal
}
#include <vector>
#include <type_traits>
template <class T>
   void f(T, typename std::enable_if<std::is_pod<T>::value>::type * = nullptr) {
      // ...
   }
int main() {
   using std::vector;
   vector<int> v;
   f(3);
   // f(v); // serait illégal
}

Depuis C++ 14, pour alléger l'écriture, il est possible de remplacer typename std::enable_if<...>::type par std::enable_if_t<...> ce qui allège beaucoup l'écriture. C'est cette approche que nous allons suivre ici. Par exemple :

enable_if pour type retourné enable_if pour paramètre silencieux
#include <vector>
#include <type_traits>
template <class T>
   std::enable_if_t<std::is_pod<T>::value> f(T) {
      // ...
   }
int main() {
   using std::vector;
   vector<int> v;
   f(3);
   // f(v); // serait illégal
}
#include <vector>
#include <type_traits>
template <class T>
   void f(T, std::enable_if_t<std::is_pod<T>::value>* = nullptr) {
      // ...
   }
int main() {
   using std::vector;
   vector<int> v;
   f(3);
   // f(v); // serait illégal
}

Depuis C++ 17, pour alléger l'écriture, il est possible de remplacer les traits qui exposent une constante ::value par un équivalent portant le suffixe _v, encore une fois pour alléger l'écriture. Nous suivrons aussi cette approche. Par exemple :

enable_if pour type retourné enable_if pour paramètre silencieux
#include <vector>
#include <type_traits>
template <class T>
   std::enable_if_t<std::is_pod_v<T>> f(T) {
      // ...
   }
int main() {
   using std::vector;
   vector<int> v;
   f(3);
   // f(v); // serait illégal
}
#include <vector>
#include <type_traits>
template <class T>
   void f(T, std::enable_if_t<std::is_pod_v<T>>* = nullptr) {
      // ...
   }
int main() {
   using std::vector;
   vector<int> v;
   f(3);
   // f(v); // serait illégal
}

Dans certains cas, la version avec enable_if comme valeur de retour est préférable, étant plus simple à écrire et ne dénaturant pas la signature de la fonction. Dans d'autres cas, comme par exemple celui des constructeurs, qui n'ont pas de type de retour, la version avec paramètre silencieux est à privilégier.

Il est possible d'utiliser enable_if à plusieurs sauces. L'exemple ci-dessous l'utilise comme type de retour pour éviter que le compilateur ne puisse générer la fonction f(T) si T n'est pas un type entier (j'utilise ici le trait std::is_integral<T> pour déterminer cet état) :

#include <type_traits>
template <class T>
   std::enable_if_t<std::is_integral_v<T>> f(T) {
      // ...
   }
class X {};
int main() {
   // f(X{}); // ne compilerait pas
   f(3); // Ok, 3 est un littéral entier
}

L'exemple ci-dessous montre comment il est possible d'utiliser enable_if pour choisir une fonction sur la base de caractéristiques spécifiques à un type :

#include <iostream>
#include <type_traits>
template <class T>
   std::enable_if_t<(sizeof(T) <= sizeof(int))> f(T) {
      std::cout << "f() version petite taille" << std::endl;
   }
template <class T>
   std::enable_if_t<(sizeof(T) > sizeof(int))> f(T) {
      std::cout << "f() version grande taille" << std::endl;
   }
class X { char _ [128]; };
int main() {
   f(X{}); // version « grande taille »
   f(3);   // version « petite taille »
}

Il est aussi possible d'utiliser enable_if pour faire des choix sur la base de paramètres d'instanciation de templates, comme par exemple :

#include <type_traits>

template <class T, class Permis = void>
   class Reel;

template <class T>
   class Reel<T, std::enable_if_t<std::is_floating_point_v<T>>> {
      // ...
   };
template <class R>
   void utiliser(const Reel<R> &) {
      // ...
   }
int main() {
   Reel<double> rd; // Ok
   utiliser(rd);
   Reel<float> rf; // Ok
   utiliser(rf);
   // Reel<int> ri; // ne compile pas
}

Ici, la classe Reel<T> ne pourra être instanciée que dans les cas où T est un type à virgule flottante (un type pour lequel le trait std::is_floating_point<T>::value s'avère).

En 2014, Charles Guerre Chaley m'a fait part, dans un courriel, de cette manière par laquelle il tire profit de std::enable_if (merci!) :

template <class T>
   using siFlottant = std::enable_if_t<std::is_floating_point_v<T>>;
template <class T, class = siFlottant<T>>
   class  Reel {
      // ...
   };

Voilà!

Lectures complémentaires


Valid XHTML 1.0 Transitional

CSS Valide !