L'opérateur d'affectation en C++ peut être rendu virtuel. Pourquoi est-ce obligatoire? Pouvons-nous aussi rendre d'autres opérateurs virtuels?
L'opérateur d'affectation n'a pas besoin d'être rendu virtuel.
La discussion ci-dessous concerne operator=
, Mais elle s'applique également à toute surcharge d'opérateur qui accepte le type en question et à toute fonction qui accepte le type en question.
La discussion ci-dessous montre que le mot-clé virtuel ne connaît pas l'héritage d'un paramètre en ce qui concerne la recherche d'une signature de fonction correspondante. Dans le dernier exemple, il montre comment gérer correctement l'affectation lorsqu'il s'agit de types hérités.
Les fonctions virtuelles ne connaissent pas l'héritage des paramètres:
La signature d'une fonction doit être la même pour que le virtuel entre en jeu. Ainsi, même si dans l'exemple suivant, operator = est rendu virtuel, l'appel n'agira jamais comme une fonction virtuelle dans D, car les paramètres et la valeur de retour de operator = sont différents.
Les fonctions B::operator=(const B& right)
et D::operator=(const D& right)
sont 100% complètement différentes et considérées comme 2 fonctions distinctes.
class B
{
public:
virtual B& operator=(const B& right)
{
x = right.x;
return *this;
}
int x;
};
class D : public B
{
public:
virtual D& operator=(const D& right)
{
x = right.x;
y = right.y;
return *this;
}
int y;
};
Valeurs par défaut et ayant 2 opérateurs surchargés:
Vous pouvez cependant définir une fonction virtuelle pour vous permettre de définir des valeurs par défaut pour D lorsqu'elle est affectée à une variable de type B. C'est même si votre variable B est vraiment un D stocké dans une référence de B. Vous n'obtiendrez pas le D::operator=(const D& right)
fonction.
Dans le cas ci-dessous, une affectation à partir d'objets 2D stockés dans 2 références B ... la substitution D::operator=(const B& right)
est utilisée.
//Use same B as above
class D : public B
{
public:
virtual D& operator=(const D& right)
{
x = right.x;
y = right.y;
return *this;
}
virtual B& operator=(const B& right)
{
x = right.x;
y = 13;//Default value
return *this;
}
int y;
};
int main(int argc, char **argv)
{
D d1;
B &b1 = d1;
d1.x = 99;
d1.y = 100;
printf("d1.x d1.y %i %i\n", d1.x, d1.y);
D d2;
B &b2 = d2;
b2 = b1;
printf("d2.x d2.y %i %i\n", d2.x, d2.y);
return 0;
}
Tirages:
d1.x d1.y 99 100
d2.x d2.y 99 13
Ce qui montre que D::operator=(const D& right)
n'est jamais utilisé.
Sans le mot-clé virtuel sur B::operator=(const B& right)
vous obtiendriez les mêmes résultats que ci-dessus mais la valeur de y ne serait pas initialisée. C'est à dire. il utiliserait la B::operator=(const B& right)
ne dernière étape pour lier le tout ensemble, RTTI:
Vous pouvez utiliser RTTI pour gérer correctement les fonctions virtuelles qui prennent votre type. Voici la dernière pièce du puzzle pour comprendre comment gérer correctement l'affectation lors du traitement de types hérités.
virtual B& operator=(const B& right)
{
const D *pD = dynamic_cast<const D*>(&right);
if(pD)
{
x = pD->x;
y = pD->y;
}
else
{
x = right.x;
y = 13;//default value
}
return *this;
}
Cela dépend de l'opérateur.
Le fait de rendre un opérateur d'affectation virtuel est de vous permettre de pouvoir le remplacer pour copier plus de champs.
Donc, si vous avez un Base & et que vous avez réellement un Derived & comme type dynamique, et que le Derived a plus de champs, les éléments corrects sont copiés.
Cependant, il existe alors un risque que votre LHS soit un dérivé et que le RHS soit une base.Par conséquent, lorsque l'opérateur virtuel s'exécute dans Derived, votre paramètre n'est pas un dérivé et vous n'avez aucun moyen d'en extraire des champs.
Voici une bonne discussion: http://icu-project.org/docs/papers/cpp_report/the_assignment_operator_revisited.html
Brian R. Bondy a écrit:
Une dernière étape pour lier le tout ensemble, RTTI:
Vous pouvez utiliser RTTI pour gérer correctement les fonctions virtuelles qui prennent votre type. Voici la dernière pièce du puzzle pour comprendre comment gérer correctement l'affectation lors du traitement de types hérités.
virtual B& operator=(const B& right) { const D *pD = dynamic_cast<const D*>(&right); if(pD) { x = pD->x; y = pD->y; } else { x = right.x; y = 13;//default value } return *this; }
Je voudrais ajouter à cette solution quelques remarques. La déclaration de l'opérateur d'affectation comme ci-dessus pose trois problèmes.
Le compilateur génère un opérateur d'affectation qui prend un argument const D & qui n'est pas virtuel et ne fait pas ce que vous pensez qu'il fait.
Le deuxième problème est le type de retour, vous renvoyez une référence de base à une instance dérivée. Probablement pas beaucoup de problème car le code fonctionne de toute façon. Il est toujours préférable de renvoyer les références en conséquence.
Troisième problème, l'opérateur d'attribution de type dérivé n'appelle pas l'opérateur d'attribution de classe de base (que se passe-t-il s'il y a des champs privés que vous souhaitez copier?), Déclarer l'opérateur d'attribution comme virtuel ne fera pas que le compilateur en génère un pour vous. C'est plutôt un effet secondaire de ne pas avoir au moins deux surcharges de l'opérateur d'affectation pour obtenir le résultat souhaité.
Compte tenu de la classe de base (identique à celle du post que j'ai cité):
class B
{
public:
virtual B& operator=(const B& right)
{
x = right.x;
return *this;
}
int x;
};
Le code suivant complète la solution RTTI que j'ai citée:
class D : public B{
public:
// The virtual keyword is optional here because this
// method has already been declared virtual in B class
/* virtual */ const D& operator =(const B& b){
// Copy fields for base class
B::operator =(b);
try{
const D& d = dynamic_cast<const D&>(b);
// Copy D fields
y = d.y;
}
catch (std::bad_cast){
// Set default values or do nothing
}
return *this;
}
// Overload the assignment operator
// It is required to have the virtual keyword because
// you are defining a new method. Even if other methods
// with the same name are declared virtual it doesn't
// make this one virtual.
virtual const D& operator =(const D& d){
// Copy fields from B
B::operator =(d);
// Copy D fields
y = d.y;
return *this;
}
int y;
};
Cela peut sembler une solution complète, ce n'est pas le cas. Ce n'est pas une solution complète car lorsque vous dérivez de D, vous aurez besoin de 1 opérateur = qui prend const B & , 1 opérateur = qui prend const D & et un opérateur qui prend const D2 & . La conclusion est évidente, le nombre de surcharges opérateur = () est équivalent au nombre de super classes + 1.
Considérant que D2 hérite de D, regardons à quoi ressemblent les deux méthodes operator = () héritées.
class D2 : public D{
/* virtual */ const D2& operator =(const B& b){
D::operator =(b); // Maybe it's a D instance referenced by a B reference.
try{
const D2& d2 = dynamic_cast<const D2&>(b);
// Copy D2 stuff
}
catch (std::bad_cast){
// Set defaults or do nothing
}
return *this;
}
/* virtual */ const D2& operator =(const D& d){
D::operator =(d);
try{
const D2& d2 = dynamic_cast<const D2&>(d);
// Copy D2 stuff
}
catch (std::bad_cast){
// Set defaults or do nothing
}
return *this;
}
};
Il est évident que l'opérateur = (const D2 &) ne fait que copier des champs, imaginez comme si c'était là. Nous pouvons remarquer un modèle dans les surcharges operator = () héritées. Malheureusement, nous ne pouvons pas définir de méthodes de modèle virtuel qui prendront soin de ce modèle, nous devons copier et coller plusieurs fois le même code afin d'obtenir un opérateur d'affectation polymorphe complet, la seule solution que je vois. S'applique également aux autres opérateurs binaires.
Comme mentionné dans les commentaires, le moins qui puisse être fait pour vous faciliter la vie est de définir l'opérateur d'affectation de superclasse le plus élevé = (), et de l'appeler à partir de toutes les autres méthodes operator = () de la superclasse. De plus, lors de la copie de champs, une méthode _copy peut être définie.
class B{
public:
// _copy() not required for base class
virtual const B& operator =(const B& b){
x = b.x;
return *this;
}
int x;
};
// Copy method usage
class D1 : public B{
private:
void _copy(const D1& d1){
y = d1.y;
}
public:
/* virtual */ const D1& operator =(const B& b){
B::operator =(b);
try{
_copy(dynamic_cast<const D1&>(b));
}
catch (std::bad_cast){
// Set defaults or do nothing.
}
return *this;
}
virtual const D1& operator =(const D1& d1){
B::operator =(d1);
_copy(d1);
return *this;
}
int y;
};
class D2 : public D1{
private:
void _copy(const D2& d2){
z = d2.z;
}
public:
// Top-most superclass operator = definition
/* virtual */ const D2& operator =(const B& b){
D1::operator =(b);
try{
_copy(dynamic_cast<const D2&>(b));
}
catch (std::bad_cast){
// Set defaults or do nothing
}
return *this;
}
// Same body for other superclass arguments
/* virtual */ const D2& operator =(const D1& d1){
// Conversion to superclass reference
// should not throw exception.
// Call base operator() overload.
return D2::operator =(dynamic_cast<const B&>(d1));
}
// The current class operator =()
virtual const D2& operator =(const D2& d2){
D1::operator =(d2);
_copy(d2);
return *this;
}
int z;
};
Il n'y a pas besoin d'une méthode set defaults car elle ne recevrait qu'un seul appel (dans l'opérateur de base = () surcharge). Modifie lorsque la copie des champs est effectuée au même endroit et que toutes les surcharges operator = () sont affectées et remplissent leur fonction.
Merci sehe pour la suggestion.
l'affectation virtuelle est utilisée dans les scénarios ci-dessous:
//code snippet
Class Base;
Class Child :public Base;
Child obj1 , obj2;
Base *ptr1 , *ptr2;
ptr1= &obj1;
ptr2= &obj2 ;
//Virtual Function prototypes:
Base& operator=(const Base& obj);
Child& operator=(const Child& obj);
cas 1: obj1 = obj2;
Dans ce concept virtuel ne joue aucun rôle car nous appelons operator=
Sur la classe Child
.
cas 2 & 3: * ptr1 = obj2;
* ptr1 = * ptr2;
Ici, l'affectation ne sera pas comme prévu. La raison d'être operator=
Est appelée à la place sur la classe Base
.
Il peut être rectifié en utilisant:
1) Casting
dynamic_cast<Child&>(*ptr1) = obj2; // *(dynamic_cast<Child*>(ptr1))=obj2;`
dynamic_cast<Child&>(*ptr1) = dynamic_cast<Child&>(*ptr2)`
2) Concept virtuel
Maintenant, en utilisant simplement virtual Base& operator=(const Base& obj)
n'aidera pas car les signatures sont différentes dans Child
et Base
pour operator=
.
Nous devons ajouter Base& operator=(const Base& obj)
dans la classe Child avec sa définition habituelle Child& operator=(const Child& obj)
. Il est important d'inclure une définition ultérieure, car en l'absence de cet opérateur d'affectation par défaut, il sera appelé (obj1=obj2
Pourrait ne pas donner le résultat souhaité).
Base& operator=(const Base& obj)
{
return operator=(dynamic_cast<Child&>(const_cast<Base&>(obj)));
}
cas 4: obj1 = * ptr2;
Dans ce cas, le compilateur recherche la définition de operator=(Base& obj)
dans Child
car operator=
Est appelé sur Child. Mais comme son type n'est pas présent et Base
ne peut pas être promu en child
implicitement, il le fera par erreur. (La conversion est requise comme obj1=dynamic_cast<Child&>(*ptr1);
)
Si nous implémentons selon les cas 2 et 3, ce scénario sera pris en charge.
Comme on peut le voir, l'affectation virtuelle rend l'appel plus élégant en cas d'affectations utilisant des pointeurs/référence de classe de base.
Pouvons-nous aussi rendre d'autres opérateurs virtuels? Oui
Elle n'est requise que lorsque vous voulez garantir que les classes dérivées de votre classe obtiennent tous leurs membres copiés correctement. Si vous ne faites rien avec le polymorphisme, vous n'avez pas vraiment à vous en préoccuper.
Je ne connais rien qui vous empêcherait de virtualiser tout opérateur que vous souhaitez - ce ne sont que des appels de méthode de cas spécial.
Cette page fournit une description excellente et détaillée de la façon dont tout cela fonctionne.
Un opérateur est une méthode avec une syntaxe spéciale. Vous pouvez le traiter comme n'importe quelle autre méthode ...