Boost logo

Boost :

From: Fernando Cacciola (fcacciola_at_[hidden])
Date: 2002-12-11 22:20:10


"Augustus Saunders" <infinite_8_monkey_at_[hidden]> escribió en el mensaje
news:20021211232741.43695.qmail_at_web41313.mail.yahoo.com...
>
>
> I like the optional-as-pointer usage,

Great!

> and I would prefer it if I
> could write generic code where pointers and optionals are completely
> interchangeable. However, it isn't really possible to take this all
> the way, and we may be luring generic programmers into a trap by
> making them think pointers and optionals are interchangeable.
>
Indeed.
optional<> follows pointer semantics precisely because pointers are
ordinarily used to convey such 'optionality'.
I tried to allow optional<> to be used interchangably with a pointer as
much as possible.
This is why I insisted on non-member functions and operators.

But as you say, the analogy has a limit.

Let's explore this limit further:

I started saying that optional is not a smart pointer nor a container.
Now I think it is rather _both_ of those at partially (but non overlapping)
at the same time.
It is like a cross-breeded beast; partly smart-pointer and partly
(deep-copy) container.

I think that this cross-concept is inherent in its purpose and not a design
mistake. Let me explain:

Optional<> intends to model a possibly-unexistent value, thus

(A)
It must act like a value-base container at some extent:

optionsl<T> opt ;
// this part of the interface can be seen as creating the container empty.

optional<T> opt(3);
// this part of the interface has value semantics: it takes a value as
initialization parameter,
// and can be seen as 'storing' 3 inside the container.

opt.reset(3);
// this part of the interface also has value semantics
// and can be seen as replacing the previously contained value with the new
value 3.

opt.reset();
// this part of the interface can be seen as removing the object from the
container.

These methods give optional<> a deep-copy value-based container-like
interface.

(B)
But it also should act like a pointer at some other extent, because it is
extremely convenient to handle the possibly unexistent value through a
pointer, since a pointer has a well defined notion of a non-existent
pointee:

*opt = 3 ;
T v = *opt ;
opt->foo();
// This part of the interface has pointer semantics.
// It is defined only if opt is initialized, which correspond to a non-null
pointer pointing to a living object.
// As long as the optional is actually initialized,
// this code will work exactly the same if opt is replaced by a real
pointer, which is really convenient.
// If the optional is uninitialized, the behaviour of this code is
undefined, just exactly as it would be undefined if
// opt is replaced by a null-pointer.

int* p =get(opt); // or get_pointer(opt) to make it consistent with other
smart-pointers.
// This part of the interface also has pointer semantics.
// If the optional is initialized, the returned pointer points to the
optional value, which corresponds
// to a non-null pointer pointing to a living object.
// If the optional is not initialized, the pointer is NULL, which clearly
conveys the fact that it is uninitialized.
// Regardless of the initialization state, this code will work exactly the
same if opt is replaced by a real pointer.

if ( opt )
if ( opt == 0 )
if ( opt != 0 )
// This part of the interface also has pointer semantics.
// Regardless of the initialization state, this code will work exactly the
same if opt is replaced by a real pointer.

So,

Part of the interface has value semantics resembling a container: the ctor
and the reset() members.
This part of the interace prevents code using _this part_ of optional to be
replaced unchanged by a pointer, but,
actually, this is OK; since the code to initialize or reset the 'pointee'
which is handed through a pointer must
(typically) use dynamic-allocation so it must also change beyond the usage
of the optional/pointer variable.
Only a self-allocating deep-copy smart-pointer would work unchanged, which
is also OK.
Consider this example that shows this reasoning:

optional<int> foo()
{
  optional<int> result (3); // (1)
  return result ;
}
void bar()
{
   optional<int> opt = foo();
   if ( opt )
     use(*opt);
}

If I want to replace optional with an ordinary smart pointer, I also need to
change the line marked with (1):

shared_ptr<int> foo()
{
  shared_ptr<int> result ( >>new<< 3 ); // (1)
  return result ;
}
void bar()
{
   shared_ptr<int> opt = foo();
   if ( opt )
     use(*opt);
}

that is, I need to change the initialization code.
But the initiazation is not given by the pointer semantics so it makes sense
to me that I need to change it if I want to replace optional by a pointer.

The same situation ocurrs with reset(). It does not correspond to pointer
semantics thus I need to change the code using reset.

In fact, this last observation is what led me to accept William's reset()
even though it was a member-function: because assigning a value to an
uninitialized optional is an operation that has no correpondence with
pointers 'alone': it also requires the allocation of a new pointee besides
the assignment.
Therefore, being a member, the code will fail to compile if optional<> is
replaced by a pointer; showing the programmer that more than just a change
of type is needed.

Therefore, I conclude that the fact that part of the interface has value
instead of pointer semantic is correct, since it is the part of the
interface that deals with initialization/deinitialization of the optional
value. The rest of the interface deals with accessing the possibly
uninitialized optional so it has pointer semantics.

It seems to me that both parts of the interface are clearly differentiated
both in terms of purpose and in terms of syntaxis, so no confusion should
arise.

> This is of course because the two optionals never point to the same
> object:
>
> // assume p1, p2 initialized
> if (p1 == p2)
> {
> *p1 = 5;
> assert(5 == *p2); // holds true for pointers but not for optionals
> }
>
> Violating this assumption would break code that wants to treat them
> interchangeably.
>
Precisely.
Which leads me to the observation that if relational operators would compare
values (via the syntax (opt1 == opt2), they would correspond to the
value-based part of the interface; but now they won't be clearly cut out: is
this working with value based or pointer based semantics?
The ambiguity can become a real problem since the code would compile but
behave differently.

> Some people want to define:
>
> o1 == o2 iff (o1 && o2 && *o1 == *o2) || (!o1 && !o2)
>
> whereas (smart)pointers define:
>
> p1 == p2 iff (p1 && p2 && &*p1 == &*p2) || (!p1 && !p2)
>
Exactly.
(smart or not ) pointers never compare pointee values.

> I don't think that it would be possible to code around this point
> generically.

But is is. Just disallow relational operators between optionals :-)

This way, if the programmer wants to compare values he must write:

if (*o1 == *o2)

which would work exactly the same if optional is replaced by a pointer.

And if he wants to compare initialization states he must write:

if ( (!o1) == (!o2) ), or
if ( get(o1) == get(o2))

which would also work the same unchanged.

> Now, having said all of that, I still like the pointer
> notation when I understand the lack of equivalence because it is very
> concise.
>
Me too!

> Maybe this is just a new concept that needs to be defined
> so you can specify it in the requirements to your generic code.

Yes I think it is a new concept.

> Obviously, there is a lot of generic code that you could write that
> doesn't depend on either of the above equivalences--smart pointers
> are still useful even though they can't be used as iterators.
> Although this is more insidious; it's not the lack of an operator
> (++, --) but a redefinition with different semantics. Generic code
> that relied on pointer semantics would compile when given an optional
> but just wouldn't work.
>
I believe that if relational operators are banned, generic code given
optional will either compile and work as expected, or not compile.
In fact, this was one of my first design goals, so if I failed, I'd like to
see what can be done to fix it.

>
> So *I* like optional and would use it like it is, but I'm undecided
> whether or not this style interface is just begging for programmer
> error.
>
I think it isn't begging for errors but the oposite.
In fact, I _really_ tried to make sure of this...
For instance, It didn't had safe_bool just to prevent potential programmer
errors! :-)

...though the __wrong__ operator*() escaped me :-(

Fernando Cacciola


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk