Boost logo

Boost Users :

From: Fernando Cacciola (fernando_cacciola_at_[hidden])
Date: 2005-04-14 18:05:46


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


Boost-users list run by williamkempf at hotmail.com, kalb at libertysoft.com, bjorn.karlsson at readsoft.com, gregod at cs.rpi.edu, wekempf at cox.net