[optional] self-assignment and assignment of references

Hi people, Recently, here: http://lists.boost.org/MailArchives/boost/msg78947.php Joe Gottman pointed out that optional<T> fails on aliasing situations like self-assignment, and proposed to forward assignment to T's assignment operator (when the lhs is initialized). In the current implementation, Optional's assignment uses a destroy + copy-construct pattern. But in the upcoming 1.33 release, it will forward the assignment to T::operator=(T const&) whenever the optional<T> is originally initialized AND when T is not a reference. This is a slight change that I doubt would cause any troubles in practice. However, that change brings up a long standing issue: assignment of optional references. You may have noticed that assignment upon optional<T&> is currently undocumented. That was on purpose. The reason is that the destroy + copy-construct pattern used in the current implementation has the effect of rebinding references upon assignment. And that is something I never felt comfortable with. That is, the current implementation works like this: int a = 1 ; int b = 2 ; int& ra = a ; int& rb = b ; ra = rb ; // [1] Changes the value of 'a' to '2' optional<T&> ora(ra); int c = 3 ; int& rc = c ; optional<T&> orc(rc); ora = orc ; // [2] Does NOT changes the value of 'a' to '3' // Instead, it rebinds 'ora' to 'c' ora = rc ; // Same as before The issue of optional<T&>'s assignment (and direct-value assignment) was initially discussed here: http://lists.boost.org/MailArchives/boost/msg53225.php And then at length here: http://lists.boost.org/MailArchives/boost/msg54871.php And a couple of weeks ago, once again here: http://lists.boost.org/MailArchives/boost/msg79670.php After reading all the discussions thoroughly and thinking over again and again, I decided that in the upcoming 1.33 release, optional<T&> will RETAIN it's current rebinding semantics. Here's a short rationale for the decision (which will be included in the updated documentation) Rationale for Boost.Optional assignment semantics: One is logically drawn to expect optional<T> to follow the behaviour of T as much as possible. The reason being that the very purpose of optional<T> is to wrap objects of type T that may or may not exist. Though different users may choose to view optional<T>'s nature in different ways, I think that it's purpose is clear and so are the things users might want to do with it. So, what do users want to do when they assign to an optional<T>? Clearly-I think- if you assign an optional B to another optional A you want the postcondition of equivalence, that is: T a = <whatever> ; T b = <whatever> ; optional<T> oa(a) ; optional<T> ob(b) ; oa = ob ; assert(oa==ob); So far so good. But if T there is a reference, what actually involves the expected postcondition of equivalence? Let see: int a = 1 ; int b = 2 ; int& ra = a ; int& rb = b ; ra = rb ; // ra IS NOT rebound to b b = 3 ; assert(rb==3); assert(ra!=3); Then, given: optional<int&> ora(ra) ; optional<int&> orb(rb) ; ora = orb ; // what shall happen here? Following the logical expectation of requiring optional<T> to follow T as much as possible, one would expect: ora = orb ; to change the value of the referee (a) to that of 'b' But there is a catch: As the song goes, optional<T> can follow T down but not that far. :-) The reason is that assignment must operate on optional<T> itself, so it must be well defined in the case it is uninitialized, and there it just can't follow T's behaviour. If 'ora' is uninitialized, 'ora = orb' can ONLY rebind the wrapped reference to 'b' if you expect any kind of equivalence to be the postcondition of the assignment. It clearly can't just ignore the assignment cause there is no referee to change its value. AFAICS, there is no doubt about what shall assignment upon an uninitialized optional<T> do: INITIALIZE the wrapped object as a copy of the object wrapped by the optional<T> rvalue. If the wrapped object is a reference, then assignment upon an uninitialized optional cannot (IMO) do anything but rebind the reference. A choice optional<T>'s assigment operator has is what to do when it is already initialized. It can, for example, CHANGE the wrapped value forwarding the assignment to T::operator=() Alone by itself, this is a reasonable choice, but... If in your code you actually need assignment to an optional<T&> reference no to rebind, then you just can't use optional<T&> assignment directly unless you can precondition that the lvalue will always be already initialized. Yet in that case, you don't really need to use optional<T&>'s assignmet. You can use the actual reference assignment directly through the access operator, as in: *opt = newvalue ; If you're using optional<T>'s assigment is because it might be the case that the lvalue is uninitialized. In fact, the very purpose of the assignment operator is to allow a user not only to change the wrapped value but to initialize it if there is none. Optional<T>'s assignment could initialize the wrapped object when the lvalue is unitialized, and change it's value (forwarding to T::operator=) otherwise. In the case of non-reference values, the difference between copy-initialization and assignment is likely to be sufficiently insignificant as to prevent any problems arising from this duality. But in the case of reference values, that difference translates into rebinding or not. That is, if optional<T&> where to forward to T&::operator=() when the lvalue is already initialized (which is the only scenario where it can do that), the following would ocurr: int a = 1 ; int& ra = a ; optional<int&> ora = ra ; int b = 2 ; int& rb = b ; optional<int&> orb = rb ; ora = orb ; // Suppose this just changed the value // of 'a' to '2' as if it were 'ra=rb' ora = none ; // 'ora' is unitialized now ora = orb ; // 'ora' CAN ONLY rebind to 'b' now. As you can see, there is no way to consistently prevent optional<T&> assignment for rebinding. If you are writting generic code and rebinding references are not a choice (as it most likely is) then I'm sorry but you just can't use optional<T&> assignment no matter what the semantics are in the already initialized case. Furthermore, if you can't rebind references upon assignment, it is highly possible that you won't ever assign to an uninitialized optional reference (because how would you do it anyway?); so most likely, you can use *opt=newvalue safely. In C++, assignment to a reference doesn't rebind, but no lvalue reference can ever be uninitialized. The possibly-uninitialized extended state brought up by optional<T&> requires a change of rules so it can offer a _consistent_ semantic. In the most recent discussion about this, Brock Peabody suggested that I could make optional<T&> entirely non-assignable. After much thinking, I don't think that's neccesary. As I've shown above, generic code just can't rely on optional<T&> assignment whether it keeps its current rebinding semantics or not. As in the case of Spirit, the matter must be handled explicitely, so I don't think rebinding semantics would really cause code to be subtletly and silently (as far as the coder is aware) incorrect. Concluding: In the upcoming 1.33 release, optional<T> assignment will have the following semantic: If the lvalue optional<> is uninitialized: Copy-initializes the wrapped object as a copy of the object wrapped by the rvalue optional<>. If a reference is being wrapped, that copy-initialization implies binding (for the first time in this case) to the referee. If the lvalue optional<> is already initialized: If the wrapped object is not of reference type, assigns to the wrapped object the value of the object wrapped by the rvalue optional via forwarding to the wrapped operator=() If the wrapped object is of reference type, copy-initializes the wrapped reference rebinding it to the referee wrapped by the rvalue optional reference. Finally, Notice that all the rationale above referred to optional<T>::operator=( optional<T> const& ) The "direct-value" assignment operator currently available in optional<> as a convenience: optional<T>::operator=( T const& ) will keep the current semantic of being equivalent to the other, that is: opt = val ; is the same as: opt = optional<T>(val) Also, note that currently references are rebinding, so there is no change there. The change is that for non-reference types assignment will forward to T::operator=() when possibe. All this digression will be properly included in the documetation. Best Fernando Cacciola

"Fernando Cacciola" <fernando_cacciola@hotmail.com> writes:
Hi people,
Recently, here:
http://lists.boost.org/MailArchives/boost/msg78947.php
Joe Gottman pointed out that optional<T> fails on aliasing situations like self-assignment, and proposed to forward assignment to T's assignment operator (when the lhs is initialized).
In the current implementation, Optional's assignment uses a
<snip>
Rationale for Boost.Optional assignment semantics:
<snip>
Following the logical expectation of requiring optional<T> to follow T as much as possible, one would expect:
ora = orb ;
to change the value of the referee (a) to that of 'b'
But there is a catch:
As the song goes, optional<T> can follow T down but not that far. :-)
The reason is that assignment must operate on optional<T> itself, so it must be well defined in the case it is uninitialized, and there it just can't follow T's behaviour.
If 'ora' is uninitialized, 'ora = orb' can ONLY rebind the wrapped reference to 'b' if you expect any kind of equivalence to be the postcondition of the assignment. It clearly can't just ignore the assignment cause there is no referee to change its value.
I don't have a strong opinion about which semantics optional<T> should adopt, but IMO this logic is a flawed way to justify your choice. An optional<T> (where, in this case, T = U&) can be viewed as a variant type: T | void An uninitialized optional<T> takes the void branch of the "or." When orb holds a U& rather than a void, ora = orb is essentially replacing the void with a new U& and initializing it with whatever is in orb. Some (sensible, correct) choices simply can't be justified on the basis of logic. -- Dave Abrahams Boost Consulting www.boost-consulting.com

"David Abrahams" <dave@boost-consulting.com> escribió en el mensaje news:ur7h7yox6.fsf@boost-consulting.com...
"Fernando Cacciola" <fernando_cacciola@hotmail.com> writes:
Hi people,
Recently, here:
http://lists.boost.org/MailArchives/boost/msg78947.php
Joe Gottman pointed out that optional<T> fails on aliasing situations like self-assignment, and proposed to forward assignment to T's assignment operator (when the lhs is initialized).
In the current implementation, Optional's assignment uses a
<snip>
Rationale for Boost.Optional assignment semantics:
<snip>
Following the logical expectation of requiring optional<T> to follow T as much as possible, one would expect:
ora = orb ;
to change the value of the referee (a) to that of 'b'
But there is a catch:
As the song goes, optional<T> can follow T down but not that far. :-)
The reason is that assignment must operate on optional<T> itself, so it must be well defined in the case it is uninitialized, and there it just can't follow T's behaviour.
If 'ora' is uninitialized, 'ora = orb' can ONLY rebind the wrapped reference to 'b' if you expect any kind of equivalence to be the postcondition of the assignment. It clearly can't just ignore the assignment cause there is no referee to change its value.
I don't have a strong opinion about which semantics optional<T> should adopt, but IMO this logic is a flawed way to justify your choice. An optional<T> (where, in this case, T = U&) can be viewed as a variant type:
T | void
An uninitialized optional<T> takes the void branch of the "or." When orb holds a U& rather than a void,
ora = orb
is essentially replacing the void with a new U& and initializing it with whatever is in orb.
I suppose you're saying that given what you wrote, and the fact (left implicit) that when 'ora' takes the 'T' branch a variant type could just assign the referred value but not rebind, then the choice of what to do in the already initialized case just don't follow directly from the only possible option in the uninitialized case. If that's the case, you're right, though I didn't intend to make my choice appear as a logical consecuence of the constrain (when uninitialized it must bind to the reference). I rather intended to show the constrain as a mere motivation for the choice. Clearly others choice are still valid. Fernando Cacciola SciSoft

"Fernando Cacciola" <fernando_cacciola@hotmail.com> writes:
I suppose you're saying that given what you wrote, and the fact (left implicit) that when 'ora' takes the 'T' branch a variant type could just assign the referred value but not rebind, then the choice of what to do in the already initialized case just don't follow directly from the only possible option in the uninitialized case.
I think that's what I'm saying, yes.
If that's the case, you're right, though I didn't intend to make my choice appear as a logical consecuence of the constrain (when uninitialized it must bind to the reference).
Actually when uninitialized you can view it as though the reference isn't there to begin with. In fact, that's probably the correct view since that reference would be an uninitialized one, which as we know is not a valid state for a C++ program to be in.
I rather intended to show the constrain as a mere motivation for the choice. Clearly others choice are still valid.
I think you may be able to legitimately motivate the choice on the grounds of usability or lack of error-prone-ness, but I don't think the constraint works as a motivation. -- Dave Abrahams Boost Consulting www.boost-consulting.com
participants (2)
-
David Abrahams
-
Fernando Cacciola