Boost logo

Boost :

Subject: Re: [boost] [convert] Version in the Vault ready for review.
From: Vladimir Batov (batov_at_[hidden])
Date: 2009-03-31 06:00:41


> From: "Stewart, Robert" <Robert.Stewart_at_[hidden]>
>>
>> 1) with default: return the conversion result if successful,
>> return the provided default if not.
>
> What about callers that want a result object for types without a default
> constructor?
> Don't you always have to return the result type?

With or without the def.ctor convert<>::from can return both:

direction dir = convert<direction>::from(str, direction::up);
convert<direction>::result res = convert<direction>::from( "junk",
direction::up);

> Does your implementation class provide conversion operators to either the
> result type or TypeOut.

Yes, the implementation provides conversion to both --
convert<TypeOut>::result and TypeOut.

> If so, that complicates the implementation classes
> which will complicate any further extension.

It did not feel that way. In the latest implementation those implicit
conversions are as follows:

operator Result () const { return apply_(); }
operator TypeOut () const { return apply_().value(); }

> My idea is to always return the result type and let it handle the
> exceptions.

I've implemented "let the result type handle the exceptions" in the latest
(uploaded) incarnation. Exceptions are thrown from
convert<>::result::value() when appropriate.

[snip]
>> > The result object can have three states defined in an
>> > enumerated type: all's well, valid value but conversion
>> > failed, and conversion failed and no valid value.
>>
>> I feel with the above policies we already have all three
>> covered -- the with-default conversion request covers the
>> first two (all's well and conversion=failed+value=valid) and
>> the no-default conversion request covers #1 and #3 (all's
>> well and conversion=failed+value=no value).
>
> I don't think it covers everything.

I do not understand. You indicated that "the result object can have three
states". I described how (I think) they are covered. What did I miss?

[snip]
> There are important use cases for each function as I thought I showed.

Apologies if I somehow missed those.

> 1. valid() indicates whether value() will throw an exception.

The difference in our views seems that you feel valid() is needed to
indicate if value() will throw or not. My view is that "will throw or not"
is defined by the usage -- no-default will throw (if conversion failed),
with-default will not throw. If I were to save convert<>::result and pass it
to some other function, I'd agree that valid() would be useful with the
anticipated usage

convert<int>::result r1 = convert<int>::from(junk);
convert<int>::result r2 = convert<int>::from(junk, -1);

I immediately see that r1.value() will throw and r2.value() will not.
Therefore, I insist that valid() is superfluous.

> 2. converted() indicates whether the conversion succeeded.

That is provided via implicit converter to safe-bool. I.e. "if (r1)" is the
same as "if (r1.converted())".

> Each question is distinct and can be used by calling code to understand
> the result of the
> conversion request.

I do not dispute that. I only answer these two questions somewhat
differently (the valid() one anyway).

> When supplying a default for a type with a default constructor,
> the implication is that the caller wants valid() to be true regardless
> of converted(), so that the caller can always use the implicit
> conversion (or call value()).

With the default supplied valid() always returns true. I do not feel like
under the conditions the function adds much.

> If there is no default constructor, the caller may actually want to know
> whether the conversion worked, so converted() provides that information.

Yes, it is certainly available. Although the functionality is provided via
safe-bool implicit conversion which I think you advocated (I used to call it
good()).

> When the caller provides no default, either call yields the same
> information:
> either the conversion succeeded and value() will not throw,
> or it failed and value() will throw.

Again, I feel the valid()-returned information is available to me without
valid().

> ...
> but uniformity of interface is valuable: use the same result type with or
> without a default.

I do use the same convert<>::result type with or without a default. It's
just that its behavior depends on the way convert<>::from was called.

[snip]
>> T t = convert<T>::from(str, def, dothrow);
>> T t = convert<T>::from(str, def) >> dothrow;
>> T t = convert<T, dothrow>::from(str, def);
>
> The more I see it, the more I realize I don't care for "dothrow."

>From the context I assume you mean you do not *like* it, right?

> It differs from the common "nothrow" by just one letter and doesn't
> quite convey the intention anyway; there should only be an exception
> on conversion failure, not in all cases. What do you think of "require?"
> If we extend the result type with one more member function, required(),
> we could get what we want without needing dothrow:
>
> int i(convert<int>::from(str, def).required());

Somehow 'required' does not give me a strong enough clue it is about
throwing behavior. How about "throw_on_failure". With that I understand we
can supply conversion configuration information in many ways:

T t = convert<T>::from(str, def).throw_on_failure();
T t = convert<T>::from(str, def) >> throw_on_failure;

> except required() throws if !converted(), whereas value() throws if
> !valid().

Oops, so your required() behaves as convert<>::result::value().

> The point is that the caller indicates their wish for an exception
> when extracting the value rather than when writing the conversion call.

Yep, that's where we diverge. I happen to like registering "their wish for
an exception when writing the conversion call" for its simplicity. It's like
templates (vs. run-time polymorphism) -- describe your intentions upfront.
You seem to favor run-time (after the conversion) configurability with
valid, converted, required.

[snip]
> Anyway, I'm proposing the following interface on the result type:
>
> bool valid() const; // have value for value() to return
> bool converted() const; // conversion succeeded
> Out value() const; // return value; throw if !valid()
> Out required() const; // return value; throw if !converted()
> operator Out() const; // same as value()

I really hate to be a party-pooper but it feels overwhelming. I usually
struggle and uncomfortable with big interfaces with subtle differences as
above. With the implemented approach things seem considerably simpler and
straightforward:

TypeOut const& value() const;
operator safebool() const;

> Setting the locale, formatting, etc. via the extraction operator
> is sensible because those operate on a std::ostream.
> Indicating that the conversion should not throw an exception
> does not affect the ostream, so it ought not to be applied in the same
> way.

I interpret manipulators somewhat more liberally (rather than so firmly
associated with std::stream) and do not feel I cannot extend the concept
onto convert adding more manipulators in the process.

Best,
V.

P.S. Rob, it is a long email and very argumentative. In such circumstances I
often manage to get in trouble with my English. If anything sounded
offensive, impolite or anything of that sort it honestly was not intentional
and I apologize unreservedly.


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