Paramètres par défaut – un type defaulted

Note : ce qui suit a été inspiré d'une question de Julien Pruvost, cohorte 17 du Diplôme de développement du jeu vidéo (DDJV) à l'Université de Sherbrooke.

Il peut arriver que l'on souhaite avoir des paramètres avec valeurs par défaut à l'appel d'une fonction. Par exemple, prenez la paire de fonctions suivante :

int next(int min, int max); // piger un nombre pseudoaléatoire dans [min,max[
int next(int min); // piger un nombre pseudoaléatoire dans [min, numeric_limits<int>::max()[

... où nous présumerons que le code des deux fonctions est identique à la borne maximale près, cette borne étant indiquée par un paramètre dans le premier cas et ayant une valeur par défaut dans le second. Pour réduire la redondance, il peut être raisonnable de fondre ces deux fonctions en une seule :

int next(int min, int max = std::numeric_limits<int>::max()); // piger un nombre pseudoaléatoire dans [min,max[

... et de ne l'implémenter qu'une seule fois.

Brève mise en garde

Notez que les paramètres avec valeurs par défaut sont souvent des choix discutables, et qu'il est préférable de les utiliser avec parcimonie. Par exemple, on pourrait vouloir remplacer cette classe :

struct Point {
   float x, y, z;
   constexpr Point() : x{}, y{}, z{} {
   }
   constexpr Point(float x, float y, float z) : x{ x }, y{ y }, z{ z } {
   }
   // ...
};

... par celle-ci :

struct Point {
   float x, y, z;
   constexpr Point(float x = {}, float y = {}, float z = {}) : x{ x }, y{ y }, z{ z } {
   }
   // ...
};

... mais il s'agirait d'une fausse bonne idée, du fait que la première admet les constructions suivantes :

Point pt0; // point à l'origine;
Point pt1{ 2, -3.5f, 1 }; // représente la coordonnée (2, -3.5, 1)

... alors que la seconde permet les constructions suivantes, dont certaines sont discutables :

Point pt0; // point à l'origine;
Point pt1{ 2, -3.5f, 1 }; // représente la coordonnée (2, -3.5, 1)
Point pt2{ 2, -3.5f }; // suspect : z vaut zéro... Est-ce voulu?
Point pt3{ 2 }; // suspect : y et z valent zéro... Est-ce voulu?
// ... ceci peut aussi surprendre :
void f(Point);
void g() {
   f(3); // euh?
}

Il y a d'autres pièges liés aux paramètres avec valeur par défaut, alors utilisez-les, mais avec parcimonie et de manière réfléchie.

En pratique, les paramètres avec valeurs par défaut doivent se trouver à la fin de la liste des paramètres d'une fonction. Ainsi, ceci est légal :

int next(int min, int max = std::numeric_limits<int>::max()); // piger un nombre pseudoaléatoire dans [min,max[

... mais ceci ne l'est pas :

// illégal :
int next(int min = 0, int max); // piger un nombre pseudoaléatoire dans [min,max[

Que peut-on faire, donc, si l'on souhaite utiliser des paramètres avec valeurs par défaut ailleurs qu'à la fin de la liste des paramètres d'une fonction? Par exemple, supposons qu'une fonction f prenne deux paramètres entiers a et b, et que l'on souhaite pouvoir omettre une valeur explicite pour a ou pour b (ou les deux). Supposons pour les besoins de l'exemple que l'on souhaite que a ait par défaut la valeur 3 et que b ait par défaut la valeur 4, de telle sorte que dans le programme suivant, les valeurs effectivement passées en paramètre soient celles indiquées dans les commentaires correspondants :

// ...
int main() {
   f(5, 5); // 5 5
   f({}, 5); // 3 5
   f(5, {}); // 5 4
   f({}, {}); // 3 4
}

Évidemment, la signature suivante ne sied pas :

void f(int a = 3, int b = 4);

... car il serait possible d'appeler f() sans paramètre (f() serait équivalent à f(3,4))), ou encore avec un seul paramètre (f(5) serait équivalent à f(5,4)), mais les appels indiqués dans le programme principal ne donneraient pas les résultats attendus ({} serait alors 0, et les valeurs par défaut souhaitées ne seraient pas utilisées).

Solution possible – un type defaulted<V>

Une solution possible à ce problème est de modéliser le concept de variable avec valeur par défaut par un type. Par exemple, le type defaulted<V> ci-dessous où V est une valeur connue à la compilation et où une valeur du même type que V peut être utilisée en lieu et place de:

template <auto V>
   struct defaulted {
      defaulted() = default;
      decltype(V) val = V;
      defaulted(decltype(V) val) : val{ val } {
      }
      explicit constexpr operator auto() const { return val; }
   };

#include <iostream>
using namespace std;

void f(defaulted<3> a, defaulted<4> b) {
    cout << static_cast<int>(a) << ' ' << static_cast<int>(b) << endl;
}

int main() {
    f({}, 5);  // 3 5
    f(5, {});  // 5 4
    f({}, {}); // 3 4
    f(5, 5);   // 5 5
}

Voir https://wandbox.org/permlink/BkqLXqJh9wEXesAo pour un exemple complet.

Il est possible d'utiliser defaulted<V> pour des types V plus complexes dans la mesure où ils peuvent être utilisés à titre de Non-Type Template Parameter (NTTP). Par exemple :

template <auto V>
   struct defaulted {
      defaulted() = default;
      decltype(V) val = V;
      defaulted(decltype(V) val) : val{ val } {
      }
      explicit constexpr operator auto() const { return val; }
   };

struct Point {
   float x{}, y{};
   Point() = default; // note : implicitement constexpr
   constexpr Point(float x, float y) : x{ x }, y{ y } {
   }
};

constexpr Point origine() { return {}; }
constexpr Point depl_x(float dx) { return { dx, 0 }; }

#include <iostream>
#include <cmath>
using namespace std;

auto distance(defaulted<Point{}> p0, defaulted<Point{1,1}> p1) {
    return sqrt(pow(static_cast<Point>(p0).x - static_cast<Point>(p1).x, 2) +
                pow(static_cast<Point>(p0).y - static_cast<Point>(p1).y, 2));
}

int main() {
   cout << distance({}, {}) << endl; // sqrt(2)
   cout << distance(Point{ 1, 0 }, {}) << endl; // 1
   cout << distance({ }, Point{ 1.5f, 0 }) << endl; // 0.f
}

Lien pour vous amuser : https://wandbox.org/permlink/7YbMCpNih4ZHONyb

Lectures complémentaires

Quelques liens pour enrichir le propos.


Valid XHTML 1.0 Transitional

CSS Valide !