11 janv. 2008

Le futur standard C++ : les expressions constantes

Il y a quelques mois, je vous parlais des classes d'énumération telles qu'elles seront intégrées dans le standard C++0x. Le même article de Herb Sutter auquel ce billet faisait référence nous indique qu'une foultitude d'autres fonctionnalités ont été votées dans le draft du futur standard - parmi lesquelles, les expressions constantes.

L'existant

Il y a quelque chose d'étrange avec le standard C++ courant (amendé, je le rappelle, en 2003). Prenons la fonctions suivante:

const int array_cells = 10;
template <class T> int my_array_size(const T* t) { return sizeof(T) * array_cells; }

sizeof(T) est une constante connue au moment de la compilation, de même que array_cells. Par conséquent, la valeur de retour de la fonction @my_array_size()@ pour un type donné est une constante connue au moment de la compilation. Je suis donc en droit de me dire que cette constante peut être utilisée pendant la compilation.

Bien évidemment, vous savez que non : le compilateur C++ considère que la valeur de retour d'une fonction n'est connue qu'à l'exécution - il est impossible de se servir de celle ci comme d'une constante. Pourtant, cette possibilité nous serait fort utile, notamment pour éviter l'utilisation de macros. Si on reprend l'exemple précédent, on se retrouve à définir une macro qui effectue un traitement similaire:

#define MY_ARRAY_SIZE(array_ptr)  (sizeof((array_ptr)0)*array_cells)

Pour un langage qui vous demande d'éviter les macros au maximum, c'est un peu dérangeant...

La généralisation des expressions constantes offrirait de multiples possibilités. Quelques exemples :

  • la déclaration de tableaux de taille fixe, mais dont la taille est calculée au moment de la compilation.
  • la simplification de code dans certain template (par exemple, dans la plupart des cas géré avec une technique SFINAE[1], au lieu de d'écrire sizeof(func(param)) == sizeof(type_yes), on peut directement écrire func(param) == a_value).
  • aider le compilateur a effectuer certaines optimisation: on sait par exemple que if (expr_const) { bloc_1(); } else { bloc_2(); } est remplacé au moment de la phase d'optimisation par { bloc_1(); } ou { bloc_2(); } suivant la valeur de expr_const. Pouvoir déporter la définition de expr_const et la paramétrer en fonction d'autres constantes serait un plus non négligeable dans les cas ou cette expression est complexe.

Il y a bien évidemment d'autres cas ou cette fonctionnalité nous serait bien utile - je vous laisse le soin de les découvrir par vous même.

Les expressions constantes - propositions N2235 et N2349 (pdf)

Partant de ce constat (et d'autres), Gabriel Dos Reis, Bjarne Stroustrup et Jens Maurer ont proposé dans N2235 la notion d'expression constante généralisées. Pour ce faire, ils introduisent dans le langage C++ un nouveau mot-clef - constexpr - qui est un modificateur (comme const, volatile).

Les expressions constantes généralisées peuvent être séparées en deux groupes :

  • expressions constantes sous forme de fonctions: elles sont définies comme étant des fonctions simple (la fonction ne doit comporter qu'une seule instruction, et celle-ci doit être une instruction return). La fonction doit de fait avoir un type de retour qui n'est pas void. Elle doit être déclarée et définie avant d'être utilisée dans une autre expression constante - ce qui fait d'ailleurs qu'une fonction expression constante ne peut pas faire référence à elle même, interdisant de fait la récursion. Enfin, elle est déclarée avec le nouveau mot-clef constexpr.

    D'un point de vue conceptuel, il faut voir les fonctions expressions constantes comme des expressions constantes paramétrables. Bien sûr, elles sont plus que cela, mais cette idée simples fournit déjà de nombreux renseignements quand à ce qui est autorisé ou non.
// sample constant expression function
constexpr float square(float x)
{
  return x * x;
}
  • expressions constantes sous forme de valeurs: une valeur expression constante est une variable ou une variable membre déclarée avec le modificateur constexpr. Elle doit être initialisée avec une expression constante ou une rvalue construite par un constructeur d'expression constante dont les arguments sont des expressions constantes. Tout comme une variable const, le compilateur n'a pas besoin d'assigner une zone mémoire spécifique pour une valeur d'expression constante à moins d'y être forcé - en prenant par exemple l'adresse de la valeur expression constante.
// sample constant expression value
const float mass = 5.0f;
constexpr float acceleration = mass * square(9.81f);

Je vois d'ici la lueur d'incompréhension qui anime votre regard. Quoi, expression constante sous forme de valeur peut être une variable membre d'une classe ? Et qu'est-ce donc qu'un constructeur d'expression constante ?

C'est la beauté de la proposition de Dos Reis & Stroustrup : en généralisant la notion d'expression constante, ils créent du même coup la notion de valeur littérale pour les types utilisateurs - alors que jusque là, cette notion était réservée aux types de base du langage C++. N2235 définit un littéral défini par l'utilisateur (user-defined litteral) comme étant une instance d'objet créée avec un constructeur expression constante et des arguments qui sont eux-aussi des expressions constantes. Pour être valide, un constructeur expression constante doit répondre à un cahier des charges précis :

  • il est déclaré avec le modificateur constexpr
  • son corps est vide
  • sa liste d'initialisation ne fait intervenir que des expressions constantes
// sample const expression constructor
class complex
{
public:
  constexpr complex(float r, float i) : m_r(r), m_i(i) { }
  // we also define 2 other constant expression functions
  constexpr float real() { return m_r; }
  constexpr float imaginary() { return m_i; }
private:
  float m_r;
  float m_i;
};
// sample use constexpr complex I(0.0f, 1.0f); constexpr complex One(1.0f, 0.0f);
// sample constexpr variable constexpr float i_imag = I.imaginary();

Conclusion

La notion d'expression constante va plus loin qu'il n'y parait: la proposition N2235 indique que si le constructeur par copie d'une classe ayant un constructeur d'expression constante est utilisé pour copier un user-defined litteral, alors la copie est aussi un user-defined litteral. De fait, il devient possible de créer des expressions constantes très complexes se basant sur des types utilisateurs. Par exemple :

 // both parameters are copied using the trivial copy constructor
 constexpr complex operator+(complex a, complex b)
 {
   // return is returning a copy of a newly created user-defined litteral
   return complex(a.real() + b.real(), a.imaginary() + b.imaginary());
 }
constexpr complex One_I = One + I;

Mais les plus gros changements se situent dans la librairie standard du C++. Prenons par exemple les classes std::numeric_limits<>: la proposition N2349 change le texte du standard pour faire en sorte que toutes les fonctions de cette classe template soient des fonctions expression constante. std::numeric_limits<int>::max() est (enfin) une constante évaluée au moment de la compilation. Un bon nombre d'autres modifications sont proposée (std::array<T,N>::size() est constexpr, les opérateurs qui prennent des std::ios_base::fmtflags aussi, la classe std::complex<> est un user-defined litteral, etc.). Il s'agit d'un changement en profondeur dont le but est de maximiser la portée des expressions constantes dans tout le standard.

Notes

[1] dans un template, on effectue une substitution de type qui force la sélection d'une méthode particulière parmi une liste de méthodes. Le but est ensuite de détecter quelle méthode a été sélectionnée. J'ai déjà discuté de cette technique dans ce billet. Oui, le billet en question comporte un grand nombre d'inexactitude, mais la description de SFINAE est correcte quand même.

Commentaires

1. Le vendredi, février 22 2008, 17:07 par Davidbrcz

Excellent.
Comme le reste du blog d'ailleurs.

2. Le mardi, février 26 2008, 10:55 par Emmanuel Deloget

Merci !

Maintenant, je vais aller me cacher, parce trop d'honneurs pourrait avoir un néfaste effet sur la taille de mes chevilles... :)

Ajouter un commentaire

Les commentaires peuvent être formatés en utilisant une syntaxe wiki simplifiée.

Fil des commentaires de ce billet