Ecriture de services en C++11, Partie 1/2

Introduction

Cet article s’adresse a des personnes ayant déjà écrit du C++, sans nécessairement être expert. L’objectif de l’article est de présenter comment écrire des services en C++11 de manière idiomatique. L’utilisation des rvalue références et de la sémantique de déplacement sera au cœur de l’article, mais il n’y a aucune vocation à expliquer en détail le fonctionnement de ces éléments.

Dans un premier temps on va revenir sur ce qu’est un service et les outils qu’offre le C++11 pour les exprimer. Ensuite on présentera d’abord les services les plus génériques et ensuite ceux propres à un type. Finalement on verra comment implémenter les services spéciaux du C++. Cette partie de l’article s’arrêtera aux services les plus génériques.

Le terme service est préféré à celui de fonction dans cet article. Les concepts présentés sont allégé, l’introduction complète n’apporterait rien à l’article. Les conseils présentés dans cet article sont personnel et toute critique est bienvenue.

Concepts et services

Un concept est caractéristique d’un type, il s’agit d’une ensemble de conditions que doit vérifier un type. Les concepts s’expriment par un ensemble d’expressions qui doivent être valides et retourner des types respectant un concept. L’exemple typique est le concept d’itérateur. Pour le concept d’itérateur, on a en particulier :

//T respect le concept d iterateur
// si (entre autre) :
T t;

*t;  //Est valide 
     //et retourne un objet de type
     //std::iterator_traits<T>::reference
++t; //Est valide
     //et retourne une reference de type
     //T
La description du concept permet donc d’avoir un ensemble d’expressions que l’on peut utiliser dans l’implémentation.

Un service est défini par un ensemble de paramètres et une implémentation. Chaque paramètre est défini par un concept qu’il doit respecter, il a éventuellement un identifiant qui sert à l’implémentation. L’implémentation est une suite d’instructions qui n’utilisent que des expressions en accord avec les concepts. Le C++11 n’offre rien permettant d’exprimer ces concepts au sein du langage à l’heure actuelle 1, la description de ces concepts passent donc par de la documentation. Par exemple :

template<
    class InputIterator,
    class Distance
>
void advance(
    InputIterator&,
    Distance
);
Ici le service advance prend deux paramètres, un respectant le concept d’InputIterator et le second celui de Distance. La seule indication du concept est le nom du paramètre template utilisé.

Ici le type InputIterator doit respecter le concept d’itérateur 2. Et cette information n’est pas indiquée au sein du langage, seulement dans la norme ou une documentation. Ceci peut donc complexifier la phase de développement en cas de mauvaise utilisation 3.

On va se restreindre à un seul paramètre où la copie est coûteuse 4. L’on traitera différentes situations selon la nature du concept et la nature de ce que l’on doit faire avec le paramètre :

  • Tout d’abord les situations où le concept regroupe plusieurs types issus de différentes classes, éventuellement template : ce sont les services génériques.
  • Ensuite les situations où le concept s’exprime comme devant être un type déterminé où l’instance d’une classe template déterminée : ce sont les services spécifiques.

Services génériques

Prenons un exemple, supposons que l’on veuille écrire un service travaillant avec un paramètre à sémantique de pointeurs constants sur un type Box const 5 qui a un service membre constant open qui donne le nombre d’objets distincts dans la boîte. Voyons donc les éléments requis par le concept de pointeur constant sur le type Box:

//T respecte le concept de
//pointeur constant sur le type Box si
T t;

(bool)t //Est valide

//Si
(bool)t==true //t est dereferencable

*t; //Est valide
    //et retourne un objet convertible en
    //reference sur Box const
    //Requière que t soit dereferencable
Ici le concept de pointeur traduit ce qu’on fait naturellement avec les pointeurs natif du langage. Mais l’utilisation du concept permet de s’étendre aux pointeurs intelligents par exemple.

Le service membre open s’exprime par :

Box const box;

box.open();
//Est valide et retourne un objet de type
//implicitement convertible en size_t
A nouveau on pourrait exprimer le retour sous forme de concept pour donner plus de liberté à l’implémentation.

Pour un premier exemple supposons que notre service ne fait qu’afficher sur la sortie standard le nombre d’éléments dans la boîte :

#include<iostream>

template<
    class Pointer
>
void print_number(
    Pointer&& pointer
)
{
    if(pointer)
        std::cout   << "La boite contient "
                    << pointer->open()
                    << " objets distincts."
                    << std::endl;
    else
        std::cout   << "La boite n'existe pas."
                    << std::endl;
}
On retrouve le nom du paramètre qui indique le concept du paramètre, ici Pointer.La syntaxe « Pointer&& pointer » indique un paramètre du service.On utilise la validité de (bool)pointer.On utilise la validité de *pointer (au travers de pointer->).

Maintenant supposons que l’on a aussi un service membre clone_all qui va dupliquer le contenu de la boîte et que l’on va écrire un service travaillant avec un paramètre à sémantique de pointeur sur le type Box. Notre service va afficher le nombre d’éléments, dupliquer le contenu si elle existe, puis afficher à nouveau le nombre d’éléments. Par soucis de factorisation du code, on va se servir de notre premier service pour réaliser ceci :

#include<utility>

template<
    class Pointer
>
void duplicate(
    Pointer&& pointer
)
{
    print_number(pointer);
    if(pointer)
        pointer->clone_all();
    print_number(
        std::forward<Pointer>(pointer)
    );  
}
A nouveau on retrouve le « Pointer » qui permet d’indiquer le concept respecté par le paramètre.A nouveau on observe la syntaxe permettant d’indiquer un paramètre.On voit comment utiliser un service dans un autre : le passage de paramètre est simple.L’utilisation de std::forward<Pointer>(pointer) permet de dire « je n’ai plus besoin du paramètre pointer, si j’en étais responsable donner la responsabilité à print_number, sinon utiliser le service sans lui donner la responsabilité.

On voit donc que l’utilisation du paramètre n’est pas la même selon la position du service utilisé au sein de l’implémentation. Si le dernier service utilisé sur le paramètre est non membre, l’utilisation de std::forward<Pointer> est conseillé pour effectuer le passage.

Passons à l’utilisation d’un service spécial au sein de l’implémentation : l’instruction return. L’instruction return est utile lorsque l’on construit un objet au sein de l’implémentation et qu’on désire le donner à l’utilisateur de notre service. Pour l’exemple on va réaliser un service qui va extraire des objets aléatoirement, de manière à en avoir strictement moins qu’au début et strictement plus d’un, et les mette dans un autre boîte qu’on retournera 6 :

#include<memory>

template<
    class Pointer
>
std::unique_ptr<Box> extract_rand(
    Pointer&& pointer
)
{       
    if(pointer)
    {
        auto result(
            std::make_unique<Box>()
        );
        //algorithme
        return result;
    }
    return nullptr;
}
On voit que l’indication du type de retour est simple.On voit aussi que l’utilisation de return est simple : il suffit de créer l’objet désiré dans le service puis de le retourner.

L’utilisation de std::unique_ptr<Box> permet d’avoir une gestion de la mémoire correcte. Ceci dit la documentation d’un tel service ne nécessite pas de préciser std::unique_ptr<Box>, simplement le concept que respect le retour 7 et que la gestion de la mémoire est automatique. Ceci permet d’avoir une plus grande flexibilité, l’on peut faire évoluer le type de retour sans impacter l’utilisateur si celui n’a bien utilisé que ce qu’indique la documentation et rien d’autre.

Jusqu’ici l’ensemble des services réalisés étaient utiles 8 même si l’objet passé en paramètre était détruit juste après l’exécution du service. Pour illustrer une situation contraire, supposons que l’on dispose du service membre expend qui agrandit l’ensemble des objets de la boîte. On va écrire un service qui, si elle existe, sélectionne certains éléments puis les modifie en les agrandissant. On va utiliser extract_rand pour factoriser le code :

#include<utility>

template<
    class Pointer
>
void shake(
    Pointer& pointer
)
{       
    using std::swap;

    auto extracted(
        extract_rand(pointer)
    );
    if(extracted)
    {
        extracted->expend();
        Pointer to_swap(
            extreacted.release()
        );
        swap(pointer,to_swap);
    }       
}
On remarque ici la première différence avec les situations précédente, la syntaxe est « Pointer& pointer ». Ceci interdira d’appeler notre service avec un objet qui sera détruit juste après le service : ceci n’aura aucun intérêt.L’utilisation de « Pointer to_swap » permet d’introduire la même politique de pointeur dans l’implémentation que celle utilisé par l’utilisateur du service : si il l’utilise avec un std::unique_ptr, alors l’ancien élément pointé par « pointer » sera détruit par exemple.

Ici on ne peut pas appeler shake sur un paramètre qui va être détruit juste après l’exécution du service, car celui-ci n’aurait aucun impact sur le programme. Notons ici l’utilisation de auto, le type réel de retour de extracted ne nous importe pas, la seule importance est la gestion automatique de la mémoire, ceci garantit l’existence du service membre release. Notons aussi ici l’importance de la résistance aux exceptions 9. L’objectif de cette résistance est de pouvoir effectuer un traitement en cas d’exceptions en se basant sur des objets dont on connait l’état.

Conclusion

Cette partie touche à sa fin, j’ai fait le choix de commencer à présenter les choses les plus génériques avant de présenter les éléments plus spécifique. Je pense qu’il est plus courant d’avoir à écrire des services génériques que des services spécifiques à une classe : il existe déjà beaucoup de chose apporté par divers bibliothèques, savoir assembler ces éléments est très important selon moi.

Dans la partie suivante j’aborderais d’abord les services spécifiques à une classe puis les services spéciaux du C++. Cette seconde partie sera plus courte que la première et n’introduira pas de nouvelles définitions.


Notes:

1 Il existe quelques éléments de solution.

2 Plus exactement d’InputIterator comme son nom l’indique.

3 L’introduction d’un outil pour spécifier ces concepts est prévu pour la future norme du C++.

4 Dans le cas contraire il s’agit de l’utilisation de passage par copie qu’on ne traitera pas ici.

5 Notons que l’intérêt d’utiliser une sémantique de pointeur ici n’est pas des plus pertinent,on pourrait passer directement par des références sur Box ou Box const , cependant il permet d’illustrer clairement le propos.

6 L’exemple utilise std::make_unique qui ne sera disponible qu’en C++14, sur le principe on peut le considérer comme un simple new.

7 C’est à dire celui de pointeur sur un type Box.

8 C’est à dire que l’état du programme avant et après l’exécution du service est sémantiquement différent.

9 On considère ici une résistance forte, qui est pour rappel que les paramètres soient non modifiés en cas d’exceptions au sein de l’implémentation.

Publicités

Une réflexion au sujet de « Ecriture de services en C++11, Partie 1/2 »

  1. Ping : Généricité, concepts et C++14 | C++, Qt, OpenGL, CUDA

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s