Boost logo

Boost :

From: Ed Brey (brey_at_[hidden])
Date: 2000-06-07 10:05:37


After some thinking (and writing) about the tradeoffs, I've come up
with another solution to the nonthrowing cast problem, which IMHO has
advantages over a nonthrowing stream_cast.

template<typename T, typename U>
bool stream_convert(const T& source, U& target);

Following is a sequence of analyses that lead me to my conclusion.

From: "Mark Rodgers" <mark.rodgers_at_[hidden]>
>
> Not in this case. In this case we care about the extra code we
> would have to write otherwise. Using a stream_cast that throws
> would be a bit verbose:
>
> int n;
> try
> {
> n = stream_cast<int>(argv[1]);
> }
> catch (boost::whatever &)
> {
> n = 0;
> }
> if (n < 5)
> {
> std::cerr << "x must be a number greater than 4.";
> return EXIT_FAILURE;
> }

The code snippet is indicative of the kind of error lumping that atof
forces on users. However, for many applications, it isn't a good
coding practice, compared to something with tighter checking:

try
{
    if (argc < 2)
        throw std::exception("Not enough parameters.");
    int n = stream_cast<int>(argv[1]);
    if (n <= 4)
        throw std::exception("n must be greater than 4.");
    ...
    return EXIT_SUCCESS;
}
catch (const boost::bad_stream_cast&)
{
    std::cerr << "n must be an integer." "\n";
}
catch (const std::exception& e)
{
    std::cerr << e.what() << '\n';
}
return EXIT_FAILURE;

With not a lot more code, this provides more verbose error handling,
but it still has a problem. The error message "n must be an integer
may be separated by quite a distance from the location where n is
caught. (All the code in the "...".) So let's look at a couple of
ways to deal with this. (Parameter count check is removed for
brevity.) One is a nested try-catch:

try
{
    int n;
    try
        {n = stream_cast<int>(argv[1]);}
    catch (const boost::bad_stream_cast&)
        {throw std::exception("n must be an integer.");}
    if (n <= 4)
        throw std::exception("n must be greater than 4.");
    ...
    return EXIT_SUCCESS;
}
catch (const std::exception& e)
{
    std::cerr << e.what() << '\n';
}
return EXIT_FAILURE;

Another is for stream_cast to provide a boolean indicating the result:

try
{
    bool cast_ok;
    int n = stream_cast<int>(argv[1], ok);
    if (!cast_ok)
        throw std::exception("n must be an integer.");
    if (n <= 4)
        throw std::exception("n must be greater than 4.");
    ...
    return EXIT_SUCCESS;
}
catch (const std::exception& e)
{
    std::cerr << e.what() << '\n';
}
return EXIT_FAILURE;

I do have to admit, this version feels much easier than the nested
try-catch version. The pivotal factor is whether each failed
conversion needs to be handled specially. If many failures can be
lumped together with a single handling mechanism, then try-catch is
better; otherwise conventional conditional flow control is better.

Of course, this bool version is harkens back to the nothrow_t version
that Dave Abrahams posted on May 29 and the version taking a default
to use in case of error that I posted on May 30 (both of which have
been mysteriously reposted as new ideas). The advantage of using a
boolean version is that it provides stringstreams's failure indicator
without requiring a reserved value of the resulting type. This makes
it a generic solution; rather than one that applies only to a subset
of cases.

Now it's time to check that we aren't inventing a useless wrapper. We
should compare this against what we can do with stringstream alone:

    int n;
    if ((std::stringstream(argv[1]) >> n).fail())
        throw std::exception("n must be an integer.");
    if (n <= 4)
        throw std::exception("n must be greater than 4.");

Well, it is pretty small, but a little ugly. Also, it only works
where the source type happens to be string, although that is a common
case. Seems like a stream_cast to clean it up would still be worth
it. So what how about another stream_cast-ish option:

    int n;
    if (!boost::stream_convert(argv[1], n))
        throw std::exception("n must be an integer.");
    if (n <= 4)
        throw std::exception("n must be greater than 4.");

Hey! This is nice because the destination template type can now be
deduced by the compiler. It still doesn't rely on n having a reserved
value to indicate an error, so it's remains generically useful. I
chose "convert" rather than "cast" because everything with cast
returns the resultant type, and it's good to keep that naming
distinction.

Basically, this idea observes that given the generic case where a
special value of the resultant type can't be reserved, two items must
be returned. Additionally, it observes that when there is no
exception thrown on error, the client code will almost always want to
check for failure immediately.

One more benefit is that stream_convert also handles another (common?)
use case that I mentioned a while back: Handling a hard coded default
that can be overridden by a configuration file. In this case you may
have:

    int n = 123; // hard coded default
    boost::stream_convert(from_config_file, n);

In this case, sometimes you care about flagging and specifically
handling an error if the value in your config file is garbage,
sometimes an exception is good to handle it generically, and sometimes
you just want to ignore the error. For stream_convert, the semantic
would be to leave the resultant variable unchanged in there is a
failure. So it handles the first and third case, depending on whether
you check the return value. The second case is handled by the
throwing stream_cast.

For dealing with defaults, stream_cast also has the property that the
defaults don't need to be defined in the same place that the values
from the configuration file are converted. This can sometimes be an
advantage.

The thing that I like best about stream_convert is that it gets around
my biggest reservation with a stream_cast that takes a fallback
parameter. Passing in a parameter specifying what to do in case of
error seems like a bad idea, although I can't put my finger on any
exact rationale. I see it as being along the lines of:

    n = stream_cast<int>(argv[1], std::exception("n must be an
integer"));

where the function takes a parameter indicating what to do if there's
a problem. This seems like it should break some paradigm of good
programming, although I can't explicate what that is.

In summary, stream_convert has these advantages:
1. Doesn't require a reserved value of the resulting type.
2. Can easily be used directly in if statement to handle error
condition.
3. Doesn't take a parameter specifying the error handling action.


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