Remarque: les réponses ont été données dans dans un ordre spécifique , mais comme de nombreux utilisateurs trient les réponses en fonction des votes plutôt qu'en fonction du moment où elles ont été données, voici un index des réponses dans l'ordre dans lequel elles ont le plus de sens:
(Remarque: il s'agit d'une entrée dans FAQ C++ de Stack Overflow . Si vous souhaitez critiquer l'idée de fournir un FAQ - sous cette forme , alors la publication sur la méta qui a commencé tout cela serait l'endroit pour le faire. Les réponses à cette question sont surveillées dans le C++ chatroom , où le FAQ L’idée a commencé au début, alors votre réponse sera très probablement lue par ceux qui l’ont proposée.)
La plupart du travail chez les opérateurs surchargés correspond au code de la plaque de chaudière. Ce n'est pas étonnant, puisque les opérateurs ne sont que du sucre syntaxique, leur travail réel pourrait être effectué par des fonctions simples (et souvent transférées à celles-ci). Mais il est important que ce code de plaque de chaudière soit correct. Si vous échouez, le code de votre opérateur ne sera pas compilé ou celui de vos utilisateurs ne le sera pas ou le code de vos utilisateurs se comportera de manière surprenante.
Il y a beaucoup à dire sur l'affectation. Cependant, la plupart d’entre elles ont déjà été mentionnées dans la fameuse FAQ "Copie et échange" de GMan , je vais donc en sauter la majeure partie ici, en énumérant uniquement l’opérateur d’affectation idéal:
X& X::operator=(X rhs)
{
swap(rhs);
return *this;
}
Les opérateurs de décalage de bits <<
et >>
, bien qu'ils soient encore utilisés dans l'interface matérielle pour les fonctions de manipulation de bits dont ils héritent du C, sont devenus plus répandus en tant qu'opérateurs d'entrée et de sortie de flux surchargés dans la plupart des applications. Pour la surcharge de guidage en tant qu'opérateurs de manipulation de bits, voir la section ci-dessous sur les opérateurs arithmétiques binaires. Pour implémenter votre propre format personnalisé et votre logique d'analyse lorsque votre objet est utilisé avec iostream, continuez.
Les opérateurs de flux, parmi les opérateurs les plus souvent surchargés, sont des opérateurs à infixes binaires pour lesquels la syntaxe ne spécifie aucune restriction quant à savoir s'ils doivent être membres ou non. Comme ils modifient leur argument de gauche (ils modifient l’état du flux), ils doivent, selon les règles empiriques, être implémentés en tant que membres du type de leur opérande de gauche. Cependant, leurs opérandes de gauche sont des flux provenant de la bibliothèque standard. Bien que la plupart des opérateurs de sortie et d’entrée définis par la bibliothèque standard soient bien définis en tant que membres des classes de flux, lorsque vous implémentez des opérations de sortie et d’entrée ne peut pas changer les types de flux de la bibliothèque standard. C’est pourquoi vous devez implémenter ces opérateurs pour vos propres types en tant que fonctions non membres. Les formes canoniques des deux sont les suivantes:
std::ostream& operator<<(std::ostream& os, const T& obj)
{
// write obj to stream
return os;
}
std::istream& operator>>(std::istream& is, T& obj)
{
// read obj from stream
if( /* no valid object of T found in stream */ )
is.setstate(std::ios::failbit);
return is;
}
Lors de l’implémentation de operator>>
, la définition manuelle de l’état du flux n’est nécessaire que lorsque la lecture a réussi, mais le résultat obtenu n’est pas celui attendu.
L'opérateur d'appel de fonction, utilisé pour créer des objets de fonction, également appelé foncteurs, doit être défini comme un membre , donc il a toujours l'argument implicite this
des fonctions membres. Sinon, il peut être surchargé pour prendre un nombre quelconque d'arguments supplémentaires, y compris zéro.
Voici un exemple de syntaxe:
class foo {
public:
// Overloaded call operator
int operator()(const std::string& y) {
// ...
}
};
Usage:
foo f;
int a = f("hello");
Dans la bibliothèque standard C++, les objets fonction sont toujours copiés. Vos propres objets de fonction devraient donc être peu coûteux à copier. Si un objet de fonction doit absolument utiliser des données dont la copie est coûteuse, il est préférable de stocker ces données ailleurs et de faire en sorte que l'objet de fonction s'y réfère.
Les opérateurs de comparaison infixes binaires devraient, selon les règles empiriques, être implémentés en tant que fonctions non membres1. La négation de préfixe unaire !
devrait (selon les mêmes règles) être implémentée en tant que fonction membre. (mais ce n’est généralement pas une bonne idée de le surcharger.)
Les algorithmes de la bibliothèque standard (par exemple std::sort()
) et ses types (par exemple std::map
) s’attendent toujours à ce que operator<
soit présent. Cependant, les utilisateurs de votre type s'attendent à ce que tous les autres opérateurs soient présents . Par conséquent, si vous définissez operator<
, veillez à respecter la troisième règle fondamentale de surcharge de l'opérateur et définir également tous les autres opérateurs de comparaison booléens. La manière canonique de les implémenter est la suivante:
inline bool operator==(const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator!=(const X& lhs, const X& rhs){return !operator==(lhs,rhs);}
inline bool operator< (const X& lhs, const X& rhs){ /* do actual comparison */ }
inline bool operator> (const X& lhs, const X& rhs){return operator< (rhs,lhs);}
inline bool operator<=(const X& lhs, const X& rhs){return !operator> (lhs,rhs);}
inline bool operator>=(const X& lhs, const X& rhs){return !operator< (lhs,rhs);}
La chose importante à noter ici est que seulement deux de ces opérateurs font réellement quelque chose, les autres ne font que transmettre leurs arguments à l'un ou à l'autre pour effectuer le travail réel.
La syntaxe permettant de surcharger les opérateurs booléens binaires restants (||
, &&
) suit les règles des opérateurs de comparaison. Cependant, il est très peu probable que vous trouviez un cas d'utilisation raisonnable pour ces applications.2.
1Comme avec toutes les règles empiriques, il peut parfois y avoir des raisons de casser celle-ci aussi. Si c'est le cas, n'oubliez pas que l'opérande de gauche des opérateurs de comparaison binaires, qui pour les fonctions membres sera *this
, doit également être const
. Donc, un opérateur de comparaison implémenté en tant que fonction membre devrait avoir cette signature:
bool operator<(const X& rhs) const { /* do actual comparison with *this */ }
(Notez le const
à la fin.)
2Il convient de noter que la version intégrée de ||
et &&
utilise une sémantique de raccourci. Bien que celles définies par l'utilisateur (car elles constituent un sucre syntaxique pour les appels de méthode) n'utilisent pas de sémantique de raccourci. L'utilisateur s'attendra à ce que ces opérateurs aient une sémantique de raccourci, et leur code peut en dépendre. Par conséquent, il est vivement conseillé de NE JAMAIS les définir.
Les opérateurs unaires d’incrémentation et de décrémentation sont proposés à la fois en préfixe et en postfixe. Pour différencier les uns des autres, les variantes de postfixes prennent un argument supplémentaire. Si vous surchargez l'incrément ou le décrément, veillez à toujours implémenter les versions préfixes et postfixes. Voici l'implémentation canonique d'incrément, décrément suit les mêmes règles:
class X {
X& operator++()
{
// do actual increment
return *this;
}
X operator++(int)
{
X tmp(*this);
operator++();
return tmp;
}
};
Notez que la variante postfix est implémentée en termes de préfixe. Notez également que postfix fait une copie supplémentaire.2
La surcharge unaire moins et plus n'est pas très commune et probablement mieux évitée. Si nécessaire, ils devraient probablement être surchargés en tant que fonctions membres.
2Notez également que la variante postfixe fait plus de travail et est donc moins efficace à utiliser que la variante de préfixe. C'est une bonne raison de préférer généralement l'incrément de préfixe à l'incrément de postfix. Bien que les compilateurs puissent généralement optimiser le travail supplémentaire d’incrémentation postfixe pour les types intégrés, ils pourraient ne pas être en mesure de faire de même pour les types définis par l’utilisateur (ce qui pourrait être quelque chose d'aussi innocent qu'un itérateur de liste). Une fois que vous vous êtes habitué à faire i++
, il devient très difficile de vous rappeler de faire ++i
à la place lorsque i
n’est pas de type intégré (de plus, vous devrez changer de code lorsque changer un type), il est donc préférable de prendre l'habitude de toujours utiliser l'incrément de préfixe, sauf si postfix est explicitement requis.
Pour les opérateurs arithmétiques binaires, n'oubliez pas d'obéir à la surcharge de l'opérateur de la troisième règle de base: si vous fournissez +
, fournissez également +=
, si vous fournissez -
, n'omettez pas -=
, etc. Andrew Koenig aurait été le premier à constater que les opérateurs d’assignation composée peuvent être utilisés comme base pour leurs homologues non composés. C'est-à-dire que l'opérateur +
est implémenté en termes de +=
, -
est implémenté en termes de -=
etc.
Selon nos règles empiriques, +
et ses compagnons doivent être des non-membres, tandis que leurs homologues d'assignation composés (+=
etc.), modifiant leur argument de gauche, doivent en être membres. Voici l'exemple de code pour +=
et +
; les autres opérateurs arithmétiques binaires doivent être implémentés de la même manière:
class X {
X& operator+=(const X& rhs)
{
// actual addition of rhs to *this
return *this;
}
};
inline X operator+(X lhs, const X& rhs)
{
lhs += rhs;
return lhs;
}
operator+=
renvoie son résultat par référence, tandis que operator+
renvoie une copie de son résultat. Bien sûr, renvoyer une référence est généralement plus efficace que renvoyer une copie, mais dans le cas de operator+
, il n'y a aucun moyen de contourner la copie. Lorsque vous écrivez a + b
, vous vous attendez à ce que le résultat soit une nouvelle valeur. C'est pourquoi operator+
doit renvoyer une nouvelle valeur.3 Notez également que operator+
prend son opérande gauche par copie plutôt que par la référence const. La raison en est la même que la raison pour laquelle operator=
prend son argument par copie.
Les opérateurs de manipulation de bits ~
&
|
^
<<
>>
doivent être implémentés de la même manière que les opérateurs arithmétiques. Cependant, (sauf pour surcharger <<
et >>
pour les sorties et les entrées), il existe très peu de cas d'utilisation raisonnables pour les surcharger.
3Là encore, la leçon à tirer de ceci est que a += b
est, en général, plus efficace que a + b
et devrait être préféré si possible.
L'opérateur indice de tableau est un opérateur binaire qui doit être implémenté en tant que membre de la classe. Il est utilisé pour les types de type conteneur qui permettent l'accès à leurs éléments de données à l'aide d'une clé. La forme canonique de fournir ceux-ci est la suivante:
class X {
value_type& operator[](index_type idx);
const value_type& operator[](index_type idx) const;
// ...
};
Sauf si vous ne souhaitez pas que les utilisateurs de votre classe puissent modifier les éléments de données renvoyés par operator[]
(dans ce cas, vous pouvez omettre la variante non-const), vous devez toujours fournir les deux variantes de l'opérateur.
Si value_type est connu pour faire référence à un type intégré, la variante const de l'opérateur devrait mieux renvoyer une copie au lieu d'une référence const:
class X {
value_type& operator[](index_type idx);
value_type operator[](index_type idx) const;
// ...
};
Pour définir vos propres itérateurs ou pointeurs intelligents, vous devez surcharger l'opérateur de déréférencement de préfixe unaire *
et l'opérateur d'accès au membre du pointeur infixe binaire ->
:
class my_ptr {
value_type& operator*();
const value_type& operator*() const;
value_type* operator->();
const value_type* operator->() const;
};
Notez que ceux-ci aussi auront presque toujours besoin d'une version const et d'une version non const. Pour l'opérateur ->
, si value_type
est de type class
(ou struct
ou union
), un autre operator->()
est appelé récursivement, jusqu'à ce qu'un operator->()
renvoie une valeur de type non-classe.
L'adresse unaire de l'opérateur ne doit jamais être surchargée.
Pour operator->*()
voir cette question . Il est rarement utilisé et donc rarement surchargé. En fait, même les itérateurs ne le surchargent pas.
Continuer vers Opérateurs de conversion
En ce qui concerne la surcharge des opérateurs en C++, il existe trois règles de base à suivre . Comme pour toutes ces règles, il existe effectivement des exceptions. Parfois, des personnes se sont écartées d’elles et le résultat n’est pas mauvais, mais ces écarts positifs sont rares. À tout le moins, 99 de ces écarts que j'ai vus étaient injustifiés. Cependant, cela aurait tout aussi bien pu être 999 sur 1000. Vous devriez donc vous en tenir aux règles suivantes.
Chaque fois que la signification d'un opérateur n'est pas clairement claire et incontestée, il ne devrait pas être surchargé. À la place, fournissez une fonction avec un nom bien choisi.
Fondamentalement, la règle première en matière de surcharge des opérateurs, en son cœur même, dit: ne le faites pas . Cela peut paraître étrange, car il y a beaucoup de choses à savoir sur la surcharge d'opérateurs. C'est pourquoi de nombreux articles, chapitres de livres et autres textes traitent de tout cela. Malgré ces preuves apparemment évidentes , il n’ya que très 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 cet opérateur dans le domaine de l’application soit bien connue et incontestée. Contrairement à la croyance populaire, ce n'est presque jamais le cas.
Toujours s'en tenir à la sémantique bien connue de l'opérateur.
C++ ne pose aucune limite à la sémantique des opérateurs surchargés. Votre compilateur acceptera volontiers le code implémentant 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 entendu, cela suppose que la sémantique de l'opérateur dans le domaine d'application soit indiscutable.
Fournit toujours toutes les opérations connexes.
Les opérateurs sont liés les uns aux autres et à d'autres opérations. Si votre type prend en charge a + b
, les utilisateurs pourront également appeler a += b
. S'il prend en charge l'incrément de préfixe ++a
, ils s'attendent à ce que 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'assignation fonctionne également.
Continuer à La décision entre membre et non-membre .
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'utilisateur1. C'est-à-dire qu'au moins un des opérandes doit être d'un type défini par l'utilisateur. Comme avec d'autres fonctions surchargées, les opérateurs ne peuvent être surchargés qu'une seule fois pour un certain ensemble de paramètres.
Tous les opérateurs ne peuvent pas être surchargés en C++. Parmi les opérateurs qui ne peuvent pas être surchargés, on trouve: .
::
sizeof
typeid
.*
et le seul opérateur ternaire en C++, ?:
Parmi les opérateurs pouvant être surchargés en C++, citons:
+
-
*
/
%
et +=
-=
*=
/=
%=
(tout infixe binaire); +
-
(préfixe unaire); ++
--
(préfixe unaire et postfix)&
|
^
<<
>>
et &=
|=
^=
<<=
>>=
(tout infixe binaire); ~
(préfixe unaire)==
!=
<
>
<=
>=
||
&&
(tout infixe binaire); !
(préfixe unaire)new
new[]
delete
delete[]
=
[]
->
->*
,
(infixe binaire); *
&
(préfixe unaire) ()
(appel de fonction, infixe n-aire)Cependant, le fait que vous puissiez 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 ou comme fonctions non membres . Que vous soyez libre de choisir ou contraint d'utiliser l'un ou l'autre dépend de plusieurs critères.2 Un opérateur unaire @
3, appliqué à un objet x, est appelé soit comme operator@(x)
, soit comme x.operator@()
. Un opérateur d'infixe binaire @
, appliqué aux objets x
et y
, est appelé soit operator@(x,y)
, soit x.operator@(y)
.4
Les opérateurs implémentés en tant que fonctions non membres sont parfois amis du type de leur opérande.
1Le 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; à cette dernière appartiennent 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.
2Ceci est couvert dans ne partie ultérieure de cette FAQ.
3Le @
n'est pas un opérateur valide en C++, c'est pourquoi je l'utilise comme espace réservé.
4Le 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++ .
Les opérateurs binaires =
(affectation), []
(abonnement à un tableau), ->
(accès membre), ainsi que l'opérateur n-ary ()
(appel de fonction), doit toujours être implémenté comme fonctions membres, car la syntaxe du langage le requiert.
D'autres opérateurs peuvent être implémentés en tant que membres ou en tant que non-membres. Cependant, certaines d'entre elles doivent généralement être implémentées en tant que fonctions non membres, car leur opérande de gauche ne peut pas être modifiée par vous. Les plus importants d'entre eux sont les opérateurs d'entrée et de sortie <<
et >>
, dont les opérandes de gauche sont des classes de flux de la bibliothèque standard que vous ne pouvez pas modifier.
Pour tous les opérateurs où vous devez choisir de les implémenter en tant que fonction membre ou en tant que fonction non-membre, tilisez les règles empiriques suivantes décider:
Bien sûr, comme pour toutes les règles empiriques, il existe des exceptions. Si vous avez un type
enum Month {Jan, Feb, ..., Nov, Dec}
et que vous souhaitez surcharger les opérateurs d'incrémentation et de décrémentation, vous ne pouvez pas le faire en tant que fonctions membres, car en C++, les types enum ne peuvent pas avoir de fonctions membres. Donc, vous devez le surcharger en tant que fonction libre. Et operator<()
pour un modèle de classe imbriqué dans un modèle de classe est beaucoup plus facile à écrire et à lire lorsque cela est fait en tant que fonction membre en ligne dans la définition de la classe. Mais ce sont en effet de rares exceptions.
(Cependant, if vous faites une exception, n'oubliez pas le problème de const
pour l'opérande qui, pour les fonctions membres, devient l'argument implicite this
. Si opérateur en tant que fonction non membre prendrait son argument le plus à gauche en tant que référence const
, le même opérateur qu'une fonction membre doit avoir un const
à la fin pour faire *this
a const
référence.)
Continuer vers opérateurs communs à surcharger .
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, implicites et explicites.
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 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, c’est que la conversion implicite intervient même quand on ne s’y attend pas. Dans le code suivant, void f(const char*)
sera appelée car my_string()
n'est pas un lvalue , le premier ne correspond donc pas:
void f(my_string&);
void f(const char*);
f(my_string());
Les débutants comprennent facilement ce qui se passe et même les programmeurs C++ expérimentés sont parfois surpris car 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.
Contrairement aux opérateurs de conversion implicites, les opérateurs de conversion explicites n'interviendront jamais lorsque vous ne le pensez 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_;
};
Remarquez le explicit
. Désormais, lorsque vous essayez d'exécuter le code inattendu à partir des opérateurs de conversion implicites, vous obtenez une erreur de compilation:
prog.cpp: Dans la fonction 'int main ()': prog.cpp: 15: 18: erreur: pas de fonction correspondante pour l'appel à 'f (my_string)' prog.cpp: 15: 18: remarque: les candidats sont: prog.cpp: 11: 10: remarque: void f (my_string &) prog.cpp: 11: 10: remarque: inconnu conversion pour l'argument 1 de 'my_string' à 'my_string &' prog.cpp: 12: 10: remarque: void f (const char *) prog.cpp: 12: 10: remarque: non Conversion connue pour l'argument 1 de 'my_string' à 'const char *'
Pour appeler l'opérateur de distribution explicite, vous devez utiliser static_cast
, une distribution de style C ou une conversion de style constructeur (c'est-à-dire T(value)
).
Cependant, il existe une exception à cela: le compilateur est autorisé à convertir implicitement en bool
. De plus, le compilateur n'est pas autorisé à effectuer une autre conversion implicite après avoir été converti en bool
(un compilateur est autorisé à effectuer 2 conversions implicites à la fois, mais une seule conversion définie par l'utilisateur au maximum).
Etant donné que le compilateur ne lancera pas le message "après" bool
, les opérateurs de conversion explicites suppriment désormais la nécessité de idiome Safe Bool . Par exemple, les pointeurs intelligents antérieurs à C++ 11 utilisaient l'idiome Safe Bool pour empêcher les conversions en types intégraux. En C++ 11, les pointeurs intelligents utilisent à la place un opérateur explicite, car le compilateur n'est pas autorisé à convertir implicitement en un type intégral après qu'il a explicitement converti un type en bool.
Continuez à surcharge new
et delete
.
new
et delete
Remarque: Ceci ne concerne que le syntaxe de la surcharge new
et delete
, pas avec le 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, en ce qui concerne la surcharge d’opérateurs, je ne pourrai jamais lui rendre justice.
En C++, lorsque vous écrivez une nouvelle expression comme new T(arg)
, il se produit deux choses expression est évaluée: First operator new
est appelé pour obtenir de la mémoire brute, puis le constructeur approprié de T
est appelé pour transformer cette mémoire brute en un objet valide. De même, lorsque vous supprimez un objet, son destructeur est d'abord appelé, puis la mémoire est renvoyée à operator delete
.
C++ vous permet d’accorder 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. Vous pouvez affiner la gestion de la mémoire en écrivant vos propres operator new
et operator delete
.
La première des règles de base de la surcharge de l’opérateur - ne le fait pas - s’applique particulièrement à la surcharge new
et delete
. Presque les seules raisons pour surcharger ces opérateurs sont des problèmes de performances et contraintes de mémoire , et dans de nombreux cas, d'autres actions, comme , modifient les algorithmes utilisé, fournira un rapport coût/gain plus élevé que de tenter de modifier la gestion de la mémoire.
La bibliothèque standard C++ est fournie avec un ensemble d'opérateurs prédéfinis new
et delete
. 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 allouent/libèrent de la mémoire pour un objet, les deux derniers pour un tableau d'objets. Si vous fournissez vos propres versions, elles ne seront pas surchargées , mais remplaceront celles de la bibliothèque standard.
Si vous surchargez operator new
, vous devriez toujours surcharger le operator delete
correspondant, même si vous n'avez jamais l'intention de l'appeler. La raison en est que, si un constructeur émet lors de l'évaluation d'une nouvelle expression, le système d'exécution renvoie la mémoire au operator delete
correspondant au operator new
qui a été appelé pour allouer la mémoire pour créer l'objet. Si vous ne fournissez pas un operator delete
correspondant, le nom par défaut est appelé, ce qui est presque toujours faux.
Si vous surchargez new
et delete
, vous devez également envisager de surcharger les variantes du tableau.
new
C++ permet aux opérateurs nouveaux et supprimés de prendre des arguments supplémentaires.
Ce que l’on appelle placement nouveau vous permet de créer un objet à une adresse donnée qui est transmise à:
class X { /* ... */ };
char buffer[ sizeof(X) ];
void f()
{
X* p = new(buffer) X(/*...*/);
// ...
p->~X(); // call destructor
}
La bibliothèque standard est fournie avec les surcharges appropriées des opérateurs new et delete pour cela:
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 le placement indiqué ci-dessus, operator delete
n'est jamais appelé, à moins que le constructeur de X lève une exception.
Vous pouvez également surcharger new
et delete
avec d'autres arguments. Comme pour l'argument supplémentaire de placement new, ces arguments sont également répertoriés entre parenthèses après le mot-clé new
. Simplement pour des raisons historiques, ces variantes sont souvent également appelées placement new, même si leurs arguments ne permettent pas de placer un objet à une adresse spécifique.
Le plus souvent, vous souhaiterez affiner la gestion de la mémoire car la mesure a montré que des instances d'une classe spécifique ou d'un groupe de classes associées sont souvent créées et détruites, et que la gestion de la mémoire par défaut du système d'exécution, adaptée performance générale, traite inefficacement dans ce cas particulier. Pour améliorer cela, vous pouvez surcharger new et delete pour une classe spécifique:
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);
// ...
};
Ainsi surchargés, new et delete se comportent comme des fonctions membres statiques. Pour les objets de my_class
, l'argument std::size_t
sera toujours sizeof(my_class)
. Cependant, ces opérateurs sont également appelés pour les objets alloués dynamiquement de classes dérivées , auquel cas être plus grand que ça.
Pour surcharger le nouveau global et le supprimer, il suffit de remplacer les opérateurs prédéfinis de la bibliothèque standard par les nôtres. Cependant, cela doit rarement être fait.
Disons que vous avez:
struct Foo
{
int a;
double b;
std::ostream& operator<<(std::ostream& out) const
{
return out << a << " " << b;
}
};
Compte tenu de cela, vous ne pouvez pas utiliser:
Foo f = {10, 20.0};
std::cout << f;
Puisque operator<<
est surchargé en tant que fonction membre de Foo
, le LHS de l'opérateur doit être un objet Foo
. Ce qui signifie que vous devrez utiliser:
Foo f = {10, 20.0};
f << std::cout
ce qui est très non intuitif.
Si vous le définissez comme une fonction non membre,
struct Foo
{
int a;
double b;
};
std::ostream& operator<<(std::ostream& out, Foo const& f)
{
return out << f.a << " " << f.b;
}
Vous pourrez utiliser:
Foo f = {10, 20.0};
std::cout << f;
ce qui est très intuitif.