Menu

Surcharge d'opérateurs en C++ : opérateurs +, == et << personnalisés

La surcharge d'opérateurs en C++ permet à vos propres types de fonctionner avec les opérateurs intégrés comme +, == et <<. Découvrez les règles fonction membre vs non membre, comment surcharger les opérateurs de comparaison et de flux, et les pièges autour des types de retour et de l'opérateur d'affectation.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

Donnez à vos types un air natif

Vous savez déjà que std::string vous laisse écrire a + b pour concaténer et cout << s pour afficher. Ce ne sont pas des astuces spéciales du compilateur : ce sont des fonctions ordinaires aux noms étranges. La surcharge d'opérateurs est la fonctionnalité qui permet à vos classes de se brancher sur la même syntaxe, de sorte qu'un type Vector2 ou Money puisse être additionné, comparé et affiché exactement comme un int.

Le mécanisme est simple une fois qu'on le voit : une expression comme a + b est un raccourci. Le compilateur la réécrit en un appel à une fonction nommée operator+ et en cherche une qui corresponde aux types des opérandes. Définissez cette fonction pour votre classe et a + b se met soudain à fonctionner. C'est en réalité une forme spécialisée de surcharge de fonctions - les mêmes règles de résolution de noms s'appliquent, simplement avec des noms en forme d'opérateur.

Notez que la fonction prend les deux opérandes par const& : l'arithmétique ne doit pas modifier ses entrées, et les références évitent les copies. Elle renvoie un nouveau Vector2 par valeur - p + q doit produire un résultat frais sans toucher à p ni à q, tout comme 2 + 3 ne change pas le 2.

Membre vs non membre

Il y a deux endroits où définir un opérateur : comme membre de la classe ou comme fonction libre (non membre). En tant que membre, l'opérande de gauche est le this implicite, donc un opérateur binaire ne prend qu'un seul paramètre explicite :

Le const après la liste des paramètres compte : a + b ne doit pas modifier a, donc le membre est marqué const. Utilisez la forme membre pour les opérateurs intrinsèquement liés à l'opérande de gauche et qui n'ont pas besoin de conversions dessus - +=, [], (), -> et les opérateurs unaires comme -x ou ++x.

Le piège avec les membres : l'opérande de gauche ne peut pas être converti. Avec le operator+ membre ci-dessus, a + 50 fonctionne (50 se convertit en Money pour le côté droit), mais 50 + a ne compile pas - l'opérande de gauche 50 est un int, et vous ne pouvez pas ajouter une fonction membre à int. Un opérateur non membre corrige cela car les deux opérandes sont des paramètres explicites et peuvent tous deux être convertis :

Règle pratique : faites des opérateurs binaires symétriques (+, ==, *) des non-membres pour que les conversions fonctionnent des deux côtés ; faites des opérateurs qui doivent modifier l'opérande de gauche ou qui y sont liés (+=, [], =) des membres.

Surcharger l'opérateur de flux

L'opérateur de loin le plus souvent surchargé est << pour l'affichage. Vous ne pouvez pas en faire un membre de votre classe, car l'opérande de gauche est un std::ostream (comme cout), pas votre type - et vous ne possédez pas ostream. C'est donc toujours un non-membre qui prend le flux par référence non constante et le renvoie :

Deux détails font que cela fonctionne. Le flux est passé et renvoyé par référence (ostream&) - les flux ne peuvent pas être copiés, et renvoyer le même flux est ce qui vous permet de chaîner cout << "p = " << p << "\n". Chaque << renvoie le flux pour que le << suivant ait quelque chose à quoi se lier. Oubliez le return os; et le chaînage est rompu.

Opérateurs de comparaison

Pour comparer vos objets avec ==, < et consorts, surchargez les opérateurs de comparaison. Avant C++20, vous écriviez chacun d'eux à la main ; le piège principal est que operator< doit renvoyer un bool et définir un ordre cohérent :

Écrire les six comparaisons (==, !=, <, <=, >, >=) à la main est fastidieux et source d'erreurs. C++20 a ajouté l'opérateur de comparaison à trois voies <=> (le « vaisseau spatial »). Le définir par défaut, avec ==, génère toutes les comparaisons pour vous :

= default dit au compilateur de comparer les membres dans l'ordre de déclaration, ce qui correspond exactement à l'ordre lexicographique que vous écririez à la main. Préférez ceci sur les compilateurs modernes.

L'opérateur d'affectation et ses pièges

operator= (affectation par copie) est spécial : le compilateur en génère un pour vous, et pour les classes simples cette valeur par défaut est correcte. Vous n'avez besoin d'écrire le vôtre que lorsque votre classe gère une ressource - mémoire brute, descripteur de fichier - où une copie membre par membre serait incorrecte. La signature canonique renvoie *this par référence pour que les affectations puissent se chaîner (a = b = c) :

Deux pièges habitent cette courte fonction. D'abord, la vérification d'auto-affectation if (this == &other) : sans elle, a = a ferait delete[] data puis lirait dans other.data qui vient d'être libéré - comportement indéfini. Ensuite, l'ordre compte - dans une version écrite à la main, vous ne devez pas supprimer l'ancien tampon avant d'avoir copié sans risque le nouveau (une vraie implémentation alloue souvent d'abord, ou utilise l'idiome copy-and-swap, de sorte qu'une allocation ratée laisse l'objet intact).

Un piège plus large : ne surchargez pas les opérateurs de manière surprenante. Un operator+ qui modifie secrètement son opérande de gauche, ou un operator== non symétrique, embrouillera tous les lecteurs et cassera le code de la bibliothèque standard qui suppose les significations habituelles. Ne surchargez les opérateurs que lorsque l'opération est véritablement « de type addition » ou « de type égalité » pour votre type.

Suite : Spécificateurs d'accès

Remarquez comment chaque exemple a gardé ses membres de données private et exposé le comportement à travers une petite surface publique - constructeurs, opérateurs et quelques méthodes. Cette frontière entre ce qui est visible du monde extérieur et ce qui est caché à l'intérieur de la classe est contrôlée par les spécificateurs d'accès : public, private et protected. Nous verrons ensuite exactement ce que chacun permet, pourquoi des données private avec des méthodes publiques sont le choix par défaut pour une bonne encapsulation, et comment protected s'intègre dans l'héritage.

Questions fréquentes

Qu'est-ce que la surcharge d'opérateurs en C++ ?

La surcharge d'opérateurs vous permet de définir ce que signifient des opérateurs intégrés comme +, == ou << pour vos propres types. Vous écrivez une fonction au nom spécial - operator+, operator==, etc. - et le compilateur l'appelle dès que l'opérateur apparaît avec des opérandes de votre classe. C'est ainsi que string + string concatène et que cout << obj affiche un objet personnalisé.

Les opérateurs doivent-ils être des fonctions membres ou non membres (friend) en C++ ?

Utilisez une fonction membre quand l'opérande de gauche est votre propre classe et n'a pas besoin de conversions (par ex. +=, [], ()). Utilisez une fonction non membre (souvent friend) quand l'opérande de gauche peut être un type intégré ou quand vous voulez des conversions symétriques des deux côtés ; c'est obligatoire pour operator<< car l'opérande de gauche est un std::ostream, pas votre classe.

Quels opérateurs C++ ne peuvent pas être surchargés ?

Vous ne pouvez pas surcharger :: (résolution de portée), . (accès aux membres), .* (accès par pointeur sur membre), ?: (ternaire) et sizeof. Vous ne pouvez pas non plus inventer de nouveaux opérateurs ni changer l'arité ou la priorité d'un opérateur - + est toujours binaire avec la même priorité, qu'il additionne des int ou votre Vector2.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER