Considérations idéologiques – l'affectation

Plus récente modification : lundi, 26 novembre 2007 (où Meyers a la gentillesse de me relancer et de me souligner une erreur; c'est un chic monsieur!).

Il semble y avoir un débat dans le petit monde de C++ sur le type de retour d'un opérateur d'affectation. Ceci peut sembler être banal, un débat de spécialiste, mais l'affectation est une opération fondamentale et cela explique le caractère... épidermique de certaines des positions dans le débat (incluant la mienne).

J'annonce mes couleurs. À mon avis, un opérateur d'affectation bien écrit devrait retourner une référence constante. Un opérateur d'affectation devrait retourner l'opérande de gauche de l'affectation dans son état suite à l'affectation, ce qui explique qu'exprimer ceci :

a = b = c;

devrait être en tout temps logiquement équivalent à exprimer ceci :

a = (b = c); // l'affectation est associative de droite à gauche

ce sur quoi tous s'entendent, mais je pense aussi que ceci :

(a = b) = c;

devrait être rendu illégal par nos pratiques de programmation, du fait que dans l'expression a = b, bien que la bienséance et l'hygiène suggèrent de retourner a suite à la modification, le compilateur ne peut y voir que du feu si la programmeuse ou le programmeur retourne b, ce qui entraîne des effets secondaires très vilains si on exprime ceci :

(a = b) = c; // est-ce que remplacera a ou b? Ça devrait être a mais le compilateur n'en sait rien!

Le 26 juin 2006, j'ai écrit ceci à Herb Sutter, personne très respectée dans le milieu pour ses nombreux livres et articles... Un gars brillant et qui a eu des nombreuses bonnes idées), mais je n'ai toujours pas de réponse. Notez que XC++ réfère à son livre très connu (et, à bien des égards, très pertinent) Exceptional C++.

Hello there!

XC++ is quite an interesting book. I noticed something that made me react on page 168, though, with respect to the return type of operator=. In XC++, you recommend returning a non-const reference instead of a const reference.

I understand the point your are bringing forward as to the expected validity of (a = b) = c; (although I would heartily recommend against writing that expression), but I would argue against it on the grounds of safety.

The expected (the correct!) practice when writing operator= is indeed to return a reference to the left operand, but that is something that is, in the end, left in the hands of the programmer. The compiler cannot protect us from a programmer deciding to return a reference to the right operand (even if it involves a). Semantically, apart from operations that would try to use the return value of assignment operator in a « modifying way » (in a way that would require it not being const), doing so would not change a thing in programs (even though, when we teach programming, we do tell people that the return value of assignment is that of the leftmost operand after assignment has been performed).

As you pointed out in your book, some standard examples use a const reference as return type for the assignment operation. Using a const reference does seem conforming in most (all?) safe operations, and does prevent unpredictable side-effect inducing operations such as expression (a = b) = c; used in your example. XC++ claims that using a const reference return type for the assignment operator would prevent some standard library uses without mentioning which. I'm willing to believe you, of course, but since your suggestion involves the potential introduction of unpredictable side-effects in programs that (of course) should be written differently but (sadly) cannot be guarded against at compile time, I'd be interested in a convincing example to the contrary before suggesting it to my students. As such, if there's a link on your site that makes a convincing claim to the effect that an assignment operator returning a const reference is a practice to avoid, I'd be quite interested in reading it.

Cheers!

Plus tôt aujourd'hui, j'ai aussi écrit à Scott Meyers pour lui faire part de mes préoccupations :

Hello there!

When I had a few minutes during, I used them to browse the many books that retailers were suggesting and I used the opportunity to buy (and read) your three « Effective » books (... C++, More ... C++ and ...STL). Mostly good stuff, as I'm sure you already know, although More ... shows its age more than the others.

I have a problem with your treatment of operator=(), and I had the same problem when I read Sutter's Exceptional C++, for the same reasons. Let met explain.

You suggest, and I agree with you, that most binary operators should return a const value. For example, you recommend the following signature :

const rational operator+(const rational&, const rational&);

rational operator+(const rational&, const rational&);

The rationale (!) behind this suggestion is that this helps prevent unrecommandable behavior from unsuspecting programmers, such as :

rational a (3, 2);
rational b (1, 4);
(a + b) = c; // should be illegal; constness prevents this from compiling

So far, so good. My problem is the non-constness of the assignment operators you use throughout the book. Here's what bugs me.

Using such a signature as

class rational
{
// ...
public:
rational& operator=(const rational&);
// ...
};

allows users to write such nice things as

rational a (3, 2);
rational b (1, 4);
rational c; // (0, 1) I presume
c = a = b; // cool

but also allows this much less nice expression :

(c = a) = b; // nasty, nasty expression!

Now I know this is expected to be legal (albeit discutable) for primitive types like int or float, but for overloaded assignments it's just plain nasty.

The well-written assignment operator should return the left-hand side of the assignment following assignment. The problem is no compiler can enforce that semantic, since following assignment both sides are value-equal. The result of this is that while this form is legal :

rational& rational::operator= (const rational &r)
{
rational(r).swap(*this); // safe assignment idiom, cool stuff
return *this; // well-behaved assignment operator
}

... this one also is, and there's no way (outside of nasty side effects) to know the difference using the nasty, nasty expression above :

rational& rational::operator=(const rational &r)
{
rational(r).swap(*this); // safe assignment idiom, cool stuff
return r; // ouch! Really not nice, introduces possible side effects
}

This, in essence, is as bad as, and probably worse than, the case covered by returning const values from binary operators. The same argument applies in both cases, and I'm surprised not to see it applied here.

I read in one of Sutter's books (I think it's Exceptional C++ but it's been a while since I have read it) that there exist STL algorithms depending on returning a non-const reference from assignment, but I must admit not knowing what he has in mind; neither me nor any of my 200+ students yearly for ten years have met problems using a const reference return type from assignment, and my students program videogames and military flight simulators among others things. We all use the STL extensively.

I fall in the camp of those who think that the compiler is supposed to be our friend. We work with it (although we must use more than one in order to avoid the occasional faulty implementations of singular features here and there). We count on the compiler for inlining optimizations (without which STL becomes much less efficient!) and we strive for logical constness (your words, I like it!) as much as possible, writing code that implements logical constness while the compile enforces bitwise constness (sigh!). Returning a non-const reference from an assignment operator is one case when we would be working against ourselves, permitting bad client code to endanger badly written yet operational server code. I think the correct signature for assignment should be :

class rational
{
// ...
public:
const rational& operator=(const rational &);
// ...
};
const rational& rational::operator=(const rational &r)
{
rational(r).swap(*this);
return *this; // well-behaved assignment operator
}

This is in no way less efficient, it enforces logical constness in all cases and lets us work together with the compiler in order to avoid nasty side effects from badly written client code such as

(c + a) = b; // depends on whether c + a return a reference to c or a
// reference to a, which the compiler cannot reasonably infer

There. My two pennies. Cheers!

Meyers a eu la gentillesse de me répondre ceci :

This won't compile, because r is const, and operator='s return value is not. In fact, this error becomes possible only if you adopt your suggestion of having operator= return a ref-to-const (or if you pass r by value, which some experts have advocated, but I do not).

... et il a raison : ça ne passe pas sur un compilateur conforme. Les compilateurs à ma disposition aujourd'hui sont stricts et ne laissent pas ce problème passer (ceux que j'avais entre les mains au moment d'écrire ces messages n'avaient pas cette gentillesse). Ce respect plus strict du concept de constance est une excellente nouvelle!

Ma faute ici a été de ne pas tester le problème relevé sur un nombre suffisant de compilateurs et de ne pas « revalider » le tout avec les générations plus récentes des compilateurs pour lesquels le problème existait. Heureusement, j'ai tendance à faire ces tests en temps normal (et nous le devrions tous). Je placerai sous peu un lien vers un cas récent de code charmant qui fonctionnait sur le compilateur livré avec Visual Studio 2005... mais n'aurait pas dû fonctionner (ce que des tests sur plusieurs autres compilateurs, avec l'aide d'amis et d'ex-étudiant(e)s, ont permis d'établir). L'histoire est intéressante en soi, et fera un chic complément à ce dossier.