81 votes

opérateur d'affectation virtuelle C++

L'opérateur d'assignation en C++ peut être rendu virtuel. Pourquoi est-il nécessaire ? Peut-on rendre les autres opérateurs virtuels également ?

69voto

Brian R. Bondy Points 141769

Il n'est pas nécessaire de rendre l'opérateur d'affectation virtuel.

La discussion ci-dessous porte sur operator= mais elle s'applique également à toute surcharge d'opérateur qui prend en compte le type en question, et à toute fonction qui prend en compte 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. Le dernier exemple 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 virtual 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.

La fonction B::operator=(const B& right) y D::operator=(const D& right) sont totalement différents et considérés comme deux 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 présence de deux 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. Et ce, même si votre variable B est en réalité un D stocké dans une référence d'un B. Vous n'obtiendrez pas la fonction D::operator=(const D& right) función.

Dans le cas ci-dessous, une assignation de 2 objets D stockés à l'intérieur de 2 références B... la D::operator=(const B& right) est utilisé.

//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;
}

Imprimés :

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 auriez les mêmes résultats que ci-dessus mais la valeur de y ne serait pas initialisée. C'est-à-dire qu'il utiliserait la fonction B::operator=(const B& right)


Une dernière étape pour lier tout ça, le RTTI :

Vous pouvez utiliser RTTI pour gérer correctement les fonctions virtuelles qui prennent en compte votre type. Voici la dernière pièce du puzzle pour savoir comment gérer correctement les affectations lorsqu'il s'agit de types éventuellement 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;
}

0 votes

Brian, j'ai trouvé un comportement étrange représenté dans cette question : stackoverflow.com/questions/969232/ . Avez-vous des idées ?

0 votes

Je comprends vos arguments concernant l'utilisation de virtual, mais dans votre pièce finale vous utilisez 'const D *pD = dynamic_cast<const D*>(&right);', ce qui ne semble pas correct à mettre dans la classe de base. Pouvez-vous nous expliquer ?

3 votes

@Jake88 : Ce n'est pas dans la classe de base. C'est dans la surcharge de la classe dérivée de l'opérateur virtuel= déclaré en premier dans la classe de base.

26voto

Uri Points 50687

Cela dépend de l'opérateur.

L'intérêt de rendre un opérateur d'affectation virtuel est de vous permettre de le remplacer pour copier davantage de champs.

Ainsi, si vous avez une Base& et que vous avez en fait un Dérivé& comme type dynamique, et que le Dérivé a plus de champs, les éléments corrects sont copiés.

Cependant, il y a alors un risque que votre LHS soit un dérivé, et que le RHS soit une base, de sorte que lorsque l'opérateur virtuel s'exécute en dérivé, 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

9voto

Andrei15193 Points 21

Brian R. Bondy écrit :


Une dernière étape pour lier tout ça, le RTTI :

Vous pouvez utiliser RTTI pour gérer correctement les fonctions virtuelles qui prennent en compte votre type. Voici la dernière pièce du puzzle pour savoir comment gérer correctement les affectations lorsqu'il s'agit de types éventuellement 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 quelques remarques à cette solution. Le fait que l'opérateur d'affectation soit déclaré de la même manière que ci-dessus pose trois problèmes.

Le compilateur génère un opérateur d'affectation qui prend un fichier 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. Ce n'est probablement pas un gros problème puisque le code fonctionne de toute façon. Cependant, il est préférable de retourner les références en conséquence.

Troisièmement, l'opérateur d'assignation de type dérivé n'appelle pas l'opérateur d'assignation de la classe de base (que se passe-t-il s'il y a des champs privés que vous voulez copier ?), déclarer l'opérateur d'assignation comme virtuel ne fera pas en sorte 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'assignation pour obtenir le résultat souhaité.

Considérant la classe de base (la même que 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, mais ce n'est pas le cas. Ce n'est pas une solution complète car lorsque vous dérivez de D, vous aurez besoin d'un 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 de l'opérateur =() est équivalent au nombre de super classes + 1.

Étant donné que D2 hérite de D, regardons à quoi ressemblent les deux méthodes de l'opérateur =() 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 le opérateur =(const D2&) il suffit de copier les champs, d'imaginer comme si c'était là. Nous pouvons remarquer un modèle dans les surcharges de l'opérateur hérité =(). Malheureusement, il n'est pas possible de définir des méthodes de template virtuelles qui prendraient en charge ce modèle, nous devons copier et coller plusieurs fois le même code afin d'obtenir un opérateur d'assignation polymorphe complet, la seule solution que je vois. Cela s'applique également aux autres opérateurs binaires.


Modifier

Comme mentionné dans les commentaires, le moins que l'on puisse faire pour 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 d'opérateur de superclasse =(). De même, 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'un définir les valeurs par défaut car elle ne recevrait qu'un seul appel (dans la surcharge de l'opérateur de base =()). Les modifications apportées lors de la copie des champs sont effectuées à un seul endroit et toutes les surcharges de l'opérateur =() sont concernées et remplissent leur fonction.

Merci sehe pour la suggestion.

0 votes

Je pense qu'empêcher les constructeurs de copie générés par défaut est probablement le plus simple. D& operator=(D const&) = delete; . Si vous doit s'il est possible de le copier, alors il faut au moins relayer la mise en œuvre de la méthode virtuelle pour le cas de base. Très rapidement, cela devient un candidat pour le pattern Cloneable, donc vous pouvez utiliser virtuels privés comme dans GotW18 ainsi que d'être moins confus. En d'autres termes, les classes polymorphes ne sont pas compatibles avec la sémantique des valeurs. Elles ne le seront jamais. Le code montre que la dissimulation est difficile. Le fardeau est entièrement sur le développeur ...

0 votes

Ce n'est pas suffisant car si je supprime l'opérateur =(const D&) de D, je ne pourrai pas faire des choses comme D d1, d2 ; d1 = d2 ;

0 votes

Erm. Ce n'est pas ce que j'ai dit ? J'ai dit que ce serait plus facile. Plus de 60% du texte du commentaire traite de l'affaire ' si vous doit faire en sorte qu'il soit possible de le copier et de l'attribuer '... :)

4voto

sblom Points 15074

Elle n'est requise que lorsque vous voulez garantir que les classes dérivées de votre classe voient tous leurs membres copiés correctement. Si vous ne faites rien avec le polymorphisme, vous n'avez pas vraiment besoin de vous en préoccuper.

Je ne connais rien qui puisse vous empêcher de virtualiser n'importe quel opérateur que vous voulez - ils ne sont rien d'autre que des appels de méthode particuliers.

Cette page fournit une description excellente et détaillée de la façon dont tout cela fonctionne.

1 votes

Il y a quelques erreurs sur cette page. Le code qu'il utilise comme exemple de découpage n'est pas réellement découpé. Et c'est ignorer le fait que l'assignation est de toute façon illégale (inadéquation const/non-const).

3voto

dmckee Points 50318

Un opérateur est une méthode dotée d'une syntaxe spéciale. Vous pouvez le traiter comme n'importe quelle autre méthode...

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X