c++ - operator - Quelles sont les règles de base et les idiomes pour la surcharge de l'opérateur?




surcharge opérateur affectation c++ (5)

La syntaxe générale de la surcharge de l'opérateur en C ++

Vous ne pouvez pas changer la signification des opérateurs pour les types intégrés en C ++, les opérateurs ne peuvent être surchargés que pour les types définis par l'utilisateur 1 . C'est-à-dire qu'au moins l'un des opérandes doit être d'un type défini par l'utilisateur. Comme avec d'autres fonctions surchargées, les opérateurs peuvent être surchargés pour un certain ensemble de paramètres qu'une seule fois.

Tous les opérateurs ne peuvent pas être surchargés en C ++. Parmi les opérateurs qui ne peuvent pas être surchargés sont:. :: sizeof typeid .* et le seul opérateur ternaire en C ++, ?:

Parmi les opérateurs qui peuvent être surchargés en C ++, on trouve:

  • opérateurs arithmétiques: + - * / % et += -= *= /= %= (tous les infixes binaires); + - (préfixe unaire); ++ -- (préfixe unaire et postfixe)
  • manipulation de bits: & | ^ << >> et &= |= ^= <<= >>= (tout l'infixe binaire); ~ (préfixe unaire)
  • Algèbre booléenne: == != < > <= >= || && (tout l'infixe binaire); ! (préfixe unaire)
  • gestion de la mémoire: new new[] delete delete[]
  • opérateurs de conversion implicites
  • miscellany: = [] -> ->* , (tout l'infixe binaire); * & (tout préfixe unaire) () (appel de fonction, infixe n-aire)

Cependant, le fait que vous pouvez surcharger tout cela ne signifie pas que vous devriez le faire. Voir les règles de base de la surcharge de l'opérateur.

En C ++, les opérateurs sont surchargés sous la forme de fonctions avec des noms spéciaux . Comme avec les autres fonctions, les opérateurs surchargés peuvent généralement être implémentés soit comme une fonction membre du type de leur opérande gauche, soit comme des fonctions non membres . Si vous êtes libre de choisir ou lié à utiliser l'un dépend de plusieurs critères. 2 Un opérateur unaire @ 3 , appliqué à un objet x, est appelé en tant [email protected](x) ou en tant que [email protected]() . Un opérateur infixe binaire @ , appliqué aux objets x et y , est appelé soit comme [email protected](x,y) soit comme [email protected](y) . 4

Les opérateurs qui sont implémentés en tant que fonctions non membres sont parfois des amis du type de leur opérande.

1 Le terme "défini par l'utilisateur" peut être légèrement trompeur. C ++ fait la distinction entre les types intégrés et les types définis par l'utilisateur. Aux premiers appartiennent par exemple int, char, et double; à ce dernier appartient tous les types struct, class, union et enum, y compris ceux de la bibliothèque standard, même s'ils ne sont pas, en tant que tels, définis par les utilisateurs.

2 Ceci est couvert dans une partie ultérieure de cette FAQ.

3 Le @ n'est pas un opérateur valide en C ++, c'est pourquoi je l'utilise comme espace réservé.

4 Le seul opérateur ternaire en C ++ ne peut pas être surchargé et le seul opérateur n-aire doit toujours être implémenté en tant que fonction membre.

Continuez vers les trois règles de base de la surcharge des opérateurs en C ++ .

Note: Les réponses ont été données dans un ordre spécifique , mais comme de nombreux utilisateurs trient les réponses en fonction des votes plutôt que de l'heure à laquelle elles ont été données, voici un index des réponses dans l'ordre où elles ont le plus de sens:

(Note: Ceci est censé être une entrée pour la FAQ C ++ de Stack Overflow .) Si vous voulez critiquer l'idée de fournir une FAQ dans ce formulaire, alors l'affichage sur meta qui a commencé tout ceci serait l'endroit pour le faire. cette question est surveillée dans le salon de discussion C ++ , où l'idée de FAQ a commencé en premier lieu, ainsi votre réponse est très susceptible d'être lue par ceux qui ont eu l'idée.)


Les trois règles de base de la surcharge des opérateurs en C ++

Quand il s'agit de surcharger l'opérateur en C ++, il y a trois règles de base à suivre . Comme avec toutes ces règles, il y a effectivement des exceptions. Parfois, les gens ont dévié d'eux et le résultat n'était pas un mauvais code, mais de tels écarts positifs sont rares. À tout le moins, 99 des 100 déviations de ce genre que j'ai vues étaient injustifiées. Cependant, il aurait tout aussi bien pu être 999 sur 1000. Donc, vous feriez mieux de respecter les règles suivantes.

  1. Chaque fois que la signification d'un opérateur n'est pas claire et incontestée, elle ne doit pas être surchargée. Au lieu de cela, fournissez une fonction avec un nom bien choisi.
    Fondamentalement, la première et la principale règle pour les opérateurs de surcharge, en son cœur même, dit: Ne le faites pas . Cela peut sembler étrange, car il y a beaucoup à savoir sur la surcharge des opérateurs et donc beaucoup d'articles, de chapitres de livres et d'autres textes traitent de tout cela. Mais malgré cette évidence apparemment évidente, il y a seulement étonnamment peu de cas où la surcharge de l'opérateur est appropriée . La raison en est qu'il est en fait difficile de comprendre la sémantique derrière l'application d'un opérateur à moins que l'utilisation de l'opérateur dans le domaine d'application soit bien connue et incontestée. Contrairement à la croyance populaire, ce n'est presque jamais le cas.

  2. Toujours coller à la sémantique bien connue de l'opérateur.
    C ++ ne pose aucune limitation sur la sémantique des opérateurs surchargés. Votre compilateur accepte volontiers le code qui implémente l'opérateur binaire + à soustraire de son opérande droit. Cependant, les utilisateurs d'un tel opérateur ne soupçonneraient jamais l'expression a + b de soustraire a de b . Bien sûr, cela suppose que la sémantique de l'opérateur dans le domaine d'application est incontestée.

  3. Fournissez toujours tout d'un ensemble d'opérations connexes.
    Les opérateurs sont liés les uns aux autres et à d'autres opérations. Si votre type supporte a + b , les utilisateurs s'attendent à pouvoir aussi appeler a += b . S'il supporte l'incrément de préfixe ++a , ils s'attendent à ce a++ fonctionne également. S'ils peuvent vérifier si a < b , ils s'attendent certainement aussi à pouvoir vérifier si a > b . S'ils peuvent copier-construire votre type, ils s'attendent à ce que l'affectation fonctionne également.

Continuez vers la décision entre le membre et le non-membre .


Opérateurs de conversion (également appelés conversions définies par l'utilisateur)

En C ++, vous pouvez créer des opérateurs de conversion, des opérateurs qui permettent au compilateur de convertir entre vos types et d'autres types définis. Il existe deux types d'opérateurs de conversion, implicite et explicite.

Opérateurs de conversion implicites (C ++ 98 / C ++ 03 et C ++ 11)

Un opérateur de conversion implicite permet au compilateur de convertir implicitement (comme la conversion entre int et long ) la valeur d'un type défini par l'utilisateur en un autre type.

Voici une classe simple avec un opérateur de conversion implicite:

class my_string {
public:
  operator const char*() const {return data_;} // This is the conversion operator
private:
  const char* data_;
};

Les opérateurs de conversion implicites, comme les constructeurs à un seul argument, sont des conversions définies par l'utilisateur. Les compilateurs accordent une conversion définie par l'utilisateur lorsqu'ils tentent de faire correspondre un appel à une fonction surchargée.

void f(const char*);

my_string str;
f(str); // same as f( str.operator const char*() )

Au début, cela semble très utile, mais le problème avec cela est que la conversion implicite intervient même quand elle n'est pas attendue. Dans le code suivant, void f(const char*) sera appelée car my_string() n'est pas une lvalue , donc la première ne correspond pas:

void f(my_string&);
void f(const char*);

f(my_string());

Les débutants se trompent facilement et même les programmeurs C ++ expérimentés sont parfois surpris parce que le compilateur choisit une surcharge qu'ils ne soupçonnaient pas. Ces problèmes peuvent être atténués par des opérateurs de conversion explicites.

Opérateurs de conversion explicites (C ++ 11)

Contrairement aux opérateurs de conversion implicites, les opérateurs de conversion explicites ne seront jamais actifs lorsque vous ne les attendez pas. Voici une classe simple avec un opérateur de conversion explicite:

class my_string {
public:
  explicit operator const char*() const {return data_;}
private:
  const char* data_;
};

Notez l' explicit . Maintenant, lorsque vous essayez d'exécuter le code inattendu à partir des opérateurs de conversion implicites, vous obtenez une erreur de compilation:

prog.cpp: In function ‘int main()’:
prog.cpp:15:18: error: no matching function for call to ‘f(my_string)’
prog.cpp:15:18: note: candidates are:
prog.cpp:11:10: note: void f(my_string&)
prog.cpp:11:10: note:   no known conversion for argument 1 from ‘my_string’ to ‘my_string&’
prog.cpp:12:10: note: void f(const char*)
prog.cpp:12:10: note:   no known conversion for argument 1 from ‘my_string’ to ‘const char*’

Pour appeler l'opérateur de distribution explicite, vous devez utiliser static_cast , un cast de style C ou un cast de style constructeur (c'est-à-dire T(value) ).

Cependant, il y a une exception à cela: Le compilateur est autorisé à convertir implicitement en bool . De plus, le compilateur n'est pas autorisé à faire une autre conversion implicite après sa conversion en bool (un compilateur est autorisé à faire deux conversions implicites à la fois, mais seulement une conversion définie par l'utilisateur à max).

Étant donné que le compilateur ne lancera pas bool "passé", les opérateurs de conversion explicite suppriment désormais le besoin de l' idiome Safe Bool . Par exemple, les pointeurs intelligents avant C ++ 11 utilisaient l'idiome Safe Bool pour empêcher les conversions en types entiers. En C ++ 11, les pointeurs intelligents utilisent un opérateur explicite à la place parce que le compilateur n'est pas autorisé à convertir implicitement en un type intégral après avoir explicitement converti un type en booléen.

Continuer à surcharger new et delete .


Surcharge new et delete

Note: Ceci ne concerne que la syntaxe de surcharge new et delete , pas avec l' implémentation de tels opérateurs surchargés. Je pense que la sémantique de la surcharge new et delete mérite leur propre FAQ , dans le cadre de la surcharge de l'opérateur, je ne peux jamais rendre justice.

Notions de base

En C ++, lorsque vous écrivez une nouvelle expression comme new T(arg) deux choses se produisent lorsque cette expression est évaluée: First operator new est invoqué pour obtenir de la mémoire brute, puis le constructeur approprié de T est appelé pour transformer cette mémoire brute en objet valide. De même, lorsque vous supprimez un objet, son destructeur est d'abord appelé, puis la mémoire est renvoyée à l' operator delete .
C ++ vous permet de régler ces deux opérations: la gestion de la mémoire et la construction / destruction de l'objet dans la mémoire allouée. Ce dernier est fait en écrivant des constructeurs et des destructeurs pour une classe. La gestion de la mémoire est effectuée en écrivant votre propre operator new et en operator delete .

La première des règles de base de la surcharge de l'opérateur - ne le faites pas - s'applique particulièrement à la surcharge et à la delete . Presque les seules raisons de surcharger ces opérateurs sont les problèmes de performance et les contraintes de mémoire , et dans de nombreux cas, d'autres actions, comme les changements d'algorithmes , fourniront un ratio coût / gain beaucoup plus élevé que la gestion de la mémoire.

La bibliothèque standard C ++ est fournie avec un ensemble d'opérateurs new et delete prédéfinis. Les plus importants sont ceux-ci:

void* operator new(std::size_t) throw(std::bad_alloc); 
void  operator delete(void*) throw(); 
void* operator new[](std::size_t) throw(std::bad_alloc); 
void  operator delete[](void*) throw(); 

Les deux premiers allouer / désallouer la mémoire pour un objet, les deux derniers pour un tableau d'objets. Si vous fournissez vos propres versions, elles ne surchargeront pas, mais remplaceront celles de la bibliothèque standard.
Si vous surchargez l' operator new , vous devez toujours surcharger la operator delete correspondant, même si vous n'avez jamais l'intention de l'appeler. La raison en est que, si un constructeur lance pendant l'évaluation d'une nouvelle expression, le système d'exécution retournera la mémoire à l' operator delete correspondant à l' operator new qui a été appelé pour allouer la mémoire pour créer l'objet. ne pas fournir un operator delete correspondant, celui par défaut est appelé, ce qui est presque toujours faux.
Si vous surchargez new et delete , vous devriez également envisager de surcharger les variantes de tableau.

Placement new

C ++ permet aux opérateurs nouveaux et supprimés de prendre des arguments supplémentaires.
Le soi-disant placement nouveau vous permet de créer un objet à une certaine adresse qui est passée à:

class X { /* ... */ };
char buffer[ sizeof(X) ];
void f()
{ 
  X* p = new(buffer) X(/*...*/);
  // ... 
  p->~X(); // call destructor 
} 

La bibliothèque standard contient les surcharges appropriées des opérateurs new et delete pour ceci:

void* operator new(std::size_t,void* p) throw(std::bad_alloc); 
void  operator delete(void* p,void*) throw(); 
void* operator new[](std::size_t,void* p) throw(std::bad_alloc); 
void  operator delete[](void* p,void*) throw(); 

Notez que, dans l'exemple de code pour placement new donné ci-dessus, l' operator delete n'est jamais appelé, à moins que le constructeur de X ne lève une exception.

Vous pouvez également surcharger new et delete avec d'autres arguments. Comme avec l'argument supplémentaire pour placement new, ces arguments sont également listés entre parenthèses après le mot-clé new . Simplement pour des raisons historiques, de telles variantes sont souvent appelées placement new, même si leurs arguments ne sont pas pour placer un objet à une adresse spécifique.

Nouvelle classe et supprimer

Most commonly you will want to fine-tune memory management because measurement has shown that instances of a specific class, or of a group of related classes, are created and destroyed often and that the default memory management of the run-time system, tuned for general performance, deals inefficiently in this specific case. To improve this, you can overload new and delete for a specific class:

class my_class { 
  public: 
    // ... 
    void* operator new();
    void  operator delete(void*,std::size_t);
    void* operator new[](size_t);
    void  operator delete[](void*,std::size_t);
    // ... 
}; 

Overloaded thus, new and delete behave like static member functions. For objects of my_class , the std::size_t argument will always be sizeof(my_class) . However, these operators are also called for dynamically allocated objects of derived classes , in which case it might be greater than that.

Global new and delete

To overload the global new and delete, simply replace the pre-defined operators of the standard library with our own. However, this rarely ever needs to be done.


Why can't operator<< function for streaming objects to std::cout or to a file be a member function?

Let's say you have:

struct Foo
{
   int a;
   double b;

   std::ostream& operator<<(std::ostream& out) const
   {
      return out << a << " " << b;
   }
};

Given that, you cannot use:

Foo f = {10, 20.0};
std::cout << f;

Since operator<< is overloaded as a member function of Foo , the LHS of the operator must be a Foo object. Which means, you will be required to use:

Foo f = {10, 20.0};
f << std::cout

which is very non-intuitive.

If you define it as a non-member function,

struct Foo
{
   int a;
   double b;
};

std::ostream& operator<<(std::ostream& out, Foo const& f)
{
   return out << f.a << " " << f.b;
}

You will be able to use:

Foo f = {10, 20.0};
std::cout << f;

which is very intuitive.





c++-faq