Boost logo

Boost :

Subject: Re: [boost] [review] Review of Outcome v2 (Fri-19-Jan to Sun-28-Jan, 2018)
From: Andrzej Krzemienski (akrzemi1_at_[hidden])
Date: 2018-02-01 08:29:42


2018-02-01 2:58 GMT+01:00 Emil Dotchevski via Boost <boost_at_[hidden]>:

> On Wed, Jan 31, 2018 at 4:52 PM, Vinícius dos Santos Oliveira <
> vini.ipsmaker_at_[hidden]> wrote:
>
> > 2018-01-31 20:25 GMT-03:00 Emil Dotchevski <emildotchevski_at_[hidden]>:
> >
> >> Where is the compile time error? Example?
> >>
> >
> > Check the first Outcome tutorials.
> >
> > This won't compile:
> >
> > std::cout << (convert(text) / 2) << std::endl;
> >
> > This will:
> >
> > OUTCOME_TRY(foo, convert(text));
> > std::cout << (foo / 2) << std::endl;
> >
>
> Does this guarantee that your program is dealing with the error correctly?
> Not at all, if the context within which you call convert is not
> exception-safe, you'll get the same leak you'd get had you used exceptions.
> The difference is that you'd also get a compile error if you forget to
> check for errors, which a C++ programmer can't forget to do in this case.
>

The guarantee you get here is not that "failure-handling will be correct"
but that "if you forget to mention explicitly what failure-handling you
intend, the compiler will detect this".

If 99% of your intended failure-handling is "just skip subsequent
instructions" then you will appreciate exception handling, because this is
what they do. But if you get accustomed to these defaults, and you are
developing the remaining 1% case, and forget to state explicitly how you
want to handle you will get a silent error, which compiler will not warn
you about.

Now, as you get to the lower layers, the ratio is no longer 99% to 1%, and
in some cases may be like 60-40, and then the default offered by stack
unwinding is not good anymore.

The solution provided by Outcome requires you to be explicit about your
intentions, so that the compiler can help detect omissions: not every bug,
but simple omissions. This follows the philosophy "be explicit about your
intentions".

The example should have been this. You write code:

std::cout << (convert(text) / 2) << std::endl;

And compiler protests, that you forgot about failure-handling. And now you
get the second chance to decide how you want to handle. Maybe go with
default:

```
auto foo = convert(text);
int a = foo ? foo.value() : 1;
std::cout << a << std::endl;
```

Just output a different message:

```
if (auto foo = convert(text))
  std::cout << *foo << std::endl;
else
  std::cerr<< "sorry" << std::endl;
```

Or just propagate up as exceptions do:

```
OUTCOME_TRY(foo, convert(text));
std::cout << (foo / 2) << std::endl;
```

You are forced to make an explicit call. And this is desired in the
isolated portions of the program where there are no good defaults for this.

> This is similar to how you have to deal with object initialization if you
> don't use exceptions. Compare:
>
> class foo
> {
> bool init_called_;
> public:
>
> foo(): init_called_(false) { }
>
> result<void> init()
> {
> ....
> init_called_=true;
> }
>
> result<int> foo()
> {
> assert(init_called_);
> return compute_int();
> }
>
> result<void> bar( int x )
> {
> assert(init_called_);
> //use x
> }
> };
>
> ....
> foo a;
> OUTCOME_TRY(a.init());
> OUTCOME_TRY(x,a.foo());
> OUTCOME_TRY(a.bar(x));
>
> Now the C++ version:
>
> class foo
> {
> foo()
> {
> //initialize, throw on errors
> }
>
> int foo()
> {
> return compute_int();
> }
>
> void bar( int x )
> {
> //use x
> }
> }
>
> ....
> foo a;
> a.bar(a.foo());
>
> Note, I am not only making the point that the Outcome version is more prone
> to errors and more verbose, I am saying that even if you make no logic
> errors writing it, the result is a program that is _semantically_ identical
> to the one a C++ programmer would write using exceptions, complete with the
> bugs that could creep in if your code is not exception-safe.
>
> Literally, there is no upside to that style of programming.
>

Your example is unfair because you are not using idioms that come with
Outcome. The example with `result` would be:

```
class Foo
{
  private: Foo()
public:
  result<Foo> create(); // uses Foo's constructor, initializes, returns
failures through result

  int foo()
  {
    return compute_int();
  }

  void bar( int x )
  {
    //use x
  }
};

// ....
OUTCOME_TRY (a, foo::create());
a.bar(a.foo());
```

That is, provided that your intention is to propagate the failure up, but
you may decide that there is a more adequate way of handling failure in
your particular situation. For instance, you may have a secondary, safer
way of creating the desired object:

```
auto a = foo::create();
if (a.has_error() && backup_may_succeed(a.error()))
  a = foo::create_backup(); // like a named constructor

if (!a) return a.as_failure();
```

If you are accustomed to stack-unwinding idioms too much, you might forget
that "propagate up" is just one way of dealing with failures, and even if
it is the best, it is not the only one.

>
> > You can't forget to handle the error case. You can look at a function and
> > you'll immediately know if this function can fail or not.
> >
>
> In C++, we use noexcept to mark functions that can not fail.
>
>
> > You can't draw conclusions only by looking at anectodal evidence like
> >> this. If you want to demonstrate that Rust-style error handling is more
> >> robust, you have to have a control, the same real-world large scale
> project
> >> written/designed for using C++ exception handling. Neither of us has a
> >> control.
> >>
> >
> > At least you dropped the "axiomatic belief". But I haven't relied on
> > anecdotal evidence. I specifically told you to show me /any/ code on Rust
> > that is as complex as the examples I gave.
> >
>
> Yes, I understand that you like Rust and dislike C++. :)
>
> I am well aware of the difficulties in writing C++ code. Like I said, with
> some exceptions, there is usually a good reason for that.
>

It is not about "liking Rust". It is about choosing tools that best address
problems at hand. Maybe what you feel is that those cases where any other
failure-handling than "propagate up" are so very rare that they do not
warrant the addition of the library to Boost. That would be a
"quantitative" argument.

>
>
> > C++ allows complexity to just snowball and it is not a opt-in feature.
> > It's always there. Therefore, you always maintain useless state in your
> > head when analysing C++ code. Go ask C programmers what they think about
> > C++ exceptions.
> >
>
> You're making my point, that C++ exception handling is not an opt-in
> feature, it is inseparable part of the language, central to its object
> encapsulation model, etc. etc. I know what C programmers think about C++
> and about exceptions in particular. Do note that they don't propose C
> libraries to be added to Boost or C++, they don't want to touch either.
>
> By the way, I have a lot of respect for C and for people who choose it over
> C++. I've also learned the value of using C-style interfaces between
> modules in a large scale C++ projects.
>
>
> > So you have to be able to make your point in the abstract, which is very
> >> difficult. It is especially difficult because using either approach, a
> high
> >> quality development team can produce a robust program. But just because
> >> Google managed to build a phone using Java, it doesn't mean it was a
> good
> >> choice.
> >>
> >
> > You still ignore the fact that I'm talking about a qualitative (not
> > quantitative) property: more errors go to compile time errors. I have
> been
> > focusing on such aspect since the beginning of the discussion.
> >
>
> I demonstrated why this is false, but there is no such thing as free lunch.
> Everything has cost. It is important to keep this in mind when criticising
> C and C++.
>

Are you saying you have demonstrated that using a tool like Outcome does
not turn some omissions in failure-handling logic into compiler errors"? I
do not think that you did. What you demonstrated is that one can use
Outcome and still have a bug in one's code. And that is true, but no-one
was making this claim.

>
> > It's fact that you can't ignore the error because the code will fail to
> > compile. Error cases are explicit.
> >
> > Yes, using exception handling you can make mistakes that would be less
> >> likely to make otherwise. Also, using C++ you can make a whole lot of
> >> mistakes you can't make in Java, that's one reason why some people use
> Java
> >> and not C++, but this doesn't make them good C++ programmers -- nor it
> >> means that there is something wrong with C++.
> >>
> >
> > Agreed.
> >
> > There are very many examples of things being very difficult to express in
> >> C++ while being trivial in another language. There are usually good
> reasons
> >> for this, and there are many examples to the contrary. The ability to
> use
> >> RAII and exceptions to enforce postconditions is one such advantage.
> >>
> >
> > Not sure what kind of postcondition you're talking about this time.
> >
>
> What postcondition, that depends on the design, but the effect is that
> you're making it impossible for control to reach contexts for which it
> would be a logic error to execute in case a previous operation failed.
>

Just note that you do not need exceptions to "enforce" preconditions.
Actually term "enforce" is very fuzzy. What you do is to:

1. Signal the inability to satisfy function postconditions, so the contract
is "I either satisfy postconditions or throw an exception"
2. Ensure that subsequent operations are not invoked if the current
function failed to satisfy its postconditions.

But, in the isolated parts of the program, where you do not want to go with
a default response to failure, you can handle this in another way:

1. Signal the inability to satisfy postconditions by returning `result`
containing failed state.
2. Decide what you want to do next, when a function failed to satisfy
postconditions, and have the compiler make sure that a decision has been
made by the programmer: no default actions taken by compiler.

>
> > If it is class invariants, you can have them without exceptions.
> >
>
> You can have them, but you can't know if they're been established because
> in case of a failure, control may still reach code that requires them to be
> in place.
>

You can if you provide factory functions instead of constructors in your
types. Factory functions have other benefits; e.g., they have names, so you
do not get this uncertainty "which constructor got actually selected and
how it interprets the arguments".

>
> > And just one note about RAII: Rust does have RAII. No GC. A lot about the
> > language was inspired by C++ knowledge.
> >
>
> Yes, I get that you love Rust. :)
>

Choosing Outcome to solve certain problems is not about loving Rust, but
about choosing the most adequate tools to the problem at hand.

>
> > I'll point out again that in C++ it is impossible to report an error from
> >> a constructor except by throwing, and this is a good thing. I assume you
> >> disagree -- so what do you propose instead? It can't be OUTCOME_TRY,
> can it?
> >>
> >
> > I'm not proposing you to code differently.
> >
>
> My question still stands though. If you don't use exceptions, in C++, how
> do you protect users from calling member functions on objects that have not
> been initialized?
>

You do not have to get such objects if you provide factory functions. You
do not have to use constructors directly.
And also. You do not use Outcome to replace exceptions in your code. You
only use it in places when it is more adequate.

Regards,
&rzej;


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