Boost logo

Boost :

From: William E. Kempf (wekempf_at_[hidden])
Date: 2002-12-10 17:04:20


Fernando Cacciola said:
> From: "William E. Kempf" <wekempf_at_[hidden]>
>> OK, I've been thinking about the interface a bit more and want to
>> voice some other opinions. First, why were pointer semantics chosen
>> at all? Why couldn't you have gone with value semantics?
>>
>> optional<int> foo()
>> {
>> //...
>> }
>>
>> void bar(int value)
>> {
>> //...
>> }
>>
>> optional<int> res = foo();
>> if (res.initialized())
>> bar(res);
>>
>> I can think of a few reasons, but want to hear about them from the
>> designer, and want to see the rationale documented.
>>
> Fair enough.
> Originally, optional<> had value semantics, so I used it pretty much
> like in the examples you've shown.
> But I had problem: since it had value semantics, it was perfectly OK to
> write:
>
> optional<int> x = foo();
> if ( x == 0 )
> some();
>
> Or even: if ( x ) some(x);
>
> Meaning "compare the 'value' of x against 0".
>
> Therefore, I needed something explicitely different to test
> if it was initialized or not:
>
> optional<int> x = foo();
> if ( x.initialied() )
> some(x);

Which is what I illustrated above ;).

> But if I wrote:
>
> optional<int> x = foo();
> if ( x )
> some(x);
>
> it really looked as if it were testing if 'x' is initialized
> while it was acutally comparing the 'x' value against 0.

I'm not sure I find this compelling. Knowing that a test for
initialization is done through optional::initialized(), I'd never mistake
the if above for testing this. This would cause problems only with
someone unfamiliar with optional, but they are going to have problems no
matter what interface you choose until they read the documentation.

> Eventually, I realized that value semantics are not really appropiate to
> deal
> with unininitialized states since it blurs the distinction between
> testing against other values and testing whether it is initialized or
> not. So I wonder which other semantics would deal with it properly,
> and pointers poped into my mind quickly.

But pointers are just the inverse of the problem above. Test for validity
is bald, while access to the "value" requires some supporting syntax (in
this case, operator*). This leaves users in the exact same scenario where
a simple mistake leads to code that compiles but doesn't do what the
programmer meant. I think you're trying just a little too hard to make
the interface "idiot proof".

>> Now, assuming we stick with pointer semantics, let's make this as
>> close to the "normal" smart pointer interface as possible:
>>
>> template <typename T>
>> class optional
>> {
>> public:
>> optional();
>> explicit optional(T const& value);
>> optional(optional const& other);
>> template <typename U>
>> optional(optional<U> const& other);
>> ~optional();
>>
>> T& operator*() const;
>> T* operator->() const;
>> T* get() const;
>> // T* release();
>> void reset();
>> void reset(T const& value);
>> operator unspecified-bool-type() const;
>> };
>>
>>
> This interface is greatly simplified mainly because
> my interface offers deep-constantness:
>
> T const* peek() const ;
> T* peek();
> T const& operator*() const;
> prxy operator*() ;
>
> instead of:
>
> T* peek() const ;
> T& operator*() const;
>
> The proxy is needed *only* to achieve deep-constantness.
> If this feature weren't introduced, my interface would have looked just
> like yours.

I still don't see the need for the proxy. The interface I gave could
provide "deep-constantness" with out a proxy. I'm not sure where I stand
on whether or not optional<> should provide this, however.

>> Semantics and rationale:
>>
>> optional()
>>
>> Constructs an "uninitialized" optional.
>>
>> explicit optional(T const& value)
>>
>> Constructs an optional initialized to value through copy
>> construction.
>>
>> optional(optional const& other)
>>
>> Copy constructs an optional using the value-type's copy
>> constructor.
>>
>> template <typename U> optional(optional<U> const& other)
>>
>> Allows copy construction from "compatible" optionals (optionals
>> with
>> value types that can be used for constructing this optional's value
>> type). I'm not sure how important this one is for optional, to be
>> honest.
>>
> I've never needed to convert between optinals.
> On a related note, if deep-constantess is not supported
> (as in your interface), an implicit conversion from
> optional<T> to optional<T const> is needed because
> the user will have to use <T const> to attach constantess
> to the value.

Actually, it's still not needed.

optional<int> opt1(666);
optional<const int> opt2(*opt1);

Granted, it *IS* an important convenience, since you don't have to check
the initialized state, but it's not strictly necessary.

>> ~optional()
>>
>> If the optional is initialized, calls the value-type's destructor.
>>
>
> Why isn't assignment supplied?

Because I believe the reset is clearer, and safer.

>> T& operator*() const
>>
>> If the optional is not initialized, results in undefined behavior.
>> Otherwise, returns a reference to the value.
>>
>> T* operator->() const
>>
>> If the optional is not initialized, results in undefined behavior.
>>
>> T* get() const
>>
>> If the optional is not initialized, returns 0. Otherwise, returns
>> a
>> pointer to the value.
>>
>> Rationale: This provides the interface to determine whether or not
>> the
>> optional has been initialized. Nothing else is required.
>>
>> // T* release()
>>
>> Rationale: Since ownership can not be transferred, this interface
>> is
>> not supported.
>>
>> void reset()
>>
>> If the optional is initialized, calls the destructor and sets the
>> optional as "uninitialized".
>>
>> Rationale: Provides an interface to put the optional back into an
>> "uninitialized" state.
>>
>> void reset(T const& other)
>>
>> If the optional is initialized, calls the destructor. Copy
>> constructs
>> the value in place.
>>
>> Rationale: Provides an interface to set the value for an optional
>> in
>> an "uninitialized" state. If there's some meta-magic that would allow
>> you to use the value's operator=() if it has one this could be used as
>> an optimization instead of the destruct/construct idiom.
>>
> I still don't like to think of optional<> as a container, so I still
> don't like
> this reset.

You can "reset" a switch, and a switch isn't a container. I never thought
of std::auto_ptr as a container either. If you do, it contains a pointer,
not the object pointed to. In this case we contain a value, not the
pointer/reference. I think this play with words doesn't really lend any
more clarity... but maintaining a consistent interface with familiar
classes *does* lend more clarity.

> Currently, you can change the value of the optional directly without any
> performance
> penalty like this: *opt = new_val, so I don't see any real need for
> reset().

You can't do that when the optional is not initialized ;). The only
avenue open to the user in this case is to construct a temporary optional
and assign the current one to it. This is less efficient (or at least
could be).

>> operator unspecified-bool-type() const
>>
>> Returns an unspecified type (safe-bool idiom) that's convertable to
>> bool, with a true value indicating the optional is not in an
>> uninitialized state.
>>
>> Rationale: Allows for easier testing for the "uninitialized"
>> state,
>> especially for testing through assignments:
>>
>> if (opt = foo())
>>
> My motivation against this is just to prevent potential problems with
> optional<bool>, but if we collectively conclude that those problems are
> not so much important, I will add the safe_bool and
> make sure to tell every not to use optional<bool> (and use tribool
> instead) or else to use it with caution.

That (obviously) would be my vote.

>> This interface is nearly identical to std::auto_ptr, and as such
>> should be instantly understandable by most C++ programmers. It
>> provides all of the functionality present in the currently proposed
>> interface, yet doesn't provide unnecessary duplicate interfaces or any
>> use cases that should be ambiguous.
>>
> I notice that besides the deep-constantess difference, this interface is
> also nearly identical to the current one except that it has no
> value() nor initialized().

Uses get() instead of peek() and contains a mechanism (reset()) for
changing the value in a (hopefully efficient) consistent manner regardless
of the initialized state.

> initialized() is there because there is no safe_bool() and some people
> complained about having to use peek() or !; but I actually dislike it
> and
> I am about to remove it.
>
> value() is there just because the proxy() fails to convert to T&;
> and the proxy is needed by the deep-constantess,
> so if this feature were to be dropped, value() will be (happily) drop
> too.

Again, I still obviously don't comprehend the use of the proxy.

> I'm not sure what would be effect of removing deep-constantness.
> It would definitely allow the leaner interface you are proposing,
> but it would allow something odd-looking like this:
>
> void foo ( optional<int> const& opt )
> {
> *opt = 3 ;
> }
>
> Of course, it can be argued that if you wanted
> to convey true constantness you should write:
>
> void foo ( optional<int const> const& opt )
>
> instead.

Yes, and people can be made to use either with the same proficiency. I
honestly don't care too much about deep-constantness or not, I chose not
only for complete faithfulness to std::auto_ptr.

> This is a possibility.
> I could accept something like this, though I constantly see
> people complaining about lack of deep constantness on wrappers like
> optional<>
> What do others think?

William E. Kempf


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