Architecture logicielle & Développement

Le futur standard C++ : concepts | 0 vote(s)

Le système de type du langage C++ s'enrichit d'une nouveauté déroutante: la notion de concepts (que l'on retrouve déjà dans certains langages fonctionnels) est en passe d'être intégrée au coeur du langage - pour le meilleur et pour le meilleur.

A l'origine de cette proposition, on retrouve le créateur même du langage - Bjarne Stroustrup - aidé de Douglas Gregor. Le but des concepts est de supporter la programmation générique à base de templates de manière à la rendre plus explicite. Dans l'idée, plutôt que de dire que telle ou telle classe template peut être paramétrée avec un type répondant à telle ou telle contrainte sémantique, il est préférable d'expliciter ces contraintes directement dans le langage de programmation. Le compilateur et le programmeurs peuvent alors s'y référer pour vérifier que - par exemple - il est possible de créer un vecteur d'objets d'une classe quelconque.

Outre cette possibilité, les concepts permettent au programmeur de définir de nouvelles contraintes qui pourront être utilisées par le compilateur dans le but de vérifier la validité du code.

L'existant

Prenons un exemple: le standard C++ définit des contraintes sémantique sous la forme d'un texte (et quelques fois d'une expression). La table 64 (section 23.1 §4) définit ainsi la contrainte Assignable de cette manière :

       Table 64: Assignable requirements
expression     return type    post-condition
t = u          T&             t is equivalent to u

Il définit ensuite un ensemble de contraintes sur les conteneurs de la librairie standard - ainsi, chaque type de conteneur X<T> doit proposer un type X<T>::value_type, dont le type de retour est T, et qui doit satisfaire à la contrainte Assignable. Selon le conteneur, d'autres contraintes peuvent être ajoutées (les plus courantes étant Assignable, DefaultConstructible et CopyConstructible).

Le problème de la version 2003 du langage C++ est que ces contraintes sémantiques ne peuvent être exprimées que dans le texte de la norme - elles ne peuvent pas être représentées par du code, faute de fournir les outils adéquats. De fait, il est impossible pour le compilateur de vérifier si toutes les contraintes qui doivent être repspectées par un type le sont réellement. On remarque ainsi que la classe std::auto_ptr<> réponds aux exigences d'un std::vector<>. Pourtant, tout ceux qui ont essayé de faire un std::vector<std::auto_ptr<> > savent qu'il ne vaut mieux pas jouer à ça: encore un problème de sémantique.

De plus, si le problème n'est pas particulièrement génant pour les classes définies par la librairie standard[1], il le devient nettement plus lorsqu'on s'attaque aux classes templates définies par un vendeur tierce partie et intégrées dans son middleware, ou la documentation peut être moins fournie et le code plus obscur.

Les concepts - proposition N2741 (pdf)

Le point fort des concepts, c'est justement de formaliser ces contraintes sémantiques textuelles afin de pouvoir les intégrer directement sous la forme de code. Dans la prochaine version du standard C++, un concept est une entité qui défini les contraintes d'interfaces que doit satisfaire un type donné.

La grammaire simplifiée permettant la définition d'un concept est relativement simple[2]:

<concept-definition>        ::= <auto-decl-opt> concept <identifier> < <template-param-list> >
                                <refinement-clause-opt> <concept-body> ;
<auto-decl-opt>             ::= auto | E
<refinement-clause-opt>     ::= : <refinement-specifier-list> | E
<refinement-specifier-list> ::= <refinement-specifier> , <refinement-specifier-list> 
                                | <refinement-specifier>
<refinement-specifier>      ::= <concept-id>

Notes: <refinement-specifier> est en fait un poil plus complexe que ça, et le ';' à la fin de la définition est optionnel. <concept-body> spécifie l'interface que le concept doit contraindre. Pour en savoir plus sur la grammaire exacte, je vous conseille de vous reporter au document N2741 (ou une version ultérieure de celui-ci).

Quelques exemples simple, afin de mieux vous présenter le principe:

concept DefaultConstructible<typename T>
{ T::T(); } // T doit posséder un constructeur par défaut.
concept CopyConstructible<typename T>
{ T::T(const T&);}  // T doit avoir un constructeur par copie
concept Destructible<typename T>
{ T::~T(); } // T doit avoir un destructeur public
concept Assignable<typename T>
{ T& operator=(const T&);}  // T doit définir un opérateur d'assignation
concept MyConcept<typename T>
: DefaultConstructible<T>
, CopyConstructible<T>
, Destructible<T>
, Assignable<T>
{ }

Ici, on définit quatre concepts - qui représentent les contraintes sémantiques de base. Le cinquième (MyConcept<>) est construit à partir des 4 précédents - la classe paramètre T doit alors respecter les concepts associés.associés.

Certes, cette notation est intéressante dans le sens ou elle permet de noter une bonne partie des contraintes sémantiques sous la forme de code. Mais noter ces contraintes sans les utiliser n'est pas d'un intérêt particulièrement probant.

concept et concept_map

Avant de donner un coup de pied dans la fourmilière des templates et de voir comment les concepts influe sur leur écriture, il est encore nécessaire d'introduire une notion qui leur est associée : la notion de correspondance des concepts - les concept_map.

Pour paraphraser N2741, une concept_map décrit la façon dont un ensemble d'arguments template satisfont les contraintes définies dans le corps d'un concept. Si on simplifie à l'extrème, une concept_map met en relation un type T avec d'autres constructions d'un programme (autre type, fonction, ...) de manière à faire croire au compilateur que T respecte les contraintes fixées par un concept donné.

Prenons un example:

// un concept nécessaire utilisé par une fonction template
// dans une librairie A
concept equality_comparable<typename T>
{
  // je dois pouvoir vérifier si deux instances sont égales.
  bool operator==(T, T);
}
// une classe quelconque, définie il y a des années dans une librairie B,
// interdiction d'effectuer des modifications.
struct student
{
   std::string id;
   std::string name;
};

La classe student ne satisfait pas la contrainte définie par le concept equality_comparable. Hors, pour une raison quelconque, j'ai besoin d'effectuer cette correspondance - de manière à utiliser la fonction template de la librairie A qui m'intéresse. Sans la possibilité d'informer le compilateur qu'une correspondance est possible, je suis bloqué. Fort heureusement, les concept_map viennent à la rescousse :

concept_map equality_comparable<student>
{
   bool operator==(const student& a, const student& b)
   { return a.id == b.id; }
}

Je peux maintenant utiliser ma fonction template, la concept_map va se charger de dire au compilateur que ma classe student vérifie bien les contraintes imposées par le concept equality_comparable<>.

Les concept_map sont le chaînon nécessaire entre un concept C et une fonction ou une classe template utiilsant ce concept. Si le compilateur peut prouver que pour un type paramètre T satisfait les contraintes de C, les concept_map associés à C<T> seront implicite. Dans le cas contraire, le programmeur doit les expliciter.

Et pour finir: l'utilisation des concepts

Le futur standard prévoie deux notations qui sont équivalentes[3] pour définir les fonctions et classes template qui sont limités par des concepts.

La première notation prends place dans la liste des paramètres. Dans la définition suivante, on contraint le type paramètre T à satisfaire aux contraintes du concept C au lieu d'être un type non contraint.

template <class T> class c { ... };     -->    template <C T> class c { ... };

La seconde notation est placée après la définition de la liste des paramètres template et prends la forme d'une liste de tests précédés par une clause requires:

template <class T> requires C<T> class c { ... };

On peut aussi définir des fonctions et des fonctions membres contraintes par un concept particulier:

template <C T>  void f(T& t) { ... }
template <class T> requires C<T> void f(T& t) { ... }

Enfin, les contraintes appliquées à une fonction membre d'une classe n'ont pas besoin d'être appliquée à tous les membres de cette classe template:

template <class T> class c
{
public:
  requires C<T> void f(T& t);
};

Si la première notation est plus concise, la seconde notation a l'avantage d'être plus explicite et permet d'être sûr de ne pas confondre un concept avec un nom de type (puisque - vous le savez - les paramètres template peuvent être typés). Au final, c'est à vous de choisir la notation qui vous convient le mieux.

Notes

[1] le volume de documentation disponible permet de rapidement comprendre son erreur dans la plupart des cas.

[2] rappel: E est la production vide.

[3] en tout cas, c'est l'impression que j'ai. Il est possible qu'une subtilité m'ai échappé pendant cette étude et que j'ai fait une erreur. Si tel est le cas et que vous vous en rendez compte, n'hésitez pas à m'en faire part afin que je corrige ce texte. Merci d'avance !

Trackbacks

Aucun trackback.

Les trackbacks pour ce billet sont fermés.

Commentaires

1. Le jeudi 28 août 2008 à 04:13, par Aszarsha

Gravatar

Merci pour l'article. Ça fait plaisir de voir le retour du C++0x sur le blog. :)

Quand aux deux notations, elle ne sont pas équivalentes. La forme avec requires est plus générale, et permet la négation pouvant donner lieu à de la spécialisation.

requires !equality_comparable<T>

2. Le jeudi 28 août 2008 à 17:24, par Emmanuel Deloget

Gravatar

Evidemment ! Pourtant, je l'avais vu cette opportunité. Au moment de rédiger, je l'ai complètement mis de coté (et j'ai même reparcouru le draft, tellement ça me semblait bizarre de ne pas voir de différences notables...)

Merci à toi pour cette précision !

Ajouter un commentaire

Si votre navigateur est compatible, vous pouvez vous aider de la barre d'outils placée au-dessus de la zone de saisie pour enrichir vos commentaires.