Boost logo

Boost :

Subject: Re: [boost] [err] RFC
From: Gavin Lambert (gavinl_at_[hidden])
Date: 2015-11-26 18:26:32


On 27/11/2015 06:18, Domagoj Šarić wrote:
> Sorry, I misunderstood you...thought you were talking about converting to
> Ts _inside_ the combiner function...
> Hm..right I missed that this is actually a shady area in the case of
> multiple function parameters...probably because I just assumed that, even
> though the order of computation of individual parameter values is
> unspecified, each value would be fully computed before the compiler moves
> onto the next one...Trying to see what the standard actually says (n3797
> draft @ 1.9.15):
> "When calling a function (whether or not the function is inline), every
> value computation and side effect associated with any argument expression,
> or with the postfix expression designating the called function, is
> sequenced before execution of every expression or statement in the body of
> the called function. [Note: Value computations and side effects associated
> with different argument expressions are unsequenced. —end note]"[1]
> -> this end note might be interpreted as implying the negative, i.e. that
> value computations and side effects associated with _same_ argument
> expressions are _not_ unsequenced ... IOW that operations/instructions
> producing the value of parameter1 may not be interleaved with those
> producing the value of parameter2 (although the order whether parameter1
> or parameter2 is produced first is left unspecified).

No, that simply says that all parameters must be fully evaluated before
the first instruction of the called function executes. It does not say
anything about the sequencing of the parameter evaluation prior to the
actual call.

As far as I am aware, function parameters may be evaluated in any order,
including decomposed orders.

By that I mean that in the expression f(a.b, c().d()), then you are
guaranteed that a is evaluated before b and c() before d(), and both a.b
and c().d() before *the actual call of f*, but you have no guarantees
about the order of the evaluation of f to a method pointer vs. a vs. c().

So the compiler is perfectly free to evaluate c() first, then a, then f,
then b, then d(), and then finally call the method that f evaluated to.
  Or several other such combinations.

> I may very well be completely mistaken in this amateur 'exegesis' but,
> even if I am wrong and 'complete/non-interleaved' parameter values
> computation is not
> guaranteed fallible_result<> can still be made to work with the desired
> or 'good enough' [2]
> semantics by allowing multiple fallible_results to exist (removing the
> related asserts)
> and inserting an if (!std::uncaught_exception()) check before throwing
> (Scot Meyers'
> std::uncaught_exception() related gotw does not apply here).
> I can then improve the debugging logic to assert that at least one of the
> multiple fallible_results was inspected before leaving the current scope
> thereby still catching the contrived case of unexamined/untouched/unused
> local fallible_result variables 'hidden&forgotten' in autos. I say
> contrived because this is still better than what you currently have with
> 'regular' return codes, i.e. you don't get even an assertion for
> unexamined results. You do get compiler specific warnings about unused
> variables but you'd get those just as well for fallible results [3].

Yes, that would probably be an improvement.

>> Yes, within the function that gets called the && reference parameter
>> is an lvalue, not an rvalue, since it has a name. But all it takes to
>> make it an rvalue again is a call to std::move.
>>
>> And this does not seem like unreasonable behaviour in itself, if that
>> only occurs once (perhaps to construct the result_or_error within the
>> function).
>
> I'm not sure what you are getting at here...

I was referring to the hypothetical combiner function that accepted
multiple fallible_result<>&& parameters (since it consumes all of them),
and then returns its own fallible_result from one or more of them.

Within the function it will not be able to call any methods on the
fallible_result<>&& parameters (since they're actually lvalues now),
except for one very common case:
     auto a = std::move(paramA).as_result_or_error();

(And that this construct is not unreasonable as long as paramA is not
accessed after this point -- it's no different from any other move.)

I was also referring to the compiler errors that would be generated from
"improper use" being the same ones that people are likely to reflexively
add std::move() calls to resolve.

>> The only way it works is to explicitly separate out the calc calls,
>> either as:
>>
>> foo_error_t a = calcA();
>> foo_error_t b = calcB();
>>
>> or as:
>>
>> auto a = calcA().as_result_or_error();
>> auto b = calcB().as_result_or_error();
>>
>> (and woe betide you if you accidentally use "auto" in the first case)
>
> What woe? You'd get a compiler error if you 'used auto in the first case'
> and tried to send a and b to 'combiner' (or pretty much do anything else
> with them)...

The woe is that you'd also get a runtime assert since two
fallible_results managed to exist at the same time again. But yes, that
could only happen if you forgot to actually use them, or if you thought
the errors meant that you were supposed to add std::move(). It does
make the behaviour a little strange for the various cases though:

     calcA(); // throws because fallible_result went uninspected
     calcB();
     --------------
     auto a = calcA();
     auto b = calcB(); // asserts because two fallible_results exist
     // compiler error if a or b are used later without std::move
     --------------
     foo_error_t a = calcA();
     foo_error_t b = calcB();
     // no errors even if a or b go unused after this point
     // a & b are either valid or have error codes
     --------------
     foo_t a = calcA(); // throws if calcA has an error
     foo_t b = calcB(); // throws if calcB has an error
     // a & b are both valid if you survive this far
     --------------
     auto a = calcA().as_result_or_error();
     auto b = calcB().as_result_or_error();
     // no errors even if a or b go unused after this point
     // a & b are either valid or have error codes
     --------------
     something(calcA(), calcB()); // asserts for two fallible_results
     --------------
     something(calcA().as_result_or_error(),
               calcB().as_result_or_error());
     // might work or might assert depending on the compiler's mood


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