Tous droits réservés

Reprendrez-vous un peu de sucre syntaxique ?

Le sucre se trouve partout aujourd’hui. Dans les gâteaux, dans les biscuits, dans les boissons et même dans notre code ! Sauf que dans ce dernier cas, on parle de sucre syntaxique. Ce terme, inventé par Peter J. Landin, un informaticien britannique, désigne des syntaxes plus lisibles ou plus faciles à écrire qu’on utilise pour se faciliter la vie.

Le but de ce chapitre va donc être d'utiliser et même d'écrire du sucre syntaxique.

Ce type est trop long !

Vous vous souvenez du chapitre sur les flux ? Nous y avons découvert comment traiter des chaînes de caractères comme des flux, avec les types std::istringstream et std::ostringstream. Mais il faut avouer que c’est assez long à écrire et fatiguant à la longue.

Heureusement, en C++, on peut définir des alias de types, des synonymes, qui permettent d’éviter l’utilisation de types longs à écrire ou bien de types composés. Cela ce fait très simplement, en utilisant le mot-clef using.

// Désormais, écrire 'oss' dans le code revient à écrire 'std::ostringstream'.
using oss = std::ostringstream;

// Désormais, écrire 'ListeResultats' dans le code revient à écrire 'std::vector<std::tuple<std::string, int>>'.
using ListeResultats = std::vector<std::tuple<std::string, int>>;

Même la bibliothèque standard utilise le concept. Ainsi, std::string est lui-même l’alias d’un type std::basic_string<char>. Notez que c’est quand même beaucoup plus agréable à écrire.

Décomposons tout ça

Dans le chapitre précédent, nous avons souligné la lourdeur et le manque d’expressivité de l’accès aux éléments d’un tuple. Il est vrai qu’une expression comme std::get<2>(tuple) ne nous donne guère d’information sur le sens de ce que nous manipulons. Heureusement, il existe des syntaxes alternatives, que nous allons voir.

En pleine décomposition

Une des nouveautés de C++17, ce sont les décompositions (en anglais « structured bindings »). Ce concept est très simple. Son but est de déclarer et initialiser plusieurs variables d’un coup, depuis un tuple ou une structure. Et comme un code vaut mille mots, regardez par vous-mêmes. J’ai repris un exemple du chapitre précédent, en l’adaptant.

#define _USE_MATH_DEFINES

#include <cmath>
#include <iostream>
#include <string>
#include <tuple>

std::tuple<int, std::string> f()
{
    using namespace std::literals;
    return { 20, "Clem"s };
}

std::tuple<double, double> g(double angle)
{
    return { std::cos(angle), std::sin(angle) };
}

int main()
{
    // AVANT
    // std::tuple resultats_scolaires = f();
    // std::cout << "Tu t'appelles " << std::get<std::string>(resultats_scolaires) << " et tu as obtenu " << std::get<int>(resultats_scolaires) << " / 20." << std::endl;

    // Maintenant, on sait qu'on a affaire à un nom et un entier.
    auto[note, nom] = f();
    std::cout << "Tu t'appelles " << nom << " et tu as obtenu " << note << " / 20." << std::endl;

    // AVANT
    // std::tuple calculs = g(M_PI / 2.);
    // std::cout << "Voici le cosinus de PI / 2 : " << std::get<0>(calculs) << std::endl;
    // std::cout << "Voici le sinus de PI / 2 : " << std::get<1>(calculs) << std::endl;

    // Maintenant, on sait qu'on a affaire à deux valeurs mathématiques.
    auto[cosinus, sinus] = g(M_PI / 4.);
    std::cout << "Voici le cosinus de PI / 4 : " << cosinus << std::endl;
    std::cout << "Voici le sinus de PI / 4 : " << sinus << std::endl;

    return 0;
}

Notez comment le code se voit simplifié et clarifié. Nous avons remplacé une ligne aussi peu claire que std::get<0>(calculs) par cosinus. D’un coup d’œil, vous voyez ce que fait la variable et ce que nous manipulons. Le code est plus lisible et donc de meilleure qualité. N’oubliez pas qu’un code est bien plus souvent lu qu’écrit.

On peut bien entendu utiliser des références, si l’on veut que des modifications sur les variables se répercutent sur le tuple lié. L’exemple suivant est tiré d’un billet de @germinolegrand.

#include <cassert>
#include <iostream>
#include <string>
#include <tuple>

int main()
{
    using namespace std::literals;
    auto bookmark = std::make_tuple("https://zestedesavoir.com"s, "Zeste de Savoir"s, 30);

    auto&[url, titre, visites] = bookmark;
    ++visites;

    assert(std::get<2>(bookmark) == 31);
    return 0;
}

Enfin, ce que nous venons de voir est tout à fait applicable aux structures. L’exemple suivant, bien que bidon, vous le montre.

#include <iostream>
#include <string>
#include <tuple>

struct Personne
{
    std::string nom;
    std::string prenom;
    int age;
};

Personne f()
{
    return { "Lagrume", "Clem", 4 };
}

int main()
{
    auto[nom, prenom, age] = f();
    std::cout << "Voici " << prenom << " " << nom << " et elle a " << age << " ans." << std::endl;
    return 0;
}
Voici Clem Lagrume et elle a 4 ans.

Une décomposition peut déclarer exactement autant de variables que d’éléments composant la structure ou le tuple, ni plus, ni moins. Même si nous ne voulons récupérer que certaines variables, il faut tout de même créer des variables inutiles qui ne seront plus utilisées par la suite.

Et avant, on faisait comment ?

Les décompositions sont apparues en C++17, mais avant, il existait une autre façon de faire, qui existe toujours soit dit en passant. Il s’agit de std::tie. Le principe est identique, puisque on veut assigner à des variables les différents éléments d’un tuple ou d’une structure. Sauf qu’ici, les variables doivent être déclarées au préalable.

#define _USE_MATH_DEFINES

#include <cmath>
#include <iostream>
#include <tuple>

std::tuple<double, double, double> f(double angle)
{
    return { std::cos(angle), std::sin(angle), std::tan(angle) };
}

int main()
{
    // Création au préalable.
    double cosinus {};
    double sinus {};
    double tangente {};

    std::tie(cosinus, sinus, tangente) = f(M_PI);

    std::cout << "Cosinus de pi : " << cosinus << std::endl;
    std::cout << "Sinus de pi : " << sinus << std::endl;
    std::cout << "Tangente de pi : " << tangente << std::endl;

    return 0;
}
Cosinus de pi : -1
Sinus de pi : 0
Tangente de pi : 0

Cette solution présente a comme inconvénient majeur que les variables doivent être déclarées à l’avance et donc qu'on ne peut pas profiter de l’inférence de type. Par contre, l’avantage qu’elle a sur la décomposition, c’est qu’elle permet d’ignorer certaines valeurs qui ne nous intéressent pas et que nous ne voulons pas récupérer, grâce à std::ignore.

#define _USE_MATH_DEFINES

#include <cmath>
#include <iostream>
#include <tuple>

std::tuple<double, double, double> f(double angle)
{
    return { std::cos(angle), std::sin(angle), std::tan(angle) };
}

int main()
{
    // Seul le cosinus m'intéresse.
    double cosinus {};

    std::tie(cosinus, std::ignore, std::ignore) = f(M_PI);

    std::cout << "Cosinus de pi : " << cosinus << std::endl;
    return 0;
}

Un autre avantage, c’est de pouvoir réutiliser une même variable plusieurs fois, alors que la décomposition C++17 ne nous le permet pas.

La surcharge d'opérateurs

Lorsque l’on crée un type, il arrive que l’on souhaite réaliser certaines opérations communes sur ce type, comme l’injection dans un flux (pour de l’affichage par exemple), la réalisation d’opérations arithmétiques (addition, soustraction, etc) ou encore d’opérations de comparaison.

Le problème…

Imaginons que nous voulions créer un type pour manipuler des fractions. On aimerait pouvoir les additionner, les soustraire, etc, en bref, faire des opérations dessus. Sauf qu’on ne peut pas se contenter de faire un simple + ou -, comme c’est le cas avec des int ou des double.

#include <iostream>
#include <string>

struct Fraction
{
    int numerateur;
    int denominateur;
};

int main()
{
    Fraction const un_demi { 1, 2 };
    Fraction const trois_quarts { 3, 4 };
    Fraction const resultat { un_demi + trois_quarts };

    std::cout << "1/2 + 3/4 font " << resultat.numerateur << "/" << resultat.denominateur << std::endl;
    return 0;
}
[Visual Studio]
Erreur  C2676 '+' binaire : 'const Fraction' ne définit pas cet opérateur ou une conversion vers un type acceptable pour l'opérateur prédéfini
Erreur (active) E0349 aucun opérateur "+" ne correspond à ces opérandes

------------------------------------------------------------

[GCC]
prog.cc: In function 'int main()':
prog.cc:14:39: error: no match for 'operator+' (operand types are 'const Fraction' and 'const Fraction')
   14 |     Fraction const resultat { un_demi + trois_quarts };
      |                               ~~~~~~~~^~~~~~~~~~~~~~

------------------------------------------------------------

[Clang]
prog.cc:14:39: error: invalid operands to binary expression ('const Fraction' and 'const Fraction')
    Fraction const resultat { un_demi + trois_quarts };
                              ~~~~~~~ ^ ~~~~~~~~~~~~
1 error generated.

On doit alors écrire des fonctions pour faire ça. Mais si on les mélange, ça peut très vite devenir indigeste à lire.

#include <iostream>
#include <string>

struct Fraction
{
    int numerateur;
    int denominateur;
};

Fraction additionner(Fraction const & a, Fraction const & b)
{
    int numerateur { a.numerateur * b.denominateur + b.numerateur * a.denominateur };
    int denominateur { a.denominateur * b.denominateur };

    return { numerateur, denominateur };
}

Fraction soustraire(Fraction const & a, Fraction const & b)
{
    int numerateur { a.numerateur * b.denominateur - b.numerateur * a.denominateur };
    int denominateur { a.denominateur * b.denominateur };

    return { numerateur, denominateur };
}

int main()
{
    Fraction const un_demi { 1, 2 };
    Fraction const trois_quarts { 3, 4 };
    Fraction const deux_huitièmes { 2, 8 };

    // Arf, c'est dur à lire.
    Fraction const resultat { soustraire(additionner(un_demi, trois_quarts), deux_huitièmes) };
    std::cout << "1/2 + 3/4 - 2/8 font " << resultat.numerateur << "/" << resultat.denominateur << std::endl;

    return 0;
}

On peut également parler de l’affichage qui est galère et bien d’autres choses encore. Il faut donc trouver une solution à cet énorme problème de lisibilité. L’idéal, ça serait de pouvoir utiliser les opérateurs avec nos types personnels. Et heureusement, C++ ne nous laisse pas seuls face à ce problème !

…et la solution

Dans le chapitre introduisant les fonctions, nous avons vu que l’on peut surcharger une fonction pour la faire fonctionner avec des paramètres de types différents. Eh bien, bonne nouvelle, les opérateurs sont des fonctions comme les autres.

Autrement dit, tous les opérateurs, aussi bien arithmétiques (+, *, etc), de comparaison (<, >, ==, etc), ou de flux (<<, >>), sont appelables et surchargeables. Simplement, ce sont des fonctions avec une syntaxe spéciale faite pour rendre le code plus lisible.

Ainsi, on va pouvoir surcharger nos opérateurs pour nos types personnels et ainsi éliminer le problème d’expressivité du code qu’on a soulevé précédemment.

Cas concret — Les fractions

Les opérateurs arithmétiques

Nous allons aborder ici les opérateurs +, -, * et /, qui nous permettront de réaliser, respectivement, des additions, des soustractions, des multiplications et des divisions de fractions. Voyons déjà à quoi ressemblent, de manière générale, les opérateurs en question.

T operator+(T const & lhs, T const & rhs);
T operator-(T const & lhs, T const & rhs);
T operator*(T const & lhs, T const & rhs);
T operator/(T const & lhs, T const & rhs);

Dans notre cas, ils se traduisent ainsi. Notez que les prototypes de + et de - ressemblent beaucoup aux fonctions additionner et soustraire que nous avons écrites.

#include <iostream>
#include <string>

struct Fraction
{
    int numerateur;
    int denominateur;
};

// Ça ressemble à quelque chose de connu, hein ?
Fraction operator+(Fraction const & a, Fraction const & b)
{
    int numerateur { a.numerateur * b.denominateur + b.numerateur * a.denominateur };
    int denominateur { a.denominateur * b.denominateur };

    return { numerateur, denominateur };
}

Fraction operator-(Fraction const & a, Fraction const & b)
{
    int numerateur { a.numerateur * b.denominateur - b.numerateur * a.denominateur };
    int denominateur { a.denominateur * b.denominateur };

    return { numerateur, denominateur };
}

int main()
{
    Fraction const un_demi { 1, 2 };
    Fraction const trois_quarts { 3, 4 };
    Fraction const deux_huitièmes { 2, 8 };

    Fraction const resultat { un_demi + trois_quarts - deux_huitièmes };
    std::cout << "1/2 + 3/4 - 2/8 font " << resultat.numerateur << "/" << resultat.denominateur << std::endl;

    return 0;
}
1/2 + 3/4 - 2/8 font 64/64
Remarque

Le résultat de 64/64 peut vous surprendre, mais il est tout à fait normal, parce qu’on ne simplifie pas la fraction.

Entraînez-vous en implémentant aussi la multiplication et la division. Le code est très proche de celui qu’on vient d’écrire.

Les opérateurs de comparaison

Continuons en essayant maintenant de comparer des fractions entre elles, pour savoir notamment si deux fractions sont égales ou non, comparer la grandeur de deux fractions, etc. C’est le même principe, les prototypes peuvent se déduire assez facilement.

bool operator==(T const& lhs, T const& rhs);
bool operator!=(T const& lhs, T const& rhs);
bool operator<(T const& lhs, T const& rhs);
bool operator>(T const& lhs, T const& rhs);
bool operator<=(T const& lhs, T const& rhs);
bool operator>=(T const& lhs, T const& rhs);

Et si vous êtes effrayés à l’idée d’implémenter autant d’opérateurs différents, rappelez-vous de la logique booléenne. Vous verrez, ça réduit énormément la quantité de code.

#include <iostream>
#include <string>

struct Fraction
{
    int numerateur;
    int denominateur;
};

// On vérifie que a et b sont égaux.
bool operator==(Fraction const & a, Fraction const & b)
{
    Fraction const gauche { a.numerateur * b.denominateur, a.denominateur * b.denominateur };
    Fraction const droite { b.numerateur * a.denominateur, b.denominateur * a.denominateur };
    return gauche.numerateur == droite.numerateur && gauche.denominateur == droite.denominateur;
}

// Pour aller plus vite, il suffit de retourner la négation de la condition 'a == b'.
bool operator!=(Fraction const & a, Fraction const & b)
{
    return !(a == b);
}

// On vérifie si a est plus petit que b.
bool operator<(Fraction const & a, Fraction const & b)
{
    if (a.numerateur == b.numerateur)
    {
        return a.denominateur > b.denominateur;
    }
    else if (a.denominateur == b.denominateur)
    {
        return a.numerateur < b.numerateur;
    }
    else
    {
        Fraction const gauche { a.numerateur * b.denominateur, a.denominateur * b.denominateur };
        Fraction const droite { b.numerateur * a.denominateur, b.denominateur * a.denominateur };
        return gauche.numerateur < droite.numerateur;
    }
}

// Pour simplifier, il suffit de vérifier que a n'est ni inférieur, ni égal à b.
bool operator>(Fraction const & a, Fraction const & b)
{
    return !(a < b) && a != b;
}

// Pour simplifier, il suffit de vérifier que a est inférieur ou égal à b.
bool operator<=(Fraction const & a, Fraction const & b)
{
    return a < b || a == b;
}

// Pour simplifier, il suffit de vérifier que a est supérieur ou égal à b.
bool operator>=(Fraction const & a, Fraction const & b)
{
    return a > b || a == b;
}

int main()
{
    Fraction const trois_septieme { 3, 7 };
    Fraction const un_quart { 1, 4 };

    std::cout << std::boolalpha;
    std::cout << "Est-ce que 3/7 est plus petit que 1/4 ? " << (trois_septieme < un_quart) << std::endl;
    std::cout << "Est ce que 3/7 est plus grand que 1/4 ? " << (trois_septieme > un_quart) << std::endl;

    Fraction const neuf_vingteunieme { 9, 21 };
    std::cout << "Est-ce que 3/7 et 9/21 sont égaux ? " << (trois_septieme == neuf_vingteunieme) << std::endl;

    return 0;
}
Est-ce que 3/7 est plus petit que 1/4 ? false
Est ce que 3/7 est plus grand que 1/4 ? true
Est-ce que 3/7 et 9/21 sont égaux ? true

Aviez-vous pensé à l’astuce consistant à réutiliser le code déjà écrit pour simplifier celui à écrire ? Si oui, c’est que vous commencez à acquérir les bons réflexes de développeur. ;)

Les opérateurs de manipulation des flux

Il nous reste le problème d’affichage de notre type Fraction. Pour l’instant, nous sommes obligés d’aller chercher le numérateur et le dénominateur nous-mêmes. Si notre type contient plein d’informations, toutes aller les récupérer devient vite pénible.

Les opérateurs de manipulation des flux << et >> sont, fort heureusement, surchargeables. Voyez leur prototype.

std::ostream& operator<<(std::ostream & os, T const & t);
std::istream& operator>>(std::istream & is, T & t);

Ce qui peut surprendre, c’est la fait que l’on renvoie une référence sur un flux à chaque fois. En fait, c’est le flux que l’on reçoit en argument qui est renvoyé. Ceci a pour but d’enchaîner des appels à l’opérateur pour pouvoir écrire du code tel que celui-ci.

std::cout << t << std::endl;

Il sera interprété ensuite comme une suite d’appel à l’opérateur << par le compilateur.

operator<<(operator<<(std::cout, t), std::endl);
Type retourné

Notez que les types retournés sont std::ostream et std::istream. Ce sont des versions génériques de flux de sortie et d’entrée. Ainsi, en renvoyant ces types, on s’assure que notre type peut être utilisé avec des flux vers l’entrée / sortie standards, vers des fichiers, des chaînes de caractères, etc.

Dans notre cas, celui qui nous intéresse le plus, c’est operator<<, afin de pouvoir afficher nos fractions sur la sortie standard.

#include <iostream>
#include <string>

struct Fraction
{
    int numerateur;
    int denominateur;
};

Fraction operator+(Fraction const & a, Fraction const & b)
{
    int numerateur { a.numerateur * b.denominateur + b.numerateur * a.denominateur };
    int denominateur { a.denominateur * b.denominateur };

    return { numerateur, denominateur };
}

std::ostream & operator<<(std::ostream & flux, Fraction const & fraction)
{
    return flux << fraction.numerateur << "/" << fraction.denominateur;
}

int main()
{
    Fraction const un_quart { 1, 4 };
    Fraction const neuf_vingteunieme { 9, 21 };

    std::cout << "1/4 + 9/21 = " << un_quart + neuf_vingteunieme << std::endl;
    return 0;
}
1/4 + 9/21 = 57/84

Si vous le souhaitez, vous pouvez vous amuser à implémenter l’opérateur >>, pour, par exemple, être capable de créer des Fraction depuis des chaînes de caractères ou en lisant un fichier.

Plus et moins unaires

Il existe encore beaucoup d’opérateurs, nous n’allons donc pas pouvoir tous les aborder dans ce chapitre. J’aimerais néanmoins vous parler du plus et du moins unaires, qui permettent de faire des opérations comme +variable ou -variable, exactement comme des entiers (entre autres).

Le modèle est très simple, le voici ci-dessous pour les deux opérateurs.

T operator+(T const & t);
T operator-(T const & t);

Dans notre code, cela nous permet donc de créer, notamment, des fractions négatives.

#include <cmath>
#include <iostream>
#include <string>

struct Fraction
{
    int numerateur;
    int denominateur;
};

Fraction operator-(Fraction const & fraction)
{
    return { -std::abs(fraction.numerateur), std::abs(fraction.denominateur) };
}

std::ostream & operator<<(std::ostream & flux, Fraction const & fraction)
{
    return flux << fraction.numerateur << "/" << fraction.denominateur;
}

int main()
{
    Fraction const un_quart { 1, 4 };
    std::cout << "L'opposé de 1/4 est " << -un_quart << "." << std::endl;
    return 0;
}
L'opposé de 1/4 est -1/4.

Aller plus loin

Pour pratiquer, je vous propose de finir ce petit projet de fractions. Voici quelques idées en vrac.

  • Il n’y a aucune gestion des erreurs. Comment y remédier ?
  • il n’y a pas de tests unitaires.
  • Renvoyer l’opposé d’une fraction.
  • Inverser une fraction.
  • Rendre une fraction irréductible. Pour cela, il faut diviser le numérateur et le dénominateur par leur PGCD.

Et la bibliothèque standard ?

Elle utilise à foison la surcharge d’opérateurs. Et vous en avez utilisé énormément sans le savoir. Examinons quelques cas.

Concaténation de chaînes

std::string const hello { "Hello" };
std::string const world { "World" };

std::string const texte { hello + " " + world };

Quand vous concaténez des chaînes de caractères, vous faites en réalité appel à operator+ appliqué au type std::string. L’utilisation de cet opérateur est parfois décrié par certains, qui le jugent peu adapté. Reste que, celui-ci ou un autre, le code est raccourci et plus agréable à lire.

Accès à un élément

std::vector<int> const tableau { 1, 2, 3, 4 };
int const nombre { tableau[0] }; 

Dans l’exemple précédent, vous pouvez remplacer std::vector par std::array ou std::map, le principe est toujours le même. Derrière, c’est en effet operator[] qui est appelé. Celui-ci attend un entier comme argument, ce qui ne doit pas vous surprendre, puisque vous êtes habitués à la manipulation de tableaux maintenant.

Définition de littéraux std::string

using namespace std::litterals;
auto chaine { "Hello, chaine C++"s }

Vous vous demandez où est la surcharge dans le code précédent ? Allez, je vous aide, c’est le s en fin de chaîne. Eh oui, même quelque chose d’aussi simple est en fait une surcharge de operator"". Ce petit nouveau, arrivé avec C++11, permet de définir des suffixes, qui s’ajoutent après des littéraux, afin d’en changer le type.

Dans le cas exposé ci-dessus, le suffixe s permet de transformer le type du littéral "Hello, chaine C++", qui était un type hérité du C, en un std::string. Sans cette surcharge, il aurait fallu écrire std::string { "Hello, chaine C++" }, ce qui est plus long et moins lisible.

Exercice — Calculs avec des durées

Pratiquons pour mieux retenir avec un petit exercice, dans lequel on va implémenter quelques opérations pour un type Duree qui, comme son nom l’indique, représente une durée. Voici déjà comment on définit une durée dans notre code.

struct Duree
{
    int secondes;
};

Comme vous voyez, on stocke les durées sous forme de secondes. L’exercice consiste à implémenter une liste d’opérations.

  • Créer une durée à partir de secondes, minutes (optionnel), heures (optionnel).
  • Renvoyer la durée sous forme de secondes.
  • Renvoyer la durée sous forme de tuple {minutes, secondes}. Le signe de la durée sera porté par les minutes.
  • Renvoyer la durée sous forme de tuple {heures, minutes, secondes}. Le signe de la durée sera porté par les heures.
  • Additionner des durées.
  • Renvoyer l’opposé d’une durée.
  • Soustraire des durées.
  • Comparer des durées.
  • Injecter une durée dans un flux sortant, au format H:mm'ss". Le signe de la durée sera mis devant les heures.
Définition d’une durée

Ici, on considère une durée comme un intervalle de temps entre deux événements. Par conséquent, une durée peut être négative ; les opérations comme prendre l’opposé d’une durée ont donc un sens.

Indice

Voici, pour vous aider, les prototypes des fonctions à implémenter.

Prototypes
Duree creer_duree(int secondes, int minutes = 0, int heures = 0);
int en_secondes(Duree d);
std::tuple<int, int> en_minutes(Duree d);
std::tuple<int, int, int> en_heures(Duree d);

Duree operator+(Duree const& lhs, Duree const& rhs);
Duree operator-(Duree const& d);
Duree operator-(Duree const& lhs, Duree const& rhs);

bool operator==(Duree const& lhs, Duree const& rhs);
bool operator!=(Duree const& lhs, Duree const& rhs);
bool operator<(Duree const& lhs, Duree const& rhs);
bool operator>(Duree const& lhs, Duree const& rhs);
bool operator<=(Duree const& lhs, Duree const& rhs);
bool operator>=(Duree const& lhs, Duree const& rhs);

std::ostream& operator<<(std::ostream& os, Duree const & d);

Corrigé

Voici le corrigé complet avec les implémentations.

Corrigé complet
#include <iostream>
#include <tuple>

struct Duree
{
    int secondes;
};

Duree creer_duree(int secondes, int minutes = 0, int heures = 0);
int en_secondes(Duree d);
std::tuple<int, int> en_minutes(Duree d);
std::tuple<int, int, int> en_heures(Duree d);

Duree operator+(Duree const& lhs, Duree const& rhs);
Duree operator-(Duree const& d);
Duree operator-(Duree const& lhs, Duree const& rhs);

bool operator==(Duree const& lhs, Duree const& rhs);
bool operator!=(Duree const& lhs, Duree const& rhs);
bool operator<(Duree const& lhs, Duree const& rhs);
bool operator>(Duree const& lhs, Duree const& rhs);
bool operator<=(Duree const& lhs, Duree const& rhs);
bool operator>=(Duree const& lhs, Duree const& rhs);

std::ostream& operator<<(std::ostream& os, Duree d);

int main()
{
    

    return 0;
}

Duree creer_duree(int secondes, int minutes, int heures)
{
    return Duree{secondes + 60 * minutes + 3600 * heures};
}

int en_secondes(Duree d)
{
    return d.secondes;
}

std::pair<int, int> div_eucl_ordinaire(int i, int div)
{
    if(i >= 0)
        return {i / div, i % div};
    
    return {i / div - 1, i % div + 60};
}

std::tuple<int, int> en_minutes(Duree d)
{
    return div_eucl_ordinaire(en_secondes(d), 60);
}

std::tuple<int, int, int> en_heures(Duree d)
{
    auto [minutes, secondes] = en_minutes(d);
    auto heures{0};
    std::tie(heures, minutes) = div_eucl_ordinaire(minutes, 60);
    return {heures, minutes, secondes};
}


Duree operator+(Duree const& lhs, Duree const& rhs)
{
    return Duree{lhs.secondes + rhs.secondes};
}

Duree operator-(Duree const& d)
{
    return Duree{-d.secondes};
}

Duree operator-(Duree const& lhs, Duree const& rhs)
{
    return lhs + (-rhs);
}


bool operator==(Duree const& lhs, Duree const& rhs)
{
    return lhs.secondes == rhs.secondes;
}

bool operator!=(Duree const& lhs, Duree const& rhs)
{
    return !(lhs == rhs);
}

bool operator<(Duree const& lhs, Duree const& rhs)
{
    return lhs.secondes < rhs.secondes;
}

bool operator>(Duree const& lhs, Duree const& rhs)
{
    return lhs.secondes > rhs.secondes;
}

bool operator<=(Duree const& lhs, Duree const& rhs)
{
    return !(lhs > rhs);
}

bool operator>=(Duree const& lhs, Duree const& rhs)
{
    return !(lhs < rhs);
}

std::ostream& operator<<(std::ostream& os, Duree const & duree)
{
    auto [heures, minutes, secondes] = en_heures(duree);
    
    auto format = [](int i)
    {        
        auto res = std::to_string(i);
        if(std::size(res) == 1)
            res = '0' + res;
        return res;
    };
    
    return os << heures << ':' << format(minutes) << '\'' << format(secondes) << '"';
}

Sur le bon usage de la surcharge

La surcharge d’opérateurs est un outil puissant, il faut donc faire attention à ne pas l’utiliser n’importe comment. Notamment, C++ ne vérifie pas ce que vous mettez dans votre implémentation. Vous pourriez alors surcharger l’opérateur / pour effectuer une addition. Dans ce cas, vous violeriez la sémantique des opérateurs, c’est-à-dire que vous leur feriez faire autre chose que ce pour quoi ils sont prévus normalement. C’est une très mauvaise pratique, à bannir absolument.

L’autre point, c’est que, même si vous pouvez surcharger des opérateurs, cela n’a pas toujours de sens de le faire. Ainsi, additionner deux personnes ou deux comptes en banque n’a aucun sens et n’est pas correct. Nous reviendrons sur cet aspect plus tard dans le cours. Retenez que ce n’est pas parce que vous pouvez que vous devez.


En résumé

  • On peut définir des alias de type grâce au mot-clé using.
  • On peut simplifier l’utilisation de std::tuple et même de structures grâce aux décompositions.
  • La surcharge d’opérateurs permet d’augmenter la lisibilité et la qualité de notre code.
  • Il est possible de surcharger de nombreux opérateurs.
  • Elle doit être utilisée à bon escient, en ne faisant pas faire aux opérateurs des opérations qui violent leur sémantique.
  • La surcharge d’opérateurs est un bonus qui, s’il est appréciable, ne doit pas pour autant être constamment et systématiquement utilisé.