Developpez.com - C++
X

Choisissez d'abord la catégorieensuite la rubrique :


C++0x : Les concepts

Par Alp Mestan (Site personnel) (Blog)
 

Cet article introduit l'une des grandes nouveautés de C++0x : les concepts.

               Version PDF (Miroir)   Version hors-ligne (Miroir)

I. Que sont les concepts ?
II. Comment déclarer un concept ?
III. Contraindre sur les fonctions associées
IV. Contraindre sur les types associés et les classes templates Ainsi, voici deux classes dont l'une respecte le concept container, l'autre non.
V. La forme simple de spécification des conditions des types ou classes templates associés
VI. Axiomes
VII. L'association (spécialisation) de concepts
VIII. Affinement de concept
IX. Concepts de support
X. Utilisation des concepts pour les templates contraints
XI. Comparaison : concepts et interfaces
XII. Concepts automatiques


I. Que sont les concepts ?

Les concepts servent à décrire des interfaces abstraites afin de contraindre les paramètres donnés aux fonctions et classes templates. Il s'agit d'imposer des contraintes syntaxiques ou sémantiques sur ces paramètres, quels qu'ils soient.

En ce qui concerne les contraintes syntaxiques, il s'agit par exemple d'obliger un type à posséder une certaine fonction membre, ou alors une certaine fonction libre qui agira sur lui, etc. Cela peut être très utile, dans la mesure où l'on peut aisément écrire un concept Serialisable qui oblige un type donné à posséder une fonction membre to_string, par exemple. De même, on peut contraindre à posséder un certain type, une certaine nested class ou une certaine class template.

Enfin, une contrainte sémantique, c'est une contrainte sur le comportement, le sens d'une classe ou d'un type donné. Par exemple, si l'on définit un opérateur <= pour une classe C représentant les nombres réels, il faudra respecter la transitivité : pour 3 réels a, b et c donnés, si a <= b et b <= c alors a <= c. Vous verrez que l'on peut imposer ce genre de contraintes via ce que l'on appelle les axiomes.

Si vous désirez mettre en application au fur et à mesure de la lecture, consultez l'annexe A afin d'obtenir le nécessaire pour télécharger un compilateur qui supporte les concepts de C++0x : ConceptGCC.


II. Comment déclarer un concept ?

En C++0x, pour déclarer un concept, il faut procéder de la manière suivante.

concept MonConcept<Param1, ..., ParamN>
{
  /* Corps du concept, expliqué plus bas */
}
Les Paramk peuvent être tout ce que l'on peut donner à une fonction ou classe template, donc par exemple typename T, int N, etc.

Imaginez désormais avoir un concept LessComp<T> qui permet de décrire l'interface d'un type tel que l'on puisse classer ses éléments : si l'on a deux objets de ce type, on doit pouvoir dire que l'un est inférieur à l'autre. Imaginez maintenant que l'on veuille d'écrire le concept d'ensemble ordonné. Il serait avantageux de pouvoir exprimer le fait que l'on attend du type des éléments contenu dans l'ensemble ordonné qu'ils puissent être ... ordonnés, ce qui correspond exactement à notre concept LessComp<T>. En C++0x, il est possible de ce faire sans devoir réécrire le corps du concept LessComp, grâce au mot clé requires.

concept OrderedSet<typename T>
{
  requires LessComp<T>;
  /* reste des contraintes */
}

III. Contraindre sur les fonctions associées

Les fonctions associées décrivent les fonctions, fonctions membres ou opérateurs, dépendant des paramètres du concepts, que le compilateur doit pouvoir trouver. Par exemple, si l'on écrit un concept Addable, qui contraint son type à posséder un opérateur binaire + :

concept Addable<typename T>
{
  T operator+(T, T);
}
Ou encore un concept Stringable qui oblige son paramètre à posséder une fonction membre to_string :

concept Stringable<typename T>
{
  std::string T::to_string() const;
}
Dans le premier cas, on demande un opérateur qui n'est pas membre de notre éventuelle classe T. Il s'agit ici d'un opérateur libre. A l'opposé, dans le second cas, on impose une contrainte de présence d'une fonction membre satisfaisant une signature bien précise, le nom compris. Enfin, voyons un exemple mettant en oeuvre 2 paramètres de types au concept.

// conversion de T vers U
concept Convertible<typename T, typename U>
{
  operator U(T);
}
Il est toutefois important de noter qu'il y a des situations qui sont interdites. Une fonction associée ne doit pas être extern, virtual, inline, friend, delete ou default. Une fonction associée ne doit pas non plus contenir de spécification des exceptions qu'elle peut lancer. Le cas particulier des opérateurs est qu'ils ne doivent pas être membres d'un paramètre du concept, à l'exception des opérateurs d'assignement (=), new, new[], delete et delete[], alors que l'on peut contraindre des fonctions membres. Toutefois, notez qu'une fonction associée peut être template, tout comme elle peut être statique. On peut aussi poser des contraintes sur un constructeur ou destructeur d'un paramètre du concept. Un concept peut imposer la présence de plusieurs fonctions du même nom, du moment qu'elles ne rentrent pas en conflit. Il s'agit des mêmes règles que pour la surcharge de fonctions classique. Un petit résumé :

concept C <typename T, typename U>
{
  friend T T::f(); // invalide car 'friend'
  virtual void T::show(); // invalide car 'virtual'
  extern void g(T); // invalide car 'extern'
  T::T(const std::string&) = delete; // invalide car '= delete'
  // etc ...

  T::T(const std::string&); // valide, impose la présence d'un constructeur de T prenant une std::string
  template <typename V, int N> V h(const T&, const U&); // valide, impose la présence d'une fonction libre qui a l'air inutile...
  // etc ...
}
A noter que l'on peut définir des implémentations par défaut pour les fonctions associées.

concept EqualityComparable<typename T>
{
  bool operator==(T,T);
  bool operator!=(T x, T y) { return !(x == y); }
}

class X { };
bool operator==(const X&, const X&) { /* ... */ }

concept_map EqualityComparable<X> { } 
// nous verrons plus tard concept_map, mais ici operator!= pour X utilisera celui déclaré dans EqualityComparable

IV. Contraindre sur les types associés et les classes templates

Dans un concept, on peut poser des contraintes sur des types ou classes templates associés aux paramètres du concept. Concrètement, on voudra donc simplement qu'il y ait tel type défini dans notre paramètre, que ce soit avec un typedef, une classe, etc. Et de même pour l'obligation de présence d'une classe template.

Pour déclarer un type associé, il suffit d'utiliser le mot-clé typename. On peut ensuite baser le reste de notre interface sur le type ainsi demandé.

concept Container <typename T>
{
  typename iterator; // on impose un type associé nommé "iterator"... comme on peut en voir dans std::vector<X>::iterator
  // ...
  iterator T::begin(); // on se sert de notre type associé pour déclarer une fonction associée
}
Ainsi, voici deux classes dont l'une respecte le concept container, l'autre non.

// respecte le concept Container
class my_int_vector
{
  int* vec;
  public:
  typedef int* iterator;
  iterator begin();
  // ...
};

// ne respecte pas le concept Container
class my_other_int_vector
{
  int* vec;
  public:
  int* begin(); 
  // ...
};
}
La seconde classe ne respecte pas le concept étant donné qu'elle ne définit aucun type nommé iterator, que ce soit par un typedef ou via une classe/structure.

Une classe template associée se déclare comme une classe template banale, à l'intérieur du concept.

concept C <typename T>
{
  template <typename U> class X; // contraint la présence d'une classe template à un paramètre de type nommée X
}

V. La forme simple de spécification des conditions des types ou classes templates associés

Imaginez avoir le code suivant.

concept InputIterator <typename T> { /* ... */ }

concept Container <typename X>
{
  typename iterator; // (1)
  requires InputIterator<iterator>; // (2)
  // ...
}
La forme simple consiste à écrire le code précédent de la manière suivante, qui est strictement équivalent.

concept InputIterator <typename T> { /* ... */ }

concept Container <typename X>
{
  InputIterator iterator; // strictement équivalent aux lignes (1) ET (2) ci-dessus
  // ...
}
Pour terminer, dans le cas particulier suivant :

concept A <typename T> { /* ... */ }

concept B <typename T>
{
  requires A<T>;
}
on peut écrire à la place :

concept A <typename T> { /* ... */ }

concept B <typename T> : A<T>
{
}
C'est une sorte d'héritage de concept, qui revient à définir un concept qui satisfera A mais un peu plus (ce qui sera demandé dans B). De la même façon que l'on dit qu'un Chien c'est un Animal mais un peu plus. Cela permet entre autres de découper des gros concepts en plusieurs petits, qui pourront être réutilisés. Imaginez un concept comme CorpsFini (au sens mathématiques)... cela s'avèrerait pratique et plus souple de découper en plusieurs concepts, certains s'occupant des opérations, un autre du cardinal fini par exemple, etc. Autre exemple : un concept CopyConstructibleAndOrderedSet serait ainsi facilement réalisé grâce au concept OrderedSet et au concept CopyConstructible.


VI. Axiomes

Les axiomes servent à définir les propriétés sémantiques, dépendantes des fonctions associées, types et classes templates associés ainsi que des paramètres du concept. Par exemple, définissons un concept EqualityComparable.

concept EqualityComparable <typename T> 
{
  bool operator==(T, T);
  bool operator!=(T, T);
  
  axiom Reflexivity(T x) {
    x == x; // propriété obligatoire
  }
}
Et comment définiriez-vous un concept NonEqualityComparable ? Il est interdit d'utiliser = delete... C'est assez simple, mais il faut le savoir : nous pouvons utiliser requires pour demander qu'un type ne satisfasse pas un certain concept, comme dans le code suivant :

concept NonEqualityComparable <typename T>
{
  requires !EqualityComparable<T>;
}
L'axiome (auquel on a donné le nom de Reflexivity) demande à n'importe quel objet de type T d'être égal à lui-même, en utilisant l'opérateur == associé. Il est à noter que si l'on avait pas défini d'opérateurs == et !=, on aurait toutefois eu le droit d'écrire x == x car, à l'intérieur des axiomes, on a des opérateurs == et != implicitement définis de la forme suivante.

bool operator==(const T&, const T&);
bool operator!=(const T&, const T&);
En effet : si dans un concept on n'a pas demandé d'opérateur ==, mais que l'on veut quand même pouvoir tester l'égalité des deux objets comme fait plus haut dans l'axiome, alors le compilateur en génèrera un, mais uniquement dans la portée à l'intérieur de l'axiome. Donc le schéma suivant est faux :

concept X <typename T>
{
  axiom A(T x)
  {
    /* je teste une égalité sur un élément x de type T et un autre objet */
  }
  /* maintenant j'ai un opérateur == généré pour mes objets de type T */
  /* CECI EST FAUX */ 
}  
C'est uniquement à l'intérieur de l'axiome A que l'on peut utiliser == mais uniquement si l'on ne l'a pas demandé dans le reste du concept. Par contre, l'on demande à avoir un opérateur == quelque part dans le concept, alors c'est ce dernier que le compilateur utilisera, et non plus un généré automatiquement... même à l'intérieur des axiomes.

Toutefois, ici nous imposons justement une propriété sur les opérateurs associés, et c'est bien ceux-là que nous voulons tester. Si dans un concept on ne déclare pas d'opérateur == associé, alors un appel à == dans un axiome utilisera la version implicite. Il en va de même pour l'opérateur !=.

Pour terminer, il faut noter 2 possibilités supplémentaires. La première est que l'on peut préfixer le mot clé "axiom" d'un requires, comme dans le code suivant, afin de raffiner les contraintes imposées juste dans le cadre de cet axiome.

concept EqualityComparable2<typename T, typename U = T> {
  bool operator==(T, U);
  bool operator!=(T, U);
  requires std::SameType<T, U> axiom Reflexivity(T x) {
      x == x; // OK: ici on exige que T = U pour définir la réflexivité
    }
}

VII. L'association (spécialisation) de concepts

Les concept maps (association/spécialisation de concepts) permettent de spécialiser un concept par rapport à un type précis.


VIII. Affinement de concept


IX. Concepts de support


X. Utilisation des concepts pour les templates contraints


XI. Comparaison : concepts et interfaces


XII. Concepts automatiques



               Version PDF (Miroir)   Version hors-ligne (Miroir)

Valid XHTML 1.1!Valid CSS!

Copyright © 2009 Alp Mestan. 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.

Contacter le responsable de la rubrique C++