moyenne() – Exemple

Nous avons examiné en classe un exemple de fonction générique moyenne(), opérant à partir d'itérateurs et de traits.

L'exemple à droite est fonctionnel, à ceci près que je n'ai implémenté les traits reel_type et cumul_type que pour un seul type (vous pourrez l'enrichir à votre convenance, évidemment).

Notez que l'implémentation à droite utilise auto pour déduire le type de n à partir du type de std::distance(debut,fin), ce qui est bien plus léger que ne le serait le recours aux traits du type d'itérateur It. Sur un compilateur pré-C++ 11, toutefois, les traits auraient été l'option à privilégier.

Notez aussi qu'avant C++ 11, les fonctions std::begin() et std::end() n'étaient pas offertes de manière standard. On aurait toutefois pu exprimer quelque chose de semblable en déduisant la taille du tableau tab dès la compilation :

int tab[] { 2, 3, 5, 7, 11 };
enum { N = sizeof(tab) / sizeof(tab[0]) };
cout << moyenne(&tab[0], &tab[N]) << endl;

Remarquez au passage le recours à des alias de types avec using. Cette pratique, possible depuis C++ 11, permet d'alléger significativement l'écriture des types génériques complexes. Ainsi, plutôt que d'écrire ceci :

template <class It>
   typename reel_traits<it_val_t<It>>::type
      moyenne(It debut, It fin);

... on peut maintenant écrire cela :

template <class It>
   reel_type_t<It_val_t<It>> moyenne(It debut, It fin);

... du fait que reel_type_t<T> est défini comme équivalent à typename reel_traits<T>::type. Le gain de simplicité est appréciable.

template <class>
   struct reel_traits;
template <>
   struct reel_traits<int> {
      using type = float;
   };
//
// ...
//
template <class T>
   using reel_type_t = typename reel_traits<T>::type;

template <class>
   struct cumul_traits;
template <>
   struct cumul_traits<int> {
      using type = long;
   };
//
// etc.
//
template <class T>
   using cumul_type_t = typename cumul_traits<T>::type;

#include <iterator>
template <class T>
   using it_val_t = std::iterator_traits<T>::type;

template <class It>
   auto moyenne(It debut, It fin) {
      using std::distance;
      auto n = distance(debut, fin);
      cumul_type_t<it_val_t<It>> cumul {};
      for (; debut != fin; ++debut)
         cumul += *debut;
      return static_cast<reel_type_t<it_val_t<It>>(cumul) / n;
   }

#include <iostream>
#include <algorithm>
int main() {
   using namespace std;
   int tab[] { 2, 3, 5, 7, 11 };
   cout << moyenne(begin(tab), end(tab)) << endl;
}

Vous avez peut-être remarqué que cumul_traits est quelque peu risqué, parce qu'il est ardu de savoir à la compilation du code serveur quels seront les besoins effectifs de cumul du code client.

Le raffinement à droite couvre (avantageusement) cette particularité. Pour cumuler sur un autre type que le cumul_type planifié, il suffit d'offrir une surcharge de la fonction moyenne(), celle-ci prenant un troisième paramètre dont le type sera celui désiré et dont la valeur initiale sera appropriée.

Évidemment, il est alors plus simple (et sans risque de perte de performance ou de généralité) d'implémenter la version à deux paramètres comme un cas particulier du cas à trois paramètres...

Notez que, dans un cas comme dans l'autre, cet exemple peut être amélioré d'au moins deux importantes manières :

  • Ironiquement, en calculant le nombre d'éléments d'une manière plus efficace qu'en invoquant std::distance() lorsque l'itérateur utilisé n'est pas de la catégorie random access (pour éviter un double parcours de la séquence dans cette situation). Cette amélioration est subtile (je vous laisse y réfléchir) mais très payante; et
  • En invoquant std::accumulate(), de la bibliothèque <numeric>, pour réaliser le cumul des valeurs.
#include <iostream>
#include <algorithm>
#include <iterator>
using namespace std;

//
// reel_traits, reel_type_t, cumul_traits, cumul_type_t
//

template <class It, class C>
   auto moyenne(It debut, It fin, C cumul) {
      auto n = distance(debut, fin);
      for (; debut != fin; ++debut)
         cumul += *debut;
      return static_cast<reel_type_t<it_val_t<It>>(cumul) / n;
   }
template <class It>
   auto moyenne(It debut, It fin) {
      using cumul_type = cumul_traits_<it_val_t<It>>;
      return moyenne(debut, fin, cumul_type_t<it_val_t<It>>{});
   }

int main() {
   int tab[] { 2, 3, 5, 7, 11 };
   // cumul implicitement fait sur un cumul_traits<int>::type
   cout << moyenne(begin(tab), end(tab)) << endl;
   // cumul explicitement fait sur un double
   cout << moyenne(begin(tab), end(tab), 0.0) << endl;
}

L'amélioration la plus immédiate ici consiste sans doute à passer par std::accumulate() pour réaliser le cumul des valeurs.

Ce faisant, le code tout entier de la fonction moyenne() devient une seule expression combinant le cumul des valeurs et le nombre d'éléments dans la séquence sur laquelle la fonction opère. Il ne reste même plus de variable locale à cette fonction!

#include <iterator>
#include <numeric>
#include <iostream>
#include <algorithm>
using namespace std;

//
// reel_traits, reel_type_t, cumul_traits, cumul_type_t
//

template <class It, class C>
   auto moyenne(It debut, It fin, C cumul) {
      return static_cast<reel_type_t<it_val_t<It>>>(
         accumulate(debut, fin, cumul)
      ) / distance(debut, fin);
   }
template <class It>
   auto moyenne(It debut, It fin) {
      return moyenne(debut, fin, cumul_type<it_val_t<It>>{});
   }

int main() {
   int tab[] { 2, 3, 5, 7, 11 };
   cout << moyenne(begin(tab), end(tab)) << endl;
   cout << moyenne(begin(tab), end(tab), 0.0) << endl;
}

Valid XHTML 1.0 Transitional

CSS Valide !