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 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 ora(ra);
int c = 3 ;
int& rc = c ;
optional 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'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 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 ora(ra) ;
optional 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 reference no to rebind, then you just
can't use optional 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'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 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 ora = ra ;
int b = 2 ;
int& rb = b ;
optional 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
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
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 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 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 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