web-dev-qa-db-fra.com

Pourquoi l'opérateur d'affectation de copie doit-il renvoyer une référence / const?

En C++, le concept de renvoi de référence par l'opérateur d'affectation de copie n'est pas clair pour moi. Pourquoi l’opérateur d’attribution de copie ne peut-il pas renvoyer une copie du nouvel objet? De plus, si j'ai la classe A, et ce qui suit:

A a1(param);
A a2 = a1;
A a3;

a3 = a2; //<--- this is the problematic line

Le operator= est défini comme suit:

A A::operator=(const A& a)
{
    if (this == &a)
    {
        return *this;
    }
    param = a.param;
    return *this;
}
61
bks

À strictement parler, le résultat d'un opérateur d'affectation de copie n'a pas besoin de renvoyer une référence, bien que pour imiter le comportement par défaut utilisé par le compilateur C++, il doit renvoyer une référence non const à l'objet qui est affecté (une copie générée implicitement). L'opérateur d'affectation renverra une référence non constante - C++ 03: 12.8/10). J'ai vu pas mal de code qui renvoie void des surcharges d'affectation de copie, et je ne me souviens pas quand cela a causé un problème grave. Le retour de void empêchera les utilisateurs de "chaîner les affectations" (a = b = c;), et empêchera par exemple d'utiliser le résultat d'une affectation dans une expression de test. Bien que ce type de code soit loin d'être inconnu, je ne pense pas non plus qu'il soit particulièrement courant - en particulier pour les types non primitifs (à moins que l'interface d'une classe ne soit destinée à ce type de tests, comme les iostreams).

Je ne recommande pas que vous le fassiez, simplement en soulignant que c'est autorisé et que cela ne semble pas causer beaucoup de problèmes.

Ces autres SO questions sont liées (probablement pas tout à fait dupes) qui ont des informations/opinions qui pourraient vous intéresser.

64
Michael Burr

Un peu de clarification sur les raisons pour lesquelles il est préférable de retourner par référence pour operator= versus retour par valeur --- comme chaîne a = b = c fonctionnera correctement si une valeur est retournée.

Si vous renvoyez une référence, un travail minimal est effectué. Les valeurs d'un objet sont copiées dans un autre objet.

Cependant, si vous retournez par valeur pour operator=, vous appellerez un constructeur ET un destructeur CHAQUE fois que l'opérateur d'affectation est appelé !!

Donc, étant donné:

A& operator=(const A& rhs) { /* ... */ };

Alors,

a = b = c; // calls assignment operator above twice. Nice and simple.

Mais,

A operator=(const A& rhs) { /* ... */ };

a = b = c; // calls assignment operator twice, calls copy constructor twice, calls destructor type to delete the temporary values! Very wasteful and nothing gained!

En somme, il n'y a rien à gagner en retournant en valeur, mais beaucoup à perdre.

(Remarque: Ceci n'est pas destiné à répondre aux avantages de renvoyer une valeur l à l'opérateur d'affectation. Lisez les autres messages pour savoir pourquoi cela pourrait être préférable)

54
Alex Collins

Lorsque vous surchargez operator=, vous pouvez l'écrire pour renvoyer le type que vous voulez. Si vous en avez assez, vous pouvez surcharger X::operator= pour renvoyer (par exemple) une instance d'une classe complètement différente Y ou Z. Ceci est généralement fortement déconseillé cependant.

En particulier, vous souhaitez généralement prendre en charge le chaînage de operator= tout comme C le fait. Par exemple:

int x, y, z;

x = y = z = 0;

Dans ce cas, vous souhaitez généralement renvoyer une valeur l ou une valeur r du type affecté à. Cela ne laisse que la question de savoir si renvoyer une référence à X, une référence const à X ou un X (par valeur).

Renvoyer une référence const à X est généralement une mauvaise idée. En particulier, une référence const est autorisée à se lier à un objet temporaire. La durée de vie du temporaire est étendue à la durée de vie de la référence à laquelle il est lié - mais pas récursivement à la durée de vie de tout ce qui pourrait être affecté. Cela permet de renvoyer facilement une référence suspendue - la référence const se lie à un objet temporaire. La durée de vie de cet objet est étendue à la durée de vie de la référence (qui se termine à la fin de la fonction). Au moment où la fonction revient, la durée de vie de la référence et temporaire est terminée, donc ce qui est attribué est une référence pendante.

Bien sûr, renvoyer une référence non const ne fournit pas une protection complète contre cela, mais au moins vous fait travailler un peu plus dur. Vous pouvez toujours (par exemple) définir un local et lui renvoyer une référence (mais la plupart des compilateurs peuvent et vont également en avertir).

Le retour d'une valeur au lieu d'une référence pose des problèmes à la fois théoriques et pratiques. Sur le plan théorique, vous avez une déconnexion de base entre = signifie normalement et ce que cela signifie dans ce cas. En particulier, lorsque l'affectation signifie normalement "prendre cette source existante et attribuer sa valeur à cette destination existante", cela commence à signifier quelque chose de plus comme "prendre cette source existante, en créer une copie et affecter cette valeur à cette destination existante. "

D'un point de vue pratique, en particulier avant que les références de valeur ne soient inventées, cela pourrait avoir un impact significatif sur les performances - la création d'un tout nouvel objet au cours de la copie de A à B était inattendue et souvent assez lente. Si, par exemple, j'avais un petit vecteur et que je l'attribuais à un vecteur plus grand, je m'attendrais à ce que cela prenne tout au plus du temps pour copier des éléments du petit vecteur plus un (petit) frais généraux fixes pour ajuster la taille de le vecteur de destination. Si cela impliquait à la place deux copies, une de la source vers la temp, une autre de la temp vers la destination, et (pire) une allocation dynamique pour le vecteur temporaire, mon attente concernant la complexité de l'opération serait - entièrement détruit. Pour un petit vecteur, le temps d'allocation dynamique pourrait facilement être plusieurs fois supérieur au temps de copie des éléments.

La seule autre option (ajoutée en C++ 11) serait de renvoyer une référence rvalue. Cela pourrait facilement conduire à des résultats inattendus - une affectation en chaîne comme a=b=c; pourrait détruire le contenu de b et/ou c, ce qui serait assez inattendu.

Cela laisse le retour d'une référence normale (pas une référence à const, ni une référence rvalue) comme la seule option qui produit (raisonnablement) de manière fiable ce que la plupart des gens veulent normalement.

8
Jerry Coffin

C'est en partie parce que renvoyer une référence à soi est plus rapide que de retourner par valeur, mais en plus, c'est pour permettre la sémantique originale qui existe dans les types primitifs.

5
Puppy

operator= peut être défini pour renvoyer ce que vous voulez. Vous devez être plus précis quant à ce qu'est réellement le problème; Je soupçonne que le constructeur de copie utilise operator= en interne et qui provoque un débordement de pile, comme le constructeur de copie appelle operator= qui doit utiliser le constructeur de copie pour renvoyer A par valeur à l'infini.

4
MSN

Il n'y a aucune exigence de langage de base sur le type de résultat d'un operator= Défini par l'utilisateur, mais la bibliothèque standard a une telle exigence:

C++ 98 §23.1/3:

Le type d'objets stockés dans ces composants doit répondre aux exigences des types CopyConstructible (20.1.3) et aux exigences supplémentaires des types Assignable.

C++ 98 §23.1/4:

Dans le tableau 64, T est le type utilisé pour instancier le conteneur, t est une valeur de T et u est une valeur de (éventuellement const) T.

enter image description here


Le retour d'une copie par valeur prendrait toujours en charge le chaînage d'affectation comme a = b = c = 42;, Car l'opérateur d'affectation est associatif à droite, c'est-à-dire qu'il est analysé comme a = (b = (c = 42));. Mais renvoyer une copie interdirait les constructions dénuées de sens comme (a = b) = 666;. Pour une petite classe, renvoyer une copie pourrait être plus efficace, tandis que pour une classe plus grande, renvoyer par référence sera généralement le plus efficace (et une copie, prohibitivement inefficace).

Jusqu'à ce que j'apprenne la configuration standard requise pour la bibliothèque, je laissais operator= Retourner void, pour plus d'efficacité et pour éviter l'absurdité de prendre en charge un mauvais code basé sur les effets secondaires.


Avec C++ 11, il existe en outre l'exigence de type de résultat T& Pour default- l'opérateur d'affectation, car

C++ 11 §8.4.2/1:

Une fonction explicitement par défaut doit […] avoir le même type de fonction déclaré (sauf pour les qualificatifs ref éventuellement différents et sauf que dans le cas d'un constructeur de copie ou d'un opérateur d'affectation de copie, le type de paramètre peut être "référence à non const T", où T est le nom de la classe de la fonction membre) comme s'il avait été implicitement déclaré

3