Une syntaxe unifiée pour les fonctions

Supposons que vous rédigiez une classe comme la suivante :

#include <vector>
template <class T>
   class tapon_en_ordre {
   public:
      using value_type = T;
   private:
      using conteneur_type = std::vector<value_type>;
      conteneur_type elems;
   public:
      using iterator = typename conteneur_type::iterator 
      using const_iterator = typename conteneur_type::const_iterator;
      using size_type = typename conteneur_type::size_type;
      void ajouter(const value_type&);
      iterator begin();
      iterator end();
      const_iterator begin() const;
      const_iterator end() const;
      size_type size() const;
      bool empty() const;
      // etc.
   };
// le tout continue plus bas...

En soi, cela semble banal. Cela dit, vous remarquerez que le code ci-dessus se limite à des déclarations de méthodes. Comme c'est le cas pour bien des gens, on peut supposer que les programmeuses et les programmeurs aient souhaité séparer interface et implémentation (bien que, dans le cas de code générique, ces deux parties doivent demeurer visibles au compilateur lorsque celui-ci rencontre le lieu où les méthodes seront appelées).

On aurait alors, probablement dans le même fichier d'en-tête, les définitions de méthodes suivantes :

// le tout débute plus haut...
#include <algorithm>
#include <iterator>
template <class T>
   void tapon_en_ordre<T>::ajouter(const value_type &elem) {
      elems.emplace_back(elem);
      std::sort(elems.begin(), elems.end());
   }
template <class T>
   tapon_en_ordre<T>::size_type tapon_en_ordre::size() const {
      return elems.size();
   }
template <class T>
   bool tapon_en_ordre::empty() const {
      return elems.empty();
   }
template <class T>
   tapon_en_ordre<T>::iterator tapon_en_ordre::begin() {
      return elems.begin();
   }
template <class T>
   tapon_en_ordre<T>::iterator tapon_en_ordre::end() {
      return elems.end();
   }
template <class T>
   tapon_en_ordre<T>::const_iterator tapon_en_ordre::begin() const {
      return elems.begin();
   }
template <class T>
   tapon_en_ordre<T>::const_iterator tapon_en_ordre::end() const {
      return elems.end();
   }
// ...

Remarquez l'écriture un peu longue du type de retour de certaines méthodes (celles où des éléments sont indiqués en caractères gras). Le type tapon_en_ordre doit explicitement qualifier chaque recours à l'un de ses types internes du fait qu'au moment de spécifier le type de la valeur retournée par une méthode définie hors de la déclaration de sa classe, le compilateur ne sait pas de quel type on parle.

Un exemple éclairant est la comparaison de l'écriture du type tapon_en_ordre::value_type en tant que paramètre à tapon_en_ordre::ajouter() et de l'écriture du type tapon_en_ordre::size_type en tant que type de ce que retourne tapon_en_ordre::size() :

En tant que paramètre En tant que type retourné
template <class T>
   void tapon_en_ordre<T>::ajouter(const value_type &elem) {
      elems.emplace_back(elem);
      std::sort(elems.begin(), elems.end());
   }
template <class T>
   tapon_en_ordre<T>::size_type tapon_en_ordre::size() const {
      return elems.size();
   }

Dans le cas du paramètre, le nom value_type est utilisé une fois le contexte tapon_en_ordre:: connu, et ce contexte est implicite. Dans le cas du type de la valeur retournée, le contexte en question n'est pas encore introduit (il apparaît plus loin, en préfixe au nom de la méthode) et celui-ci doit donc être exprimé explicitement.

Un raffinement proposé par C++ 11 est une formule dite « unifiée » pour l'écriture de fonctions. Cette formule permet d'exprimer le format habituel, soit :

type nom(params) { code }

...par un nouveau format, qui place le type de la valeur retournée par la fonction après le nom de la fonction lui-même, comme ceci :

auto nom(params) -> type { code }

Le mot clé auto est obligatoire dans cette deuxième écriture. Dans bien des cas, cette nouvelle formule a un apport cosmétique, et on peut l'aimer ou non sans que cela n'ait de réel impact (le code généré étant, dans les deux cas, le même) :

Format traditionnel Format « unifié »
int f() {
   return 3;
}
auto f() -> int {
   return 3;
}

Dans le cas d'une λ, il y a parfois un intérêt à expliciter le type de retour souhaité. Dans un tel cas, le format « unifié » est clairement pertinent.

auto carre = [](int n) -> long { return n * n; }

Dans un cas comme celui de tapon_en_ordre, cela dit, l'impact simplificateur de la nouvelle syntaxe saute aux yeux :

Format traditionnel Format « unifié »
// ...
#include <algorithm>
#include <iterator>
template <class T>
   void tapon_en_ordre<T>::ajouter(const value_type &elem) {
      elems.emplace_back(elem);
      std::sort(elems.begin(), elems.end());
   }
template <class T>
   tapon_en_ordre<T>::size_type tapon_en_ordre::size() const {
      return elems.size();
   }
template <class T>
   bool tapon_en_ordre<T>::empty() const {
      return elems.empty();
   }
template <class T>
   tapon_en_ordre<T>::iterator tapon_en_ordre::begin() 
      return elems.begin();
   }
template <class T>
   tapon_en_ordre<T>::iterator tapon_en_ordre::end() {
      return elems.end();
   }
template <class T>
   tapon_en_ordre<T>::const_iterator tapon_en_ordre::begin() const {
      return elems.begin();
   }
template <class T>
   tapon_en_ordre<T>::const_iterator tapon_en_ordre::end() const {
      return elems.end();
   }
// ...
// ...
#include <algorithm>
#include <iterator>
template <class T>
   void tapon_en_ordre<T>::ajouter(const value_type &elem) {
      elems.emplace_back(elem);
      std::sort(elems.begin(), elems.end());
   }
template <class T>
   auto tapon_en_ordre<T>::size() const -> size_type {
      return elems.size();
   }
template <class T>
   bool tapon_en_ordre<T>::empty() const {
      return elems.empty();
   }
template <class T>
   auto tapon_en_ordre<T>::begin() -> iterator {
      return elems.begin();
   }
template <class T>
   auto tapon_en_ordre<T>::end() -> iterator {
      return elems.end();
   }
template <class T>
   auto tapon_en_ordre<T>::begin() const -> const_iterator {
      return elems.begin();
   }
template <class T>
   auto tapon_en_ordre<T>::end() const -> const_iterator {
      return elems.end();
   }
// ...

Vous remarquerez que, étant placé dans le contexte du nom de la classe à laquelle appartient la méthode, la version « unifiée » est moins verbeuse, plus directe dans sa formulation. Il s'agit donc d'un outil intéressant pour alléger le travail de rédaction du code sans que cela n'entraîne quelque coût que ce soit en termes de temps d'exécution.

Mot clé auto et fonctions depuis C++ 14

Depuis C++ 14, les fonctions peuvent avoir auto come « type de retour ». Le sens de cette écriture n'est pas que la fonction peut retourner n'importe quoi, bien entendu, mais bien que le type de la fonction sera déduit du type du premier return qui y apparaîtra. Si la fonction comprend plusieurs return, il importe que tous retournent une valeur du type de l'expression retournée par le premier return rencontré – il ne suffit pas que l'un des types soit convertible dans l'autre type. Ainsi :

...ceci compilera, et le type de x sera int... ...mais ceci ne compilera pas
#include <iostream>
#include <typeinfo>
auto f(int i) {
    if (i > 0)
        return 3;
    else if (i < 0)
        return -3;
    else
        return 1-1;
}
int main() {
    using namespace std;
    auto x = f(1);
    cout << x << " de type " << typeid(x).name() << endl;
}
#include <iostream>
#include <typeinfo>
auto f(int i) {
    if (i > 0)
        return 3.5;
    else if (i < 0)
        return -3; // incorrect; double attendu
    else
        return 1-1;
}
int main() {
    using namespace std;
    auto x = f(1);
    cout << x << " de type " << typeid(x).name() << endl;
}

Notez que bien que le type de x soit int dans l'exemple à gauche, ce qui sera affiché à l'exécution à titre de « nom » du type dépendra de l'implémentation.

Ce mécanisme permet de réaliser aisément plusieurs tâches qui étaient très difficiles à réaliser auparavant. Par exemple :

  • Il permet de retourner des objets de type inconnu du code client... et de les utiliser!
#include <iostream>
auto f() {
    struct X {
        int g() const { return 3; }
    };
    return X{};
}
int main() {
    using namespace std;
    cout << f().g() << endl;
}
  • Il permet d'exprimer de manière élégante des opérations complexes sur des séquences variadiques incluant plusieurs types distincts.

Notez que C++ 17 permet de réaliser ce type de calcul de manière encore plus simple, avec ce que l'on  nomme des Fold Expressions. Par exemple :

template <class ... Ts>
   auto somme(Ts && ... args) {
      return args + ...;
   }
#include <iostream>
template <class T>
    auto somme(T && arg) {
        return std::forward<T>(arg);
    }
template <class T, class ... Ts>
   auto somme(T && arg, Ts && ...args) {
       return arg + somme(std::forward<Ts>(args)...);
   }
template <class ... Ts>
    auto moyenne(Ts && ... args) {
        return somme(std::forward<Ts>(args)...) / sizeof...(Ts);
    }
int main() {
    using namespace std;
    cout << moyenne(2,3,5.0,7,11) << endl;
}
  • Plus simplement, il permet d'exprimer sans peine des types de retour qui seraient autrement lourds et laborieux à exprimer.
template <class C>
   auto cbegin(const C &conteneur) {
      return std::begin(conteneur);
   }
//
// sans auto, nous aurions dû écrire ceci...
//
//template <class C>
//   auto cbegin(const C &conteneur) -> typename C::const_iterator {
//      return std::begin(conteneur);
//   }
//
// ... ou cela...
//
//template <class C>
//   typename C::const_iterator cbegin(const C &conteneur) {
//      return std::begin(conteneur);
//   }
//

Il faut bien sûr éviter d'abuser de ce mécanisme, comme de toute bonne chose, pour ne pas que la lisibilité du code n'en souffre, mais ses avantages sont très nets.

Lectures complémentaires

Quelques liens pour enrichir le propos.


Valid XHTML 1.0 Transitional

CSS Valide !