[outcome] How to drop the formal empty state
There has been a fair bit of review feedback that people don't like the formal empty state in outcome<T> and result<T>. I still think this opinion daft, just because it's there doesn't mean you have to use it, but here are some options: 1. Like Expected, we could require the E types to provide a nothrow move constructor. This would allow us to guarantee no empty state, not ever. Default constructing an outcome<T> or result<T> would set a default constructed T state, just like Expected. The only remaining difference now between result<T> and expected<T> would be the wide vs narrow observer functions. outcome<T> still can also carry a std::exception_ptr. 2. We could do the above, but default constructor constructs to some constexpr undefined state that is not state T nor E. 3. We could only provide a guarantee of no empty state if the E types provide nothrow move construction, but if they do not we fall back to an empty state arising if throws occur during assignment. 4. The option for an empty state becomes a template parameter defaulted to false i.e. disabled. This can be combined with any of the above. Thoughts? Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
2017-05-24 21:43 GMT+02:00 Niall Douglas via Boost
There has been a fair bit of review feedback that people don't like the formal empty state in outcome<T> and result<T>. I still think this opinion daft, just because it's there doesn't mean you have to use it, but here are some options:
1. Like Expected, we could require the E types to provide a nothrow move constructor. This would allow us to guarantee no empty state, not ever. Default constructing an outcome<T> or result<T> would set a default constructed T state, just like Expected.
The only remaining difference now between result<T> and expected<T> would be the wide vs narrow observer functions. outcome<T> still can also carry a std::exception_ptr.
2. We could do the above, but default constructor constructs to some constexpr undefined state that is not state T nor E.
3. We could only provide a guarantee of no empty state if the E types provide nothrow move construction, but if they do not we fall back to an empty state arising if throws occur during assignment.
4. The option for an empty state becomes a template parameter defaulted to false i.e. disabled. This can be combined with any of the above.
Thoughts?
Let me just clarify the nomenclature here. I understand that the only thing you *need* to have is the *moved-form* state. There is no strong need to provide a default constructor. Sure if you have a moved-from state, you might as well use it in default constructor, but it is not the only option. You could allow the moved-from state only as the result of a move. Regards, &rzej;
Let me just clarify the nomenclature here. I understand that the only thing you *need* to have is the *moved-form* state. There is no strong need to provide a default constructor. Sure if you have a moved-from state, you might as well use it in default constructor, but it is not the only option. You could allow the moved-from state only as the result of a move.
You don't need a moved-from state. If expected
Let me just clarify the nomenclature here. I understand that the only thing you *need* to have is the *moved-form* state. There is no strong need to provide a default constructor. Sure if you have a moved-from state, you might as well use it in default constructor, but it is not the only option. You could allow the moved-from state only as the result of a move. You don't need a moved-from state. If expected
is moved from and it had state T, it retains a state T, the value is whatever type T's move constructor left it in. Right and this corresponds to a *moved-from* state. You can not do too much with this until you know more about T behavior. I would have preferred to have written a wording that just says that the
Le 25/05/2017 à 01:06, Niall Douglas via Boost a écrit : post-condition is a *moved-from* state where you can just assign or destroy.
No need to be complex when simple will do.
This *moved-from* state corresponds exactly to the state we could have with a uninitialized default constructor. The single things you can do it to re-assign them or destroy them. When we move an expected, we don't have any more the value or the error if the move is not a copy. I need to state this clearly on the wording of the proposal as it is not so clear for everyone. Vicente
2017-05-25 1:06 GMT+02:00 Niall Douglas via Boost
Let me just clarify the nomenclature here. I understand that the only thing you *need* to have is the *moved-form* state. There is no strong need to provide a default constructor. Sure if you have a moved-from state, you might as well use it in default constructor, but it is not the only option. You could allow the moved-from state only as the result of a move.
You don't need a moved-from state. If expected
is moved from and it had state T, it retains a state T, the value is whatever type T's move constructor left it in. No need to be complex when simple will do.
You are right. Maybe we do not need the default constructor at all, then? If the purpose of outcome<> and friends is to be just returned from functions mabe only movable and non-default constructlble interface is sufficient. But if you later want to store them in containers this will not be enough. Maybe the scope should be fixed: what usages do we want to handle? Regards, &rzej;
Le 25/05/2017 à 10:47, Andrzej Krzemienski via Boost a écrit :
2017-05-25 1:06 GMT+02:00 Niall Douglas via Boost
: Let me just clarify the nomenclature here. I understand that the only thing you *need* to have is the *moved-form* state. There is no strong need to provide a default constructor. Sure if you have a moved-from state, you might as well use it in default constructor, but it is not the only option. You could allow the moved-from state only as the result of a move. You don't need a moved-from state. If expected
is moved from and it had state T, it retains a state T, the value is whatever type T's move constructor left it in. No need to be complex when simple will do.
You are right. Maybe we do not need the default constructor at all, then? If the purpose of outcome<> and friends is to be just returned from functions mabe only movable and non-default constructlble interface is sufficient. But if you later want to store them in containers this will not be enough. Maybe the scope should be fixed: what usages do we want to handle?
I want to be able to store them in containers and in particular in std::array. I'd need it to be default constructible. Vicente
Niall Douglas wrote:
There has been a fair bit of review feedback that people don't like the formal empty state in outcome<T> and result<T>. I still think this opinion daft, just because it's there doesn't mean you have to use it, but here are some options:
1. Like Expected, we could require the E types to provide a nothrow move constructor. This would allow us to guarantee no empty state, not ever. Default constructing an outcome<T> or result<T> would set a default constructed T state, just like Expected.
As I said, my preference is to indeed require E to provide nothrow move - which shouldn't be hard as E is in our case std::error_code which already does - and then to either: 1a. make result<T>/outcome<T> default-construct to T{} or 1b. make them default-construct to `make_error_code( outcome::errc::uninitialized_result )`. The inability of 1b to be constexpr is a bit annoying, but (a) that's arguably a defect in the standard, not on our side, and (b) if one wants a constexpr result<T>, one could always initialize it explicitly to T{}. For result<void>/outcome<void> I tend towards 1a, that is, always default-construct to a value result, even when result<T> is 1b. That's inconsistent, but there's parallel with ordinary functions where if you leave void f() without a return, all is well, but leaving int f() without a return is undefined.
1b. make them default-construct to `make_error_code( outcome::errc::uninitialized_result )`.
I am not keen on magic constants in an errored state. I am also not keen on overloading the errored state with alternative meaning. The errored state is for indicating an end user operation failed somehow, not for detecting logic errors in user code. This is precisely why I added a formal empty state, and default initialised to that. Because it causes behaviour different to valued or errored, it **very** effectively traps logic errors during code development. On **many** occasions it has successfully illuminated poorly thought through code that I have written by bringing to my attention - early and very obviously - that someone was very wrong. I am absolutely convinced it is a great design choice. And I don't like to wave this flag too hard, but I have been using these objects in anger in my own code for two years now. I have a wealth of experience in using these objects. Now, all that said, my strongest argument in favour of a formal empty state is one based on debugging. In release builds of finished code I don't find the logic error argument compelling. My strongest argument now is how it saves writing boilerplate for sending out-of-band signals, for terminating loops and such, and those are not strong arguments seeing as the end user could just send a bool& and achieve the same. So for non-debug builds, I can live with no empty state which traps logic problems. I'd therefore be happier with default construction giving uninitialised contents, or a default constructed T or E. No overloading state of E. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
Niall Douglas wrote:
1b. make them default-construct to `make_error_code( outcome::errc::uninitialized_result )`.
I am not keen on magic constants in an errored state. I am also not keen on overloading the errored state with alternative meaning. The errored state is for indicating an end user operation failed somehow, not for detecting logic errors in user code.
This is precisely why I added a formal empty state, and default initialised to that. Because it causes behaviour different to valued or errored, it **very** effectively traps logic errors during code development. On **many** occasions it has successfully illuminated poorly thought through code that I have written by bringing to my attention - early and very obviously - that someone was very wrong. I am absolutely convinced it is a great design choice.
OK, fair enough. I don't think that the benefits of having a singular state outweigh its disadvantages, but to each his own.
I'd therefore be happier with default construction giving uninitialised contents, or a default constructed T or E. No overloading state of E.
result and outcome have no E. I'm not talking about E or expected
I'd therefore be happier with default construction giving uninitialised contents, or a default constructed T or E. No overloading state of E.
result and outcome have no E. I'm not talking about E or expected
here. I'm talking specifically about result<T> and outcome<T>. That is, I'm trying to answer the question "Under the assumption that expected doesn't exist, what should the default constructor of result<T> do?" Default constructing to an std::error_code of 0 is kind of stupid because "The operation failed: The operation succeeded", although one might, I suppose, make an argument in favor of constructing into a stupid state precisely because it's stupid.
I'd choose uninitialised bytes or default constructing T before default constructing E to a null error_code. As I've mentioned many times now, default constructed error_code is only by convention not an error, the unfortunate choice of the system_category by the C++ standard makes it not portable to assume a default constructed error_code is not an error. If people did agree with me that a default trapping empty state is particularly useful in debug builds, the obvious choice for a release build default is uninitialised bytes. As Vicente mentioned, valgrind and the AddressSanitizer should catch misuse of those. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
2017-05-25 17:14 GMT+02:00 Niall Douglas via Boost
I'd therefore be happier with default construction giving uninitialised contents, or a default constructed T or E. No overloading state of E.
result and outcome have no E. I'm not talking about E or expected
here. I'm talking specifically about result<T> and outcome<T>. That is, I'm trying to answer the question "Under the assumption that expected doesn't exist, what should the default constructor of result<T> do?" Default constructing to an std::error_code of 0 is kind of stupid because "The operation failed: The operation succeeded", although one might, I suppose, make an argument in favor of constructing into a stupid state precisely because it's stupid.
I'd choose uninitialised bytes or default constructing T before default constructing E to a null error_code. As I've mentioned many times now, default constructed error_code is only by convention not an error, the unfortunate choice of the system_category by the C++ standard makes it not portable to assume a default constructed error_code is not an error.
If people did agree with me that a default trapping empty state is particularly useful in debug builds, the obvious choice for a release build default is uninitialised bytes. As Vicente mentioned, valgrind and the AddressSanitizer should catch misuse of those.
UB sanitizers will also catch the call to __builtin_unreachable(). If `result<>` and `outcome<>` adopted narrow-contract semantics, you could help the sanitizer by implementing observers like: ``` T const& error() const { if (has_error()) return _error; else { __builtin_unreachable(); // here goes any rescue semantics, if needed } } ``` Regards, &rzej;
Niall Douglas wrote:
I'd choose uninitialised bytes or default constructing T before default constructing E to a null error_code.
I'd choose (by a factor of 10^3) a singular empty state before uninitialized bytes. If it comes to that.
In fact, seeing people suggest default-constructing to uninitialized bytes is a very strong argument - in fact the strongest argument so far - in favor of having a formal empty state. I almost regret I brought that up.
Le 25/05/2017 à 16:30, Niall Douglas via Boost a écrit :
1b. make them default-construct to `make_error_code( outcome::errc::uninitialized_result )`. I am not keen on magic constants in an errored state. I am also not keen on overloading the errored state with alternative meaning. The errored state is for indicating an end user operation failed somehow, not for detecting logic errors in user code.
This is precisely why I added a formal empty state, and default initialised to that. Because it causes behaviour different to valued or errored, it **very** effectively traps logic errors during code development. On **many** occasions it has successfully illuminated poorly thought through code that I have written by bringing to my attention - early and very obviously - that someone was very wrong. I am absolutely convinced it is a great design choice.
And I don't like to wave this flag too hard, but I have been using these objects in anger in my own code for two years now. I have a wealth of experience in using these objects.
Now, all that said, my strongest argument in favour of a formal empty state is one based on debugging. In release builds of finished code I don't find the logic error argument compelling. My strongest argument now is how it saves writing boilerplate for sending out-of-band signals, for terminating loops and such, and those are not strong arguments seeing as the end user could just send a bool& and achieve the same. So for non-debug builds, I can live with no empty state which traps logic problems.
I'd therefore be happier with default construction giving uninitialised contents, or a default constructed T or E. No overloading state of E.
We agree here :) Vicente
On 26/05/2017 02:30, Niall Douglas wrote:
This is precisely why I added a formal empty state, and default initialised to that. Because it causes behaviour different to valued or errored, it **very** effectively traps logic errors during code development. On **many** occasions it has successfully illuminated poorly thought through code that I have written by bringing to my attention - early and very obviously - that someone was very wrong. I am absolutely convinced it is a great design choice.
My worry with the formal empty state is precisely that as neither a value nor an error, someone might fail to check for it in code and then proceed under a false assumption. (If not has_error() then this void method must have succeeded, and I don't call value() because it was void.) Provided that .value() and .error() both throw if called in the empty state (which I assume is the case), that helps mitigate a large part of that, but not entirely, as in the case above. Yes, it's a misuse of the type, but it's one that I can see being very likely to happen in the real world. Even if the possibility of an empty state is heralded with bold blinking all-caps on all doc pages.
I'd therefore be happier with default construction giving uninitialised contents, or a default constructed T or E. No overloading state of E.
I strongly agree with Peter Dimov that introducing uninitialized state is a very undesirable thing. I still regard it as very rare in the majority of codebases that sanitizers that would detect that sort of thing would ever be run, so this would simply introduce the very type of UB that having a formal error-handling type is intended to reduce. I don't like the idea of a default-constructed T because T is not always default-constructible, and this makes it inconsistently behaved for different T and makes it harder to use uniformly in containers, especially in generic code. I don't like the idea of a default-constructed E because by convention (even if not quite in fact as Niall has pointed out -- though I've yet to see a platform where a 0 error code *didn't* mean success, other than cases where the formal type is int but is actually used as bool) the default-constructed error_code means "no error", and this is heavily reinforced by its operator bool semantics. I do like the idea of a non-default-constructed error code, because failure to initialise the result does seem like an error to me. Niall points out that this is harder to detect and treat specially in code but I don't agree with that; as long as a suitably unique error code is used then a simple assert in the error path would pick it up, no problem. If the consensus is that an initial non-default error code is not satisfactory, then a formal empty state seems to me like the least worst alternative. I just know that it's going to bite someone at some point.
Le 26/05/2017 à 01:36, Gavin Lambert via Boost a écrit :
On 26/05/2017 02:30, Niall Douglas wrote:
This is precisely why I added a formal empty state, and default initialised to that. Because it causes behaviour different to valued or errored, it **very** effectively traps logic errors during code development. On **many** occasions it has successfully illuminated poorly thought through code that I have written by bringing to my attention - early and very obviously - that someone was very wrong. I am absolutely convinced it is a great design choice.
My worry with the formal empty state is precisely that as neither a value nor an error, someone might fail to check for it in code and then proceed under a false assumption. (If not has_error() then this void method must have succeeded, and I don't call value() because it was void.)
Provided that .value() and .error() both throw if called in the empty state (which I assume is the case), that helps mitigate a large part of that, but not entirely, as in the case above. std::experimentall:expected
::error, doesn't throws. I don't see a use case where we want to retrieve an error without checking before. Maybe you have a case. Yes, it's a misuse of the type, but it's one that I can see being very likely to happen in the real world. Even if the possibility of an empty state is heralded with bold blinking all-caps on all doc pages.
I'd therefore be happier with default construction giving uninitialised contents, or a default constructed T or E. No overloading state of E.
I strongly agree with Peter Dimov that introducing uninitialized state is a very undesirable thing. I still regard it as very rare in the majority of codebases that sanitizers that would detect that sort of thing would ever be run, so this would simply introduce the very type of UB that having a formal error-handling type is intended to reduce.
I don't like the idea of a default-constructed T because T is not always default-constructible, and this makes it inconsistently behaved for different T and makes it harder to use uniformly in containers, especially in generic code.
I don't like the idea of a default-constructed E because by convention (even if not quite in fact as Niall has pointed out -- though I've yet to see a platform where a 0 error code *didn't* mean success, other than cases where the formal type is int but is actually used as bool) the default-constructed error_code means "no error", and this is heavily reinforced by its operator bool semantics.
I do like the idea of a non-default-constructed error code, because failure to initialise the result does seem like an error to me. Niall points out that this is harder to detect and treat specially in code but I don't agree with that; as long as a suitably unique error code is used then a simple assert in the error path would pick it up, no problem.
If the consensus is that an initial non-default error code is not satisfactory, then a formal empty state seems to me like the least worst alternative. I just know that it's going to bite someone at some point.
If we don't provide a default constructor for expected<T> we could be
forced to use optional
On 26/05/2017 17:33, Vicente J. Botet Escriba wrote:
std::experimentall:expected
::error, doesn't throws. I don't see a use case where we want to retrieve an error without checking before. Maybe you have a case.
Perhaps unit tests, where you're expecting an error but the code unexpectedly succeeds. Also as in the case above, when you forget that an empty state exists: result<T> r = something(); if (r.has_value()) { do_something(r.value()); } else { log(r.error()); // oops, r might be empty } I dislike gratuitous UB, and Niall assures us that optimisers will discard a double check so it should be reasonably cheap.
Le 26/05/2017 à 01:36, Gavin Lambert a écrit :
I don't like the idea of a default-constructed T because T is not always default-constructible, and this makes it inconsistently behaved for different T and makes it harder to use uniformly in containers, especially in generic code.
For the record, not having a default constructor at all also makes it harder to use in containers, so I don't like that either. Though it's a weaker dislike than my dislike of a default-constructed T or E.
I don't like the idea of a default-constructed E because by convention (even if not quite in fact as Niall has pointed out -- though I've yet to see a platform where a 0 error code *didn't* mean success, other than cases where the formal type is int but is actually used as bool) the default-constructed error_code means "no error", and this is heavily reinforced by its operator bool semantics.
I do like the idea of a non-default-constructed error code, because failure to initialise the result does seem like an error to me. Niall points out that this is harder to detect and treat specially in code but I don't agree with that; as long as a suitably unique error code is used then a simple assert in the error path would pick it up, no problem.
If the consensus is that an initial non-default error code is not satisfactory, then a formal empty state seems to me like the least worst alternative. I just know that it's going to bite someone at some point.
If we don't provide a default constructor for expected<T> we could be forced to use optional
. This allows to don't pay for this empty state when we don't need it. The problem is that we are paying more than needed when we need it.
We have two options: * we specialize optional
> * we rename the intended specialization xxx<T> is similar to optional . xxx could be outcome::result or optional_expected
I'm not entirely sure how this relates to what I was saying.
At least in terms of storage, the current implementation of empty state
is presumably free (it should be no more expensive to internally store a
variant
Le 26/05/2017 à 08:41, Gavin Lambert via Boost a écrit :
On 26/05/2017 17:33, Vicente J. Botet Escriba wrote:
std::experimentall:expected
::error, doesn't throws. I don't see a use case where we want to retrieve an error without checking before. Maybe you have a case. Perhaps unit tests, where you're expecting an error but the code unexpectedly succeeds. Okay. We could have a wide_error function for this purposes.
Also as in the case above, when you forget that an empty state exists:
I will not take this in consideration for my purposes (expected proposal) as I have no empty state.
result<T> r = something(); if (r.has_value()) { do_something(r.value()); } else { log(r.error()); // oops, r might be empty }
I dislike gratuitous UB, and Niall assures us that optimisers will discard a double check so it should be reasonably cheap.
Do we need a probe?
Le 26/05/2017 à 01:36, Gavin Lambert a écrit :
I don't like the idea of a default-constructed T because T is not always default-constructible, and this makes it inconsistently behaved for different T and makes it harder to use uniformly in containers, especially in generic code.
For the record, not having a default constructor at all also makes it harder to use in containers, so I don't like that either. Though it's a weaker dislike than my dislike of a default-constructed T or E.
If you want empty we need a good implementation of
optional
I don't like the idea of a default-constructed E because by convention (even if not quite in fact as Niall has pointed out -- though I've yet to see a platform where a 0 error code *didn't* mean success, other than cases where the formal type is int but is actually used as bool) the default-constructed error_code means "no error", and this is heavily reinforced by its operator bool semantics.
I do like the idea of a non-default-constructed error code, because failure to initialise the result does seem like an error to me. Niall points out that this is harder to detect and treat specially in code but I don't agree with that; as long as a suitably unique error code is used then a simple assert in the error path would pick it up, no problem.
If the consensus is that an initial non-default error code is not satisfactory, then a formal empty state seems to me like the least worst alternative. I just know that it's going to bite someone at some point.
If we don't provide a default constructor for expected<T> we could be forced to use optional
. This allows to don't pay for this empty state when we don't need it. The problem is that we are paying more than needed when we need it.
We have two options: * we specialize optional
> * we rename the intended specialization xxx<T> is similar to optional . xxx could be outcome::result or optional_expected I'm not entirely sure how this relates to what I was saying.
At least in terms of storage, the current implementation of empty state is presumably free (it should be no more expensive to internally store a variant
than a variant ). And it's currently required to exist due to exception guarantees (and possible noexcept(false) move constructors).
In terms of storage you are right, but not in terms of possible values. This is why I'm using optional here to state clearly that we have an additional value.
I don't think that T should be restricted to noexcept(true)-movable types only, as this prevents using it with C++03 non-POD types (that have a copy constructor but lack a move constructor), which are still likely to be widespread in codebases (although perhaps less common as return values).
Do you have an example of a C++03 error type that will throw?
Given that, from the sounds of it an empty state does need to exist in the implementation. Where it sounds like Niall and you differ is whether that state should be exposed to the user. I think if it's there anyway then it probably should be, since this enables useful behaviour (such as storing in containers and using that state as "method not called yet", implying that the empty state should be the default-constructed state).
If it turns out that the empty state is not needed by the implementation, then a non-default-constructed-E seems like a better default value, at least for Outcome where E is a known type. (It's a bit harder for Expected.)
Another consideration is that regardless of default construction or not is that you need to decide what an expected
will contain if someone moves-from it (directly). Is it now formal-empty or does it now contain a moved-from-T or moved-from-E? Or does it contain a moved-from-variant (if that's different)?
This is already defined in the proposal. What do you expect to have?
The return type of value() plays a role here as well. If it returns by value, then you can probably pick whatever you like. If it returns by reference, then the caller can now move-from the internal T and ensure it will be in the has-a-moved-from-T state, not the empty state. (Which may or may not be desirable, but implies that moved-from is not the same as empty, which might surprise users of smart pointers.)
If you want something that can be empty, model it from optional<T>. Al the functions will follow. Maybe you don't agree with the std::optional interface and then my previous advise will not apply. Is this your case?
(Returning by reference also disallows possible future storage optimisations from nested variant merging, as mentioned in another thread.)
Do you want optional to take care of this possible future? Best, Vicente
On 26/05/2017 07:41, Gavin Lambert via Boost wrote:
On 26/05/2017 17:33, Vicente J. Botet Escriba wrote:
std::experimentall:expected
::error, doesn't throws. I don't see a use case where we want to retrieve an error without checking before. Maybe you have a case. Perhaps unit tests, where you're expecting an error but the code unexpectedly succeeds.
Very true. I have a next gen unit testing infrastructure built around Outcomes where UB on any of the observers would be a no-go. You can see the idea of it at https://github.com/ned14/boost.afio/blob/master/test/tests/file_handle_creat... and note how self describing the tables of success and failure are.
At least in terms of storage, the current implementation of empty state is presumably free (it should be no more expensive to internally store a variant
than a variant ). And it's currently required to exist due to exception guarantees (and possible noexcept(false) move constructors).
As mentioned in another discussion thread, the empty state is also used
internally as a micro-optimisation. So it would likely remain internally
whatever the decision taken here, as a tool for making the CPU expend
identical CPU cycles on both positive and negative branches on state.
Again, if people don't like that behaviour of outcome/result to be
equally costly on T or E branches chosen, expected
I don't think that T should be restricted to noexcept(true)-movable types only, as this prevents using it with C++03 non-POD types (that have a copy constructor but lack a move constructor), which are still likely to be widespread in codebases (although perhaps less common as return values).
I couldn't agree more about type T. But Expected does not demand nothrow move construction from type T, only type E. And usually most of the time the end user will control the source code for any type E used. It's a fair restriction in exchange for never empty.
Another consideration is that regardless of default construction or not is that you need to decide what an expected
will contain if someone moves-from it (directly). Is it now formal-empty or does it now contain a moved-from-T or moved-from-E? Or does it contain a moved-from-variant (if that's different)?
We never change the state unless the user asks explicitly for that. So
if they move from an expected
The return type of value() plays a role here as well. If it returns by value, then you can probably pick whatever you like. If it returns by reference, then the caller can now move-from the internal T and ensure it will be in the has-a-moved-from-T state, not the empty state. (Which may or may not be desirable, but implies that moved-from is not the same as empty, which might surprise users of smart pointers.)
(Returning by reference also disallows possible future storage optimisations from nested variant merging, as mentioned in another thread.)
Reference returning .value() I've found in real world usage to be surprisingly useful. Back when I began using Outcome, I used to write code something like: ``` result<Foo> something() { Foo ret; ... build ret ... return make_valued_result<Foo>(std::move(ret)); } ``` And that's probably what first time users are going to write. But after I built up some experience, now I tend to write this instead: ``` result<Foo> something() { result<Foo> ret(Foo()); Foo &foo = ret.value(); ... build foo ... return ret; } ``` I can tell people are going to ask me: why the second form instead of the first form? That's surprisingly hard to answer in a meaningful way. I guess the former form you are writing code to generate a Foo, and then wrapping that Foo up into result<Foo> for the purposes of indicating success. The second form you are being more specific, you are not generating a Foo, you are generating a result<Foo>. Somehow the code feels right with the second form. It's somehow more idiomatic. I appreciate that's very vague. But my point is, the reference returning .value() makes a lot of sense. You will end up using these objects to build returned values a **lot** in the code you write. In case anyone wishes to study real world code using Outcomes, https://github.com/ned14/boost.afio/blob/master/include/boost/afio/v2.0/deta... is a use case. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
Niall Douglas wrote:
But after I built up some experience, now I tend to write this instead:
result<Foo> something() { result<Foo> ret(Foo()); Foo &foo = ret.value(); ... build foo ... return ret; }
This takes us back to "what if due to a logic error the code that should build `foo` doesn't?" And since you've constructed `ret` to non-empty, the point of empty is, again, lost.
On 26/05/2017 15:23, Peter Dimov via Boost wrote:
Niall Douglas wrote:
But after I built up some experience, now I tend to write this instead:
result<Foo> something() { result<Foo> ret(Foo()); Foo &foo = ret.value(); ... build foo ... return ret; }
This takes us back to "what if due to a logic error the code that should build `foo` doesn't?"
And since you've constructed `ret` to non-empty, the point of empty is, again, lost.
But if I am writing my implementation that way, it is because there is no purpose here to my function returning empty. Else I would implement it differently. I appreciate that the above is me speaking vaguely from experience and without concrete evidence. But I think most programmers, once they are using these objects for a while, will start to naturally write code which never *could* return an empty object if returning an empty object makes no sense for that function. Equally, if some function could return an empty object, the programmer will structure their code appropriately. To be honest, writing code using these objects becomes so second nature after a while you stop thinking about why you write the code you do. You just do it and certainly with code written using Outcome, most of the time the code works first time, including error handling, which is unusual. My port of AFIO v2 to POSIX was particularly weird - I wrote hundreds of lines of new code. Damn thing passed all the unit tests written for Windows very first time. I actually had to step through with the debugger to prove it to myself, I didn't believe the result. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
Niall Douglas wrote:
But if I am writing my implementation that way, it is because there is no purpose here to my function returning empty. Else I would implement it differently.
I appreciate that the above is me speaking vaguely from experience and without concrete evidence.
I usually place high value on argument from experience, even if presented
without evidence.
But in this case, you've cited
https://github.com/ned14/boost.afio/blob/master/include/boost/afio/v2.0/deta...
which is this:
result
But if I am writing my implementation that way, it is because there is no purpose here to my function returning empty. Else I would implement it differently.
I appreciate that the above is me speaking vaguely from experience and without concrete evidence.
I usually place high value on argument from experience, even if presented without evidence.
But in this case, you've cited
https://github.com/ned14/boost.afio/blob/master/include/boost/afio/v2.0/deta...
which is this:
result
file_handle::file(file_handle::path_type _path, file_handle::mode _mode, file_handle::creation _creation, file_handle::caching _caching, file_handle::flag flags) noexcept { result ret(file_handle(native_handle_type(), 0, 0, std::move(_path), _caching, flags)); native_handle_type &nativeh = ret.get()._v; BOOST_OUTCOME_TRY(attribs, attribs_from_handle_mode_caching_and_flags(nativeh, _mode, _creation, _caching, flags)); nativeh.behaviour |= native_handle_type::disposition::file; const char *path_ = ret.value()._path.c_str(); nativeh.fd = ::open(path_, attribs, 0x1b0 /*660*/); if(-1 == nativeh.fd) return make_errored_result (errno, last190(ret.value()._path.native())); BOOST_AFIO_LOG_FUNCTION_CALL(nativeh.fd); if(!(flags & flag::disable_safety_unlinks)) { BOOST_OUTCOME_TRYV(ret.value()._fetch_inode()); } if(_creation == creation::truncate && ret.value().are_safety_fsyncs_issued()) fsync(nativeh.fd); return ret; } Now, the first line constructs
result
ret(file_handle(native_handle_type(), 0, 0, std::move(_path), _caching, flags)); and it's immediately apparent that returning `ret` in this initial state makes no sense. That is, you either need to further initialize it into a valid file_handle, or return an "errored" result.
Correct. AFIO v2 uses a two-phase initialisation design pattern throughout. The first phase is always a constexpr noexcept constructor which ONLY ever initialises trivial types, and NEVER can error. The newly constructed object is valid, but not useful. The second phase occurs as the static init function runs its course, and is able to bail out due to error at any time whereupon file_handle's destructor will correctly clean up partial initialisation. Remember that the static init function never experiences an exception throw. It calls 100% noexcept functions. So you write code knowing for a fact that you don't need to order things for correct exception safety during unexpected stack unwind. Somehow that's important to my opinion here, but I don't know why. Maybe when I employ this design pattern, later on when I revisit this code it immediately signals to me that this code is 100% never throwing code. So maybe it's a signalling design choice.
For this reason, I'd think that a style that does
file_handle fh(native_handle_type(), 0, 0, std::move(_path), _caching, flags);
first, then initializes it appropriately, and as a final step returns it via result
, would perhaps make more sense (and also save a number of ret.value() calls).
I agree, as I said originally, that most first time users of these objects would think the same way as you. But I think after you've been using them for a while, when you're writing init functions, you'll end up thinking like me. My way is somehow ... "cleaner". I really can't say why. It just feels right. The logically more obvious way you spoke of feels wrong, or rather, bad or dirty somehow. I wish I could justify myself here. I cannot. I will say that my way doesn't have RVO problems on C++ 14, yours you need to type extra non-obvious stuff to some folk. But that's not an argument really. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
Le 26/05/2017 à 18:16, Niall Douglas via Boost a écrit :
On 26/05/2017 15:23, Peter Dimov via Boost wrote:
Niall Douglas wrote:
But after I built up some experience, now I tend to write this instead:
result<Foo> something() { result<Foo> ret(Foo()); Foo &foo = ret.value(); ... build foo ... return ret; } This takes us back to "what if due to a logic error the code that should build `foo` doesn't?"
And since you've constructed `ret` to non-empty, the point of empty is, again, lost. But if I am writing my implementation that way, it is because there is no purpose here to my function returning empty. Else I would implement it differently. If the function can not return empty it should use another type, isn't it? Well I can admit that you must follow a specific protocol and return a result<T> even if in this specific case it cannot be empty.
I appreciate that the above is me speaking vaguely from experience and without concrete evidence. But I think most programmers, once they are using these objects for a while, will start to naturally write code which never *could* return an empty object if returning an empty object makes no sense for that function. Equally, if some function could return an empty object, the programmer will structure their code appropriately. If we don't have cases where we can return empty, I don't see why we want to support it. Vicenet
This takes us back to "what if due to a logic error the code that should build `foo` doesn't?"
And since you've constructed `ret` to non-empty, the point of empty is, again, lost. But if I am writing my implementation that way, it is because there is no purpose here to my function returning empty. Else I would implement it differently.
If the function can not return empty it should use another type, isn't it?
Well that's exactly the crux of the debate isn't it? How much should the specific type of object you choose to return T|E exactly match the contract of that function? It's easy to say "exactly match". But in a large code base, too many slight variants of an Outcome significantly increases cognitive load on programmers having to remember what each one of dozens of Outcome variants mean and how they are slightly different. Nevertheless, I can see a good argument for result<T> vs result_e<T> as you suggest. It does increase the clarity of the API's public contract. But one can also go too far. I think, in combination with my proposed typedef factory, we can reach a happy medium somewhere. Obviously there would be implicit conversion from outcomes incapable of empty state into outcomes capable of it, but not the other way round. So like at the moment you can return a result<T> into something accepting an outcome<T> implicitly, you could implicitly return a result<T> into something accepting a result_e<T>, where result_e<T> is empty | T | error_code_extended. What do you think? Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On 26/05/2017 17:33, Vicente J. Botet Escriba wrote:
std::experimentall:expected
::error, doesn't throws. I don't see a use case where we want to retrieve an error without checking before. Maybe you have a case. Perhaps unit tests, where you're expecting an error but the code unexpectedly succeeds. Very true. I have a next gen unit testing infrastructure built around Outcomes where UB on any of the observers would be a no-go. You can see On 26/05/2017 07:41, Gavin Lambert via Boost wrote: the idea of it at https://github.com/ned14/boost.afio/blob/master/test/tests/file_handle_creat... and note how self describing the tables of success and failure are.
At least in terms of storage, the current implementation of empty state is presumably free (it should be no more expensive to internally store a variant
than a variant ). And it's currently required to exist due to exception guarantees (and possible noexcept(false) move constructors). As mentioned in another discussion thread, the empty state is also used internally as a micro-optimisation. So it would likely remain internally whatever the decision taken here, as a tool for making the CPU expend identical CPU cycles on both positive and negative branches on state. Again, if people don't like that behaviour of outcome/result to be equally costly on T or E branches chosen, expected
never has an empty state and therefore always naturally favours the E state (because in .value() you check for an errored state, and if so throw an exception, so returning a value is usually the branch the compiler generates a branch to later code for). I don't think that T should be restricted to noexcept(true)-movable types only, as this prevents using it with C++03 non-POD types (that have a copy constructor but lack a move constructor), which are still likely to be widespread in codebases (although perhaps less common as return values). I couldn't agree more about type T. But Expected does not demand nothrow move construction from type T, only type E. And usually most of the time the end user will control the source code for any type E used. It's a fair restriction in exchange for never empty.
Another consideration is that regardless of default construction or not is that you need to decide what an expected
will contain if someone moves-from it (directly). Is it now formal-empty or does it now contain a moved-from-T or moved-from-E? Or does it contain a moved-from-variant (if that's different)? We never change the state unless the user asks explicitly for that. So if they move from an expected and the state was an E, it remains an E, just a moved-from E. Resetting to empty looks attractive, but I found out the hard way it is a bad design decision. Code consuming a rvalue reference does not actually have to move anything, nothing in the C++ standard says it does. It's only a widely held convention that it ought to.
(move constructors/assignment don't actually have to move. libstdc++'s std::string famously didn't for example)
The return type of value() plays a role here as well. If it returns by value, then you can probably pick whatever you like. If it returns by reference, then the caller can now move-from the internal T and ensure it will be in the has-a-moved-from-T state, not the empty state. (Which may or may not be desirable, but implies that moved-from is not the same as empty, which might surprise users of smart pointers.)
(Returning by reference also disallows possible future storage optimisations from nested variant merging, as mentioned in another thread.) Reference returning .value() I've found in real world usage to be surprisingly useful.
Back when I began using Outcome, I used to write code something like:
``` result<Foo> something() { Foo ret; ... build ret ... return make_valued_result<Foo>(std::move(ret)); } ```
And that's probably what first time users are going to write.
But after I built up some experience, now I tend to write this instead:
``` result<Foo> something() { result<Foo> ret(Foo()); Foo &foo = ret.value(); ... build foo ... return ret; } ``` But here you cannot have an empty result, so expected will be more adapted here. And as you are sure you have a value, so you will use * instead of value. I can tell people are going to ask me: why the second form instead of the first form?
That's surprisingly hard to answer in a meaningful way. I guess the former form you are writing code to generate a Foo, and then wrapping that Foo up into result<Foo> for the purposes of indicating success. The second form you are being more specific, you are not generating a Foo, you are generating a result<Foo>. Somehow the code feels right with the second form. It's somehow more idiomatic. The problem with this idiom is that you have a reference to something
Le 26/05/2017 à 16:18, Niall Douglas via Boost a écrit : that could become an error :( We are introducing with this idiom a possibly reference leak. But this is C++.
I appreciate that's very vague. But my point is, the reference returning .value() makes a lot of sense. You will end up using these objects to build returned values a **lot** in the code you write.
I would prefer to program it as the first form and let the compiler do the needed optimizations :) But sometimes we cannot forget the time. Vicente
That's surprisingly hard to answer in a meaningful way. I guess the former form you are writing code to generate a Foo, and then wrapping that Foo up into result<Foo> for the purposes of indicating success. The second form you are being more specific, you are not generating a Foo, you are generating a result<Foo>. Somehow the code feels right with the second form. It's somehow more idiomatic.
The problem with this idiom is that you have a reference to something that could become an error :( We are introducing with this idiom a possibly reference leak. But this is C++.
Actually no. We are in fact employing a two stage object construction design pattern. Let me flesh out the example code a bit so it's more realistic: ``` class Foo { A *a {nullptr}; B *b {nullptr}; public: constexpr Foo() noexcept {} // never throws, constexpr ~Foo() { // destructor is capable of destroying partially // constructed instances! delete b; // really if(b) delete b; delete a; // really if(a) delete a; } }; result<Foo> make_Foo() noexcept { // constexpr construct a valid but uninitialised Foo instance result<Foo> ret(Foo()); // get a reference to the Foo instance to be returned Foo &foo = ret.value(); // initialise member variable "a" in Foo foo.a = new(std::nothrow) A(); if(foo.a == nullptr) { return make_errored_result(std::errc::not_enough_memory); } // initialise member variable "b" in Foo foo.b = new(std::nothrow) B(); if(foo.b == nullptr) { // Foo's destructor will be called by result<Foo> being unwound, // thus destroying the A object created earlier return make_errored_result(std::errc::not_enough_memory); } // RVOed on C++ 14 as well as C++ 17 return ret; } ``` Every object in AFIO v2 is constructed like this. A static init function for every class returns a result<TYPE> and is implemented using a two-stage construction design pattern like the above. The constexpr noexcept constructor never initialises anything except trivial types, and once the object has exited the constexpr constructor it is now valid and its destructor will definitely be called. So returning to my previous post to Peter, in the above there is no good reason to ever return an empty result. It would make no sense, so you never write code which could from the beginning. And type Foo knows how to destroy a partially constructed edition of itself, so your init function can bail out with an error at any time and all resources are tidied up. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
2017-05-26 16:18 GMT+02:00 Niall Douglas via Boost
On 26/05/2017 07:41, Gavin Lambert via Boost wrote:
On 26/05/2017 17:33, Vicente J. Botet Escriba wrote:
std::experimentall:expected
::error, doesn't throws. I don't see a use case where we want to retrieve an error without checking before. Maybe you have a case. Perhaps unit tests, where you're expecting an error but the code unexpectedly succeeds.
Very true. I have a next gen unit testing infrastructure built around Outcomes where UB on any of the observers would be a no-go. You can see the idea of it at https://github.com/ned14/boost.afio/blob/master/test/ tests/file_handle_create_close/runner.cpp#L34 and note how self describing the tables of success and failure are.
At least in terms of storage, the current implementation of empty state is presumably free (it should be no more expensive to internally store a variant
than a variant ). And it's currently required to exist due to exception guarantees (and possible noexcept(false) move constructors). As mentioned in another discussion thread, the empty state is also used internally as a micro-optimisation. So it would likely remain internally whatever the decision taken here, as a tool for making the CPU expend identical CPU cycles on both positive and negative branches on state.
Again, if people don't like that behaviour of outcome/result to be equally costly on T or E branches chosen,
What does it mean tha "outcome is equally costly on T or E branches chosen"?
expected
empty state and therefore always naturally favours the E state (because in .value() you check for an errored state, and if so throw an exception, so returning a value is usually the branch the compiler generates a branch to later code for).
I don't think that T should be restricted to noexcept(true)-movable types only, as this prevents using it with C++03 non-POD types (that have a copy constructor but lack a move constructor), which are still likely to be widespread in codebases (although perhaps less common as return values).
I couldn't agree more about type T. But Expected does not demand nothrow move construction from type T, only type E. And usually most of the time the end user will control the source code for any type E used. It's a fair restriction in exchange for never empty.
Another consideration is that regardless of default construction or not is that you need to decide what an expected
will contain if someone moves-from it (directly). Is it now formal-empty or does it now contain a moved-from-T or moved-from-E? Or does it contain a moved-from-variant (if that's different)? We never change the state unless the user asks explicitly for that. So if they move from an expected
and the state was an E, it remains an E, just a moved-from E. Resetting to empty looks attractive, but I found out the hard way it is a bad design decision. Code consuming a rvalue reference does not actually have to move anything, nothing in the C++ standard says it does. It's only a widely held convention that it ought to.
In optional<T> we deliberately don't put the moved-from object into an empty state (which surprises many people). This is for performance reasons: when `T` is a trivial type, `optional<T>` can be made trivially_copyable.
As mentioned in another discussion thread, the empty state is also used internally as a micro-optimisation. So it would likely remain internally whatever the decision taken here, as a tool for making the CPU expend identical CPU cycles on both positive and negative branches on state.
Again, if people don't like that behaviour of outcome/result to be equally costly on T or E branches chosen,
What does it mean tha "outcome is equally costly on T or E branches chosen"?
Have a look at https://godbolt.org/g/bVd4D7, specifically the disassembly. As you'll note, the first possible state (empty) tends to be chosen by the compiler as the most likely. That implies a 20 cycle branch misprediction cost for each of the valued or errored states. So they are equally costly, which is intentional.
Resetting to empty looks attractive, but I found out the hard way it is a bad design decision. Code consuming a rvalue reference does not actually have to move anything, nothing in the C++ standard says it does. It's only a widely held convention that it ought to.
In optional<T> we deliberately don't put the moved-from object into an empty state (which surprises many people). This is for performance reasons: when `T` is a trivial type, `optional<T>` can be made trivially_copyable.
+1 Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
Niall Douglas wrote:
What does it mean tha "outcome is equally costly on T or E branches chosen"?
Have a look at https://godbolt.org/g/bVd4D7, specifically the disassembly.
As a side note, the Clang/LLVM optimizer is indeed impressive. printf("a") -> putchar('a'), which is just one example of what it can do.
As you'll note, the first possible state (empty) tends to be chosen by the compiler as the most likely. That implies a 20 cycle branch misprediction cost for each of the valued or errored states. So they are equally costly, which is intentional.
Wait a minute. Are you saying that you consider the fact that valued and errored are equally slow a feature, instead of one of them being fast? How is that a good thing? Of course empty should be the least likely - it _is_ the least likely.
As you'll note, the first possible state (empty) tends to be chosen by the compiler as the most likely. That implies a 20 cycle branch misprediction cost for each of the valued or errored states. So they are equally costly, which is intentional.
Wait a minute. Are you saying that you consider the fact that valued and errored are equally slow a feature, instead of one of them being fast? How is that a good thing? Of course empty should be the least likely - it _is_ the least likely.
As I've mentioned several times already in other threads, that was a deliberate and intentional design choice for outcome/result. Predictable latency throughout. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
Niall Douglas wrote:
As you'll note, the first possible state (empty) tends to be chosen by the compiler as the most likely. That implies a 20 cycle branch misprediction cost for each of the valued or errored states. So they are equally costly, which is intentional.
Wait a minute. Are you saying that you consider the fact that valued and errored are equally slow a feature, instead of one of them being fast? How is that a good thing? Of course empty should be the least likely - it _is_ the least likely.
As I've mentioned several times already in other threads, that was a deliberate and intentional design choice for outcome/result. Predictable latency throughout.
That's an intriguing statement. Now I know that you usually know what you're talking about, so perhaps you can provide a bit of further explanation. If we take a typical example - your AFIO function from earlier - it issues at least three syscalls, every one of which has different latency when it succeeds or when it fails, and in addition, later ones are skipped when an earlier one fails, and to top all that off, the last syscall is an fsync, which does not inhabit the same galaxy as the words predictable latency. So what use case are we targeting here where success and failure are equally costly and measured in cycles?
As you'll note, the first possible state (empty) tends to be chosen by >> the compiler as the most likely. That implies a 20 cycle branch misprediction cost for each of the valued or errored states. So they >> are equally costly, which is intentional.
Wait a minute. Are you saying that you consider the fact that valued and > errored are equally slow a feature, instead of one of them being fast? > How is that a good thing? Of course empty should be the least likely - > it _is_ the least likely.
As I've mentioned several times already in other threads, that was a deliberate and intentional design choice for outcome/result. Predictable latency throughout.
That's an intriguing statement.
Now I know that you usually know what you're talking about, so perhaps you can provide a bit of further explanation.
Well, sometimes at least. I have to admit this far into this review my head is beginning to swim a bit, so much information and threads of discussion.
If we take a typical example - your AFIO function from earlier - it issues at least three syscalls, every one of which has different latency when it succeeds or when it fails, and in addition, later ones are skipped when an earlier one fails, and to top all that off, the last syscall is an fsync, which does not inhabit the same galaxy as the words predictable latency.
So what use case are we targeting here where success and failure are equally costly and measured in cycles?
In most of AFIO 20 CPU cycles is utterly irrelevant. Indeed, 2 million
CPU cycles would be irrelevant.
But Outcome was always written for a SG14 low latency type audience. The
group is a bit misnamed, they actually much prefer predictable latency
over low latency. Some over there even compile release code with -O0 to
ensure they get exactly the performance of the source code written, no
surprises. That sort of thing.
As I mentioned before, if identical performance for success vs failure
is not what you want, and you really care about 20 CPU cycles,
expected
2017-05-27 14:54 GMT+02:00 Niall Douglas via Boost
As you'll note, the first possible state (empty) tends to be chosen by >> the compiler as the most likely. That implies a 20 cycle branch misprediction cost for each of the valued or errored states. So they >> are equally costly, which is intentional.
Wait a minute. Are you saying that you consider the fact that valued and > errored are equally slow a feature, instead of one of them being fast? > How is that a good thing? Of course empty should be the least likely - > it _is_ the least likely.
As I've mentioned several times already in other threads, that was a deliberate and intentional design choice for outcome/result. Predictable latency throughout.
That's an intriguing statement.
Now I know that you usually know what you're talking about, so perhaps you can provide a bit of further explanation.
Well, sometimes at least. I have to admit this far into this review my head is beginning to swim a bit, so much information and threads of discussion.
If we take a typical example - your AFIO function from earlier - it issues at least three syscalls, every one of which has different latency when it succeeds or when it fails, and in addition, later ones are skipped when an earlier one fails, and to top all that off, the last syscall is an fsync, which does not inhabit the same galaxy as the words predictable latency.
So what use case are we targeting here where success and failure are equally costly and measured in cycles?
In most of AFIO 20 CPU cycles is utterly irrelevant. Indeed, 2 million CPU cycles would be irrelevant.
But Outcome was always written for a SG14 low latency type audience. The group is a bit misnamed, they actually much prefer predictable latency over low latency. Some over there even compile release code with -O0 to ensure they get exactly the performance of the source code written, no surprises. That sort of thing.
As I mentioned before, if identical performance for success vs failure is not what you want, and you really care about 20 CPU cycles, expected
will always bias towards either T or E.
Interesting. Did I missed that in the docs? According to your description it should be really relevant to people who make a decision whether to go with your library or not. Regards, &rzej;
But Outcome was always written for a SG14 low latency type audience. The group is a bit misnamed, they actually much prefer predictable latency over low latency. Some over there even compile release code with -O0 to ensure they get exactly the performance of the source code written, no surprises. That sort of thing.
As I mentioned before, if identical performance for success vs failure is not what you want, and you really care about 20 CPU cycles, expected
will always bias towards either T or E. Interesting. Did I missed that in the docs? According to your description it should be really relevant to people who make a decision whether to go with your library or not.
People who actually care about 20 CPU cycles per branch on state will without fail insist on examining the source code before deciding on using a library. They wouldn't trust claims in its documentation. Therefore I didn't bother documenting it publicly, I let the code speak for itself. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
2017-05-28 0:17 GMT+02:00 Niall Douglas via Boost
But Outcome was always written for a SG14 low latency type audience. The group is a bit misnamed, they actually much prefer predictable latency over low latency. Some over there even compile release code with -O0 to ensure they get exactly the performance of the source code written, no surprises. That sort of thing.
As I mentioned before, if identical performance for success vs failure is not what you want, and you really care about 20 CPU cycles, expected
will always bias towards either T or E. Interesting. Did I missed that in the docs? According to your description it should be really relevant to people who make a decision whether to go with your library or not.
People who actually care about 20 CPU cycles per branch on state will without fail insist on examining the source code before deciding on using a library. They wouldn't trust claims in its documentation. Therefore I didn't bother documenting it publicly, I let the code speak for itself.
You do not make these claims so that people just trust you. You make them to encourage people to examine the sources. Also, I believe that if I were interested in this performance guarantee, I would like it documented, because it is a guarantee that what I see in the code is not a happenstance of this GIT SHA, but a feature you commit to supporting. I, for one, am not interested in these optimizaitons, but if I have seen them documented, I would have a slightly better understanding on what goals of this library are. Regards, &rzej;
2017-05-27 1:36 GMT+02:00 Niall Douglas via Boost
As mentioned in another discussion thread, the empty state is also used internally as a micro-optimisation. So it would likely remain internally whatever the decision taken here, as a tool for making the CPU expend identical CPU cycles on both positive and negative branches on state.
Again, if people don't like that behaviour of outcome/result to be equally costly on T or E branches chosen,
What does it mean tha "outcome is equally costly on T or E branches chosen"?
Have a look at https://godbolt.org/g/bVd4D7, specifically the disassembly.
As you'll note, the first possible state (empty) tends to be chosen by the compiler as the most likely. That implies a 20 cycle branch misprediction cost for each of the valued or errored states. So they are equally costly, which is intentional.
Would you not rather manually "predict" for the valued state?
What does it mean tha "outcome is equally costly on T or E branches chosen"?
Have a look at https://godbolt.org/g/bVd4D7, specifically the disassembly.
As you'll note, the first possible state (empty) tends to be chosen by the compiler as the most likely. That implies a 20 cycle branch misprediction cost for each of the valued or errored states. So they are equally costly, which is intentional.
Would you not rather manually "predict" for the valued state?
Predictable latency for success or failure is a key design goal for Outcome. If you want success to be preferred, choose Expected instead. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
Would you not rather manually "predict" for the valued state?
Predictable latency for success or failure is a key design goal for Outcome. If you want success to be preferred, choose Expected instead.
If success is 0 extra cycles, and failure is 20 extra cycles, it is still predictable, just not equal. ? Tony
On 30/05/2017 23:34, Gottlob Frege via Boost wrote:
Would you not rather manually "predict" for the valued state?
Predictable latency for success or failure is a key design goal for Outcome. If you want success to be preferred, choose Expected instead.
If success is 0 extra cycles, and failure is 20 extra cycles, it is still predictable, just not equal. ?
No, because for every branch taken on an outcome, you multiply your potential runtime execution paths by two. If success and failure are not balanced, it totally ruins predictability. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
2017-05-31 14:35 GMT+02:00 Niall Douglas via Boost
On 30/05/2017 23:34, Gottlob Frege via Boost wrote:
Would you not rather manually "predict" for the valued state?
Predictable latency for success or failure is a key design goal for Outcome. If you want success to be preferred, choose Expected instead.
If success is 0 extra cycles, and failure is 20 extra cycles, it is still predictable, just not equal. ?
No, because for every branch taken on an outcome, you multiply your potential runtime execution paths by two. If success and failure are not balanced, it totally ruins predictability.
Niall, could you point me to some materials that would explain and justify why people need this guarantee? Regards, &rzej;
On 31/05/2017 13:51, Andrzej Krzemienski via Boost wrote:
2017-05-31 14:35 GMT+02:00 Niall Douglas via Boost
: On 30/05/2017 23:34, Gottlob Frege via Boost wrote:
Would you not rather manually "predict" for the valued state?
Predictable latency for success or failure is a key design goal for Outcome. If you want success to be preferred, choose Expected instead.
If success is 0 extra cycles, and failure is 20 extra cycles, it is still predictable, just not equal. ?
No, because for every branch taken on an outcome, you multiply your potential runtime execution paths by two. If success and failure are not balanced, it totally ruins predictability.
Niall, could you point me to some materials that would explain and justify why people need this guarantee?
The kind of people who study http://www.agner.org/optimize/microarchitecture.pdf in detail. In my opinion at least it's irrelevant on any Intel CPU since Haswell whose ability to recognise branch patterns is spooky, and manual tuning really makes no difference any more. It still matters a lot for some ARM CPUs though. But I wouldn't worry about any of this too much, it's such a micro optimisation. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
This is precisely why I added a formal empty state, and default initialised to that. Because it causes behaviour different to valued or errored, it **very** effectively traps logic errors during code development. On **many** occasions it has successfully illuminated poorly thought through code that I have written by bringing to my attention - early and very obviously - that someone was very wrong. I am absolutely convinced it is a great design choice.
My worry with the formal empty state is precisely that as neither a value nor an error, someone might fail to check for it in code and then proceed under a false assumption. (If not has_error() then this void method must have succeeded, and I don't call value() because it was void.)
Provided that .value() and .error() both throw if called in the empty state (which I assume is the case), that helps mitigate a large part of that, but not entirely, as in the case above.
Both do throw on empty, yes. The only other time Outcomes throw is on .value() where the state is errored, in this case we throw the error directly if it's excepted or the error code wrapped into std::system_error. Regarding the danger of proceeding under a false assumption, it can't happen if users never create an empty outcome because outcomes don't suddenly turn empty on their own, so if you never create one, you'll never get one. That's why I think the formal empty state is safe. If you never create an empty outcome, you can safely totally ignore empty as a possible state. It will *NEVER* happen.
Yes, it's a misuse of the type, but it's one that I can see being very likely to happen in the real world. Even if the possibility of an empty state is heralded with bold blinking all-caps on all doc pages.
I think the docs does need to emphasise that if you never create an empty outcome, you can safely ignore ever dealing with the empty state.
I'd therefore be happier with default construction giving uninitialised contents, or a default constructed T or E. No overloading state of E.
I do like the idea of a non-default-constructed error code, because failure to initialise the result does seem like an error to me. Niall points out that this is harder to detect and treat specially in code but I don't agree with that; as long as a suitably unique error code is used then a simple assert in the error path would pick it up, no problem.
I am strongly opposed to this design. It conflates logic errors in the code design with runtime errors. It's a bad design choice.
If the consensus is that an initial non-default error code is not satisfactory, then a formal empty state seems to me like the least worst alternative. I just know that it's going to bite someone at some point.
I have been persuaded by argument here that default construction to empty is in fact a defect in the design. The formal empty state ought to *always* be explicitly constructed, and **never** occur implicitly. I have logged this defect to https://github.com/ned14/boost.outcome/issues/44. Still another option is that if T has a default constructor, we default construct to a T instance, and if T does not have a default constructor, we implement no default constructor. This is what Expected already does incidentally. Outcome could do the same. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
Niall Douglas wrote:
I have been persuaded by argument here that default construction to empty is in fact a defect in the design. The formal empty state ought to *always* be explicitly constructed, and **never** occur implicitly. I have logged this defect to https://github.com/ned14/boost.outcome/issues/44.
I don't think I agree with that. The whole point of having a singular empty state is to default-construct to it so that the mistake of failing to assign to the default-constructed result/outcome can be detected. This: result<T> function() { result<T> r; // do things that put something into r return r; } where the "do things" part sometimes fails to put something into r. If you don't default-construct to empty, you pretty much lose the point of having empty.
I have been persuaded by argument here that default construction to empty is in fact a defect in the design. The formal empty state ought to *always* be explicitly constructed, and **never** occur implicitly. I have logged this defect to https://github.com/ned14/boost.outcome/issues/44.
I don't think I agree with that. The whole point of having a singular empty state is to default-construct to it so that the mistake of failing to assign to the default-constructed result/outcome can be detected. This:
result<T> function() { result<T> r;
// do things that put something into r
return r; }
where the "do things" part sometimes fails to put something into r.
But you still get exactly that *if* you ask for it: result<T> function() { result<T> r(empty); // do things that put something into r return r; } Combined with the never-empty guarantees, this new behaviour makes the empty state completely impossible unless someone asked for it at least once somewhere. I like that.
If you don't default-construct to empty, you pretty much lose the point of having empty.
I am currently minded that default constructing an outcome or result is not possible if T does not have a default constructor. If it does, it default constructs to a default constructed type T. This matches the current behaviour of Expected, though Vicente is currently planning on eliminating the default constructor entirely. If he gets LEWG buy in to that design, I am minded to match Expected on this in outcome/result: no default constructor, not ever. The reason my opinion has been changed on this is that I cannot think of any situation where you really need a default constructor or else everything blows up and the end user reaches a showstopping game ever situation. If someone can suggest one, I think we'd all change our opinion. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
Niall Douglas wrote:
But you still get exactly that *if* you ask for it:
result<T> function() { result<T> r(empty);
// do things that put something into r
return r; }
Sure, but explicitly asking for it correlates negatively with making the logic error of not overwriting it, and there are also C-style arrays to consider.
Le 26/05/2017 à 18:07, Niall Douglas via Boost a écrit :
I have been persuaded by argument here that default construction to empty is in fact a defect in the design. The formal empty state ought to *always* be explicitly constructed, and **never** occur implicitly. I have logged this defect to https://github.com/ned14/boost.outcome/issues/44. I don't think I agree with that. The whole point of having a singular empty state is to default-construct to it so that the mistake of failing to assign to the default-constructed result/outcome can be detected. This:
result<T> function() { result<T> r;
// do things that put something into r
return r; }
where the "do things" part sometimes fails to put something into r. But you still get exactly that *if* you ask for it:
result<T> function() { result<T> r(empty);
// do things that put something into r
return r; }
Combined with the never-empty guarantees, this new behaviour makes the empty state completely impossible unless someone asked for it at least once somewhere. I like that.
I'm missing surely something. Do you consider that you support the never-empty warranties when you have already the empty alternative?
If you don't default-construct to empty, you pretty much lose the point of having empty. I am currently minded that default constructing an outcome or result is not possible if T does not have a default constructor. If it does, it default constructs to a default constructed type T.
This matches the current behaviour of Expected, though Vicente is currently planning on eliminating the default constructor entirely. If he gets LEWG buy in to that design, I am minded to match Expected on this in outcome/result: no default constructor, not ever.
I believed that you needed the empty state at the interface level. Are
you saying that Outcome will not have this empty state?
It seems there are some that find it useful to have such optimization
for optional
The reason my opinion has been changed on this is that I cannot think of any situation where you really need a default constructor or else everything blows up and the end user reaches a showstopping game ever situation. If someone can suggest one, I think we'd all change our opinion.
I believed that outcome was a model for optional
Le 24/05/2017 à 21:43, Niall Douglas via Boost a écrit :
There has been a fair bit of review feedback that people don't like the formal empty state in outcome<T> and result<T>. I still think this opinion daft, just because it's there doesn't mean you have to use it, but here are some options:
1. Like Expected, we could require the E types to provide a nothrow move constructor. This would allow us to guarantee no empty state, not ever. Default constructing an outcome<T> or result<T> would set a default constructed T state, just like Expected. We could as well choose to don't initialize at all. I want to open this debate in Toronto.
The only remaining difference now between result<T> and expected<T> would be the wide vs narrow observer functions. outcome<T> still can also carry a std::exception_ptr.
2. We could do the above, but default constructor constructs to some constexpr undefined state that is not state T nor E. Agreed, we could as well choose to don't initialize it at all. I want to open this debate in Toronto. I don't want this to be observable.
3. We could only provide a guarantee of no empty state if the E types provide nothrow move construction, but if they do not we fall back to an empty state arising if throws occur during assignment. This is a *valueless-by-exception* as for variant. When do you want to report an error that has a move constructor that can throw? What would you report at the end. Sorry I can not report to you the reason for failure as an exception was throws while constructing the report :( If we want to KISS Error types must be no-throw movable.
4. The option for an empty state becomes a template parameter defaulted to false i.e. disabled. This can be combined with any of the above. No thanks.
I don't see yet the need for this empty state. Vicente P.S. I expect to have in C++20 a std::variant that don't have this *valueless-by-exception* state when the conditions are satisfied.
participants (6)
-
Andrzej Krzemienski
-
Gavin Lambert
-
Gottlob Frege
-
Niall Douglas
-
Peter Dimov
-
Vicente J. Botet Escriba