[outcome] success-or-failure objects
I'm liking what I saw so far. Just a small comment on the documentation. I'm curious what the following sentence does even mean? Is it supposed to confuse the user or something? It's a sign of bad taste really.
Outcome’s default is to not provide value-or-error objects. It provides success-or-failure objects.
What does that even mean? value-or-error... well, if it is non-error, I assume it is success. Both sentences mean just the same. As do their API (encapsulate either A or B). The rest of the paragraph is okay. If you just remove this comment, confusion will go away. No other changes need to be done in this paragraph. Just remove this misleading means-nothing comment. -- Vinícius dos Santos Oliveira https://vinipsmaker.github.io/
Vinícius dos Santos Oliveira wrote:
Outcome’s default is to not provide value-or-error objects. It provides success-or-failure objects.
What does that even mean?
I assume this refers to the choice to model a struct and not a union; that is, to store both an error and a value, not one or the other.
2018-01-23 11:05 GMT-03:00 Peter Dimov via Boost
Vinícius dos Santos Oliveira wrote:
Outcome’s default is to not provide value-or-error objects. It provides success-or-failure objects.
What does that even mean?
I assume this refers to the choice to model a struct and not a union; that is, to store both an error and a value, not one or the other.
The term he uses is a "or", not an "and". And it is very confusing. The fact that you're guessing a completely different direction from mine just gives point to my argument. -- Vinícius dos Santos Oliveira https://vinipsmaker.github.io/
2018-01-23 10:52 GMT-03:00 Vinícius dos Santos Oliveira < vini.ipsmaker@gmail.com>:
value-or-error... well, if it is non-error
Let me further elaborate here. A success is a non-failure and a failure is a non-success. I guess that's the same understanding everybody has here. Now... value-or-error... if it is non-error (value)... I already assume it is success. I just don't see a difference between value-or-error and success-or-failure. Not even on the "layer level of language". I see people writing "error case" and "failure case" interchangeably. They just mean the same. The argument "value-or-error is not what we provide" could buy me if Outcome didn't carry a T on the success case, but that is not the case. -- Vinícius dos Santos Oliveira https://vinipsmaker.github.io/
Let me further elaborate here.
A success is a non-failure and a failure is a non-success. I guess that's the same understanding everybody has here.
Now... value-or-error... if it is non-error (value)... I already assume it is success. I just don't see a difference between value-or-error and success-or-failure.
A value is a value. An error is an error. The meaning of each is weaker than success or failure. The latter imply *interpretation*, the former are simply values.
Not even on the "layer level of language". I see people writing "error case" and "failure case" interchangeably. They just mean the same.
The argument "value-or-error is not what we provide" could buy me if Outcome didn't carry a T on the success case, but that is not the case.
Outcome interprets value and error for you according to the rules you program it with. Hence the choice of the terms success-or-failure. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On 23/01/2018 13:52, Vinícius dos Santos Oliveira via Boost wrote:
I'm liking what I saw so far. Just a small comment on the documentation. I'm curious what the following sentence does even mean? Is it supposed to confuse the user or something? It's a sign of bad taste really.
Outcome’s default is to not provide value-or-error objects. It provides success-or-failure objects.
What does that even mean? value-or-error... well, if it is non-error, I assume it is success. Both sentences mean just the same. As do their API (encapsulate either A or B). The rest of the paragraph is okay. If you just remove this comment, confusion will go away. No other changes need to be done in this paragraph. Just remove this misleading means-nothing comment.
Literally, straight after what you quoted it says: "Outcome’s default is to not provide value-or-error objects. It provides success-or-failure objects. We define the difference as being “having programmable actions in response to no-value observation other than throwing a hard coded logic error type exception”." Can you explain why this is confusing to you? Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
2018-01-23 11:44 GMT-03:00 Niall Douglas via Boost
Can you explain why this is confusing to you?
You missed the point. I'm not questioning what is the difference between Boost.Outcome and Expected (a.k.a. missing the point). I'm questioning why you use vocabulary such as...
He is implementing "hi". We implement "hello". We define the difference between "hi" and "hello" as...
If I was to choose any confusing term /on purpose/, the best term I'd come up with is the term you have chosen. There is no worse term to choose in the paragraph you wrote. That's my point. You define "hi" and "hello" as different terms when they mean the same thing. Worst thing is, you don't need to define the difference between these two terms (value-or-error and success-or-failure) as these terms are not used in the rest of the page. Just erase the "Outcome’s default is to not provide value-or-error objects. It provides success-or-failure objects" sentence and the paragraph becomes perfect. -- Vinícius dos Santos Oliveira https://vinipsmaker.github.io/
Can you explain why this is confusing to you?
You missed the point. I'm not questioning what is the difference between Boost.Outcome and Expected (a.k.a. missing the point). I'm questioning why you use vocabulary such as...
He is implementing "hi". We implement "hello". We define the difference between "hi" and "hello" as...
If I was to choose any confusing term /on purpose/, the best term I'd come up with is the term you have chosen. There is no worse term to choose in the paragraph you wrote. That's my point.
You define "hi" and "hello" as different terms when they mean the same thing.
I think you're reading more into this than most other people do. Let me do some substitution: "Outcome’s default is to not provide value-or-error objects. It provides $TOKEN objects. We define the difference as being “having programmable actions in response to no-value observation other than throwing a hard coded logic error type exception”." Here we define $TOKEN as an object having programmable actions ... etc etc What I'm trying to do here is explain how these ValueOrError Concept matching objects are philosophically different in design to the proposed WG21 objects. We model success-vs-failure. They model value-vs-error. That has implications throughout the whole design of Outcome, which the tutorial hopefully covers. The reason we cover it there after the hand holding parts on result and outcome is because it is intended to "set the scene" for the remainder of that section, and ultimately, the rest of the tutorial.
Worst thing is, you don't need to define the difference between these two terms (value-or-error and success-or-failure) as these terms are not used in the rest of the page. Just erase the "Outcome’s default is to not provide value-or-error objects. It provides success-or-failure objects" sentence and the paragraph becomes perfect.
Would others agree? The page is https://ned14.github.io/outcome/tutorial/default-actions/ Even better is if somebody who isn't me refactored the page via a pull request to develop branch with improved wording. I can't see the problem you raise, you see. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
2018-01-23 16:00 GMT-03:00 Niall Douglas via Boost
I think you're reading more into this than most other people do.
So far the library is excellent. Not many things to complain about. =P Let me
do some substitution:
"Outcome’s default is to not provide value-or-error objects. It provides $TOKEN objects. We define the difference as being “having programmable actions in response to no-value observation other than throwing a hard coded logic error type exception”."
I'll try to think about some suggestion, but it doesn't change the fact that the "part II" of the paragraph doesn't need the added confusion of this introduction which probably nobody understood the same way as you understand. -- Vinícius dos Santos Oliveira https://vinipsmaker.github.io/
On 24/01/2018 08:00, Niall Douglas wrote:
What I'm trying to do here is explain how these ValueOrError Concept matching objects are philosophically different in design to the proposed WG21 objects. We model success-vs-failure. They model value-vs-error. That has implications throughout the whole design of Outcome, which the tutorial hopefully covers.
I think Vinícius' point is that for people unfamiliar with the terms used in WG21 papers, this is a meaningless statement as both terms seem equivalent in general English. Using ValueOrError rather than value-or-error would be some improvement as it hints that you're talking about a specific concept; adding a link to the WG21 paper would be better. However like Vicinius I'm not sure that this is a point that even needs to be made. Additionally, I'm not even sure that it's accurate. We've just had a big [system] discussion expressly about the fact that the presence of an error_code does not necessarily imply failure -- there are some codes that indicate warnings or otherwise qualified successes, and ultimately the decision of what constitutes success is up to the interpretation of the caller, not the callee or the code transport mechanism. The whole thing about "programmable actions in response to no-value" is a good feature but it doesn't sound like it should be described as related to success or failure in those terms.
The whole thing about "programmable actions in response to no-value" is a good feature but it doesn't sound like it should be described as related to success or failure in those terms.
Ok, I'll yield. I'll try to think of something better. Logged to https://github.com/ned14/outcome/issues/111 Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On Tue, Jan 23, 2018 at 6:44 AM, Niall Douglas via Boost < boost@lists.boost.org> wrote:
On 23/01/2018 13:52, Vinícius dos Santos Oliveira via Boost wrote:
I'm liking what I saw so far. Just a small comment on the documentation. I'm curious what the following sentence does even mean? Is it supposed to confuse the user or something? It's a sign of bad taste really.
Outcome’s default is to not provide value-or-error objects. It provides success-or-failure objects.
What does that even mean? value-or-error... well, if it is non-error, I assume it is success. Both sentences mean just the same. As do their API (encapsulate either A or B). The rest of the paragraph is okay. If you just remove this comment, confusion will go away. No other changes need to be done in this paragraph. Just remove this misleading means-nothing comment.
Literally, straight after what you quoted it says:
"Outcome’s default is to not provide value-or-error objects. It provides success-or-failure objects. We define the difference as being “having programmable actions in response to no-value observation other than throwing a hard coded logic error type exception”."
Can you explain why this is confusing to you?
I have a question, who throws exceptions to indicate logic errors? Emil
I have a question, who throws exceptions to indicate logic errors?
I do. In a production server environment, I want to catch logic errors with an exception trace (nested exceptions) and the emit that nested exception info to a FATAL log record so it can be investigated. Of course in a debug build, this will assert first, but that's the last thing I want in production. On 23 January 2018 at 21:42, Emil Dotchevski via Boost < boost@lists.boost.org> wrote:
On Tue, Jan 23, 2018 at 6:44 AM, Niall Douglas via Boost < boost@lists.boost.org> wrote:
I'm liking what I saw so far. Just a small comment on the documentation. I'm curious what the following sentence does even mean? Is it supposed to confuse the user or something? It's a sign of bad taste really.
Outcome’s default is to not provide value-or-error objects. It
On 23/01/2018 13:52, Vinícius dos Santos Oliveira via Boost wrote: provides
success-or-failure objects.
What does that even mean? value-or-error... well, if it is non-error, I assume it is success. Both sentences mean just the same. As do their API (encapsulate either A or B). The rest of the paragraph is okay. If you just remove this comment, confusion will go away. No other changes need to be done in this paragraph. Just remove this misleading means-nothing comment.
Literally, straight after what you quoted it says:
"Outcome’s default is to not provide value-or-error objects. It provides success-or-failure objects. We define the difference as being “having programmable actions in response to no-value observation other than throwing a hard coded logic error type exception”."
Can you explain why this is confusing to you?
I have a question, who throws exceptions to indicate logic errors?
Emil
_______________________________________________ Unsubscribe & other changes: http://lists.boost.org/ mailman/listinfo.cgi/boost
I have a question, who throws exceptions to indicate logic errors?
There's a sizeable number of people who are very keen on doing so. Hence std::logic_error and family i.e. WG21 historically has agreed. I would agree it's a least worst choice for C++ 98 and 03. Maybe even C++ 11. But any new code written in the last five years has much better alternatives available to it. I'd now go so far as to assert - not even claim - that if you're throwing logic-error-type-exceptions in brand new C++ 14 or 17 library code, you're doing something very wrong. It's a design pattern which imposes considerable costs on your users, and for very little gain when there are so many better alternatives available in today's tooling and support libraries. Cue now some of said sizeable number of people who think that I am dead wrong on that assertion ... Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On Tue, Jan 23, 2018 at 1:25 PM, Niall Douglas via Boost < boost@lists.boost.org> wrote:
I have a question, who throws exceptions to indicate logic errors?
There's a sizeable number of people who are very keen on doing so. Hence std::logic_error and family i.e. WG21 historically has agreed.
It is unreasonable to think that a program that has just encountered a logic error can recover gracefully from it any more it can do so from a crash. Try to recover, and you might make things worse, possibly much worse.
I'd now go so far as to assert - not even claim - that if you're throwing logic-error-type-exceptions in brand new C++ 14 or 17 library code, you're doing something very wrong.
Yes indeed. Emil
On 24/01/2018 11:13, Emil Dotchevski wrote:
There's a sizeable number of people who are very keen on doing so. Hence std::logic_error and family i.e. WG21 historically has agreed.
It is unreasonable to think that a program that has just encountered a logic error can recover gracefully from it any more it can do so from a crash. Try to recover, and you might make things worse, possibly much worse.
Not necessarily. It depends on how well-partitioned things are and how far-reaching the side effects executed thus far affect other things. For an example, think of a web browser. At some point it parses some malformed HTML and sets a property local to the page to null. At some later point something accesses that property without checking for null first. This is a logic error, but it's a completely benign and recoverable one (at least on platforms where memory around null is guaranteed to be invalid). The browser can recover simply by aborting the load/render of that page and showing an error, without affecting any other pages or crashing the browser itself. Other similar cases exist, where a data structure might be left in an undefined but still destructible state, but it doesn't matter because the error recovery process will just destroy that structure anyway.
On Tue, Jan 23, 2018 at 2:40 PM, Gavin Lambert via Boost < boost@lists.boost.org> wrote:
On 24/01/2018 11:13, Emil Dotchevski wrote:
There's a sizeable number of people who are very keen on doing so. Hence
std::logic_error and family i.e. WG21 historically has agreed.
It is unreasonable to think that a program that has just encountered a logic error can recover gracefully from it any more it can do so from a crash. Try to recover, and you might make things worse, possibly much worse.
Not necessarily. It depends on how well-partitioned things are and how far-reaching the side effects executed thus far affect other things.
For an example, think of a web browser. At some point it parses some malformed HTML and sets a property local to the page to null. At some later point something accesses that property without checking for null first. This is a logic error, but it's a completely benign and recoverable one (at least on platforms where memory around null is guaranteed to be invalid). The browser can recover simply by aborting the load/render of that page and showing an error, without affecting any other pages or crashing the browser itself.
This fits my "any more it can do so from a crash" caveat, but the problem is that while in a given program there might be a subset of logic errors that can be classified as "benign", this requires knowing the exact nature of the logic error; it is unsafe to make this assumption in the abstract about all logic errors. The motivation behind "doing something" in response a logic error is, obviously, to soften the impact of the bug on the user. The problem is that without knowing what went wrong, "doing something" might make the impact much worse. If you step on a mine, it's probably not a good idea to keep on walking.
Other similar cases exist, where a data structure might be left in an undefined but still destructible state
This is an oxymoron. If the state is undefined, you don't know if it is destructible.
but it doesn't matter because the error recovery process will just destroy that structure anyway.
No, it will attempt to destroy it. The result cold be a nice error message, but it could end up sending a nasty email to your boss instead. Emil
On 24/01/2018 12:39, Emil Dotchevski wrote:
The motivation behind "doing something" in response a logic error is, obviously, to soften the impact of the bug on the user. The problem is that without knowing what went wrong, "doing something" might make the impact much worse. If you step on a mine, it's probably not a good idea to keep on walking.
In the general case I completely agree. I'm just pointing out that the general case is not all cases. Granted, once the kind of errors shift from null references to things like use-after-free or double-free (or unchecked out-of-bounds), then all bets are off and chaos can and will ensue with little hope of recovery (unless the app has been careful to correctly use arena allocators). These types of errors are less common with modern smart pointers, though. But we're not even really talking about those things, we're talking about parameter validation and state preconditions. Ideally, public APIs should not trust their callers to pass sane arguments or refrain from calling a method in an inappropriate state, and should by default verify this and throw (with an option to disable it in release builds for performance, or keep doing it). Private methods can do whatever they like, assuming they have sufficient test coverage including things like ASan and UBSan. This is why things like debug iterators exist. (And should be used more often for custom iterators.)
This is an oxymoron. If the state is undefined, you don't know if it is destructible.
Tell that to moved-from objects. (I'm using "undefined" in the English "it could be in one of many intermediate states" sense, not the Standard "dogs and cats living together" sense. Mutexes might be broken, the data might be silly, and the class invariant might have been violated, but it is probably still destructible.)
On Tue, Jan 23, 2018 at 5:36 PM, Gavin Lambert via Boost < boost@lists.boost.org> wrote:
On 24/01/2018 12:39, Emil Dotchevski wrote:
The motivation behind "doing something" in response a logic error is, obviously, to soften the impact of the bug on the user. The problem is that without knowing what went wrong, "doing something" might make the impact much worse. If you step on a mine, it's probably not a good idea to keep on walking.
In the general case I completely agree. I'm just pointing out that the general case is not all cases.
Granted, once the kind of errors shift from null references to things like use-after-free or double-free (or unchecked out-of-bounds), then all bets are off and chaos can and will ensue with little hope of recovery (unless the app has been careful to correctly use arena allocators). These types of errors are less common with modern smart pointers, though.
But we're not even really talking about those things, we're talking about parameter validation and state preconditions.
I was talking about logic errors. Bugs.
Ideally, public APIs should not trust their callers to pass sane arguments or refrain from calling a method in an inappropriate state, and should by default verify this and throw (with an option to disable it in release builds for performance, or keep doing it).
That depends on the API. It is the responsibilty of the caller to not violate the documented preconditions when calling a function, and if he does, that is a logic error. The whole point of defining preconditions is to say that all bets are off otherwise. This is not to be confused with a function documenting that if this or that argument is outside of some valid range it throws exceptions. In this case it is NOT a logic error to call it with "bad" arguments, because the function specifies what it does in that case.
This is why things like debug iterators exist. (And should be used more often for custom iterators.)
Debug iterators exist to help find logic errors, not to define behavior in case of logic errors. Do note that debug iterators abort the program in case of logic errors, rather than throw exceptions.
This is an oxymoron. If the state is undefined, you don't know if it is
destructible.
Tell that to moved-from objects.
(I'm using "undefined" in the English "it could be in one of many intermediate states" sense, not the Standard "dogs and cats living together" sense. Mutexes might be broken, the data might be silly, and the class invariant might have been violated, but it is probably still destructible.)
And I'm using its domain-specific meaning: moved-from objects don't have undefined state, they have well-defined but unspecified state. More to the point, this situation is NOT a logic error, because the object can be safely destroyed. Logic error would be if the object ends up in undefined state, which may or may not be destructible. You most definitely don't want to throw a C++ exception in this case. Emil
On 24/01/2018 14:53, Emil Dotchevski wrote:
But we're not even really talking about those things, we're talking about parameter validation and state preconditions.
I was talking about logic errors. Bugs.
Yes. Which (other than business logic errors) are usually the result of the above.
That depends on the API. It is the responsibilty of the caller to not violate the documented preconditions when calling a function, and if he does, that is a logic error. The whole point of defining preconditions is to say that all bets are off otherwise.
Yes. *But* it is useful to actually verify that preconditions are not violated by accident. Usually this is done only in debug mode by putting in an assert. And that is sufficient, when unit tests exercise all paths in debug mode. The checks are usually omitted from release builds for performance reasons. But some applications might prefer to retain the checks but throw an exception instead, in order to sacrifice performance for correctness even in the face of unexpected input. The more confident that you are (hopefully backed up by static analysis and unit tests with coverage analysis) that the code doesn't contain such logic errors, the more inclined you might be to lean towards the performance end rather than the checking end. But this argument can't apply to public APIs of a library, since by definition you cannot know all callers so cannot prove they all get it "right". To bring this back to Outcome: Some people would like .error() to be assert/UB if called on an object that has no error for performance reasons (since they will "guarantee" that they don't ever call it in any other case). Other people would prefer that this throws (or does something assert-like that still works in release builds), so that it always has non-UB failure characteristics in the case that some programmer fails to meet that "guarantee". Outcome supports this by using a configurable policy. Some other libraries support it by using BOOST_ASSERT. Many don't support it at all, which is unfortunate, and which (I believe) leads to the vast majority of bugs (that aren't caught during development).
Debug iterators exist to help find logic errors, not to define behavior in case of logic errors.
Those are the same thing.
(I'm using "undefined" in the English "it could be in one of many intermediate states" sense, not the Standard "dogs and cats living together" sense. Mutexes might be broken, the data might be silly, and the class invariant might have been violated, but it is probably still destructible.)
And I'm using its domain-specific meaning: moved-from objects don't have undefined state, they have well-defined but unspecified state.
If you prefer, then, what I was saying is that in the absence of outright memory corruption (double free, writes out of bounds, etc), then all objects should at all times be in *unspecified* but destructible states -- even after logic errors. They may contain incorrect results, or unexpected nulls, or otherwise not be in intended or expected states, but that shouldn't prevent destruction.
More to the point, this situation is NOT a logic error, because the object can be safely destroyed. Logic error would be if the object ends up in undefined state, which may or may not be destructible. You most definitely don't want to throw a C++ exception in this case.
If the invalid-parameter and out-of-bounds classes of logic errors are rigorously checked at all points before the bad behaviour even happens then the object won't ever end up in an undefined state to begin with -- merely an unexpected state from the caller's perspective. Obviously (it's turtles all the way down) if the checks themselves have incorrect logic then this doesn't really help; but that's what the unit tests are for.
On Tue, Jan 23, 2018 at 9:44 PM, Gavin Lambert via Boost < boost@lists.boost.org> wrote:
Yes. *But* it is useful to actually verify that preconditions are not violated by accident.
Usually this is done only in debug mode by putting in an assert. And that is sufficient, when unit tests exercise all paths in debug mode.
Yes. Notably, assert does not throw. The checks are usually omitted from release builds for performance
reasons. But some applications might prefer to retain the checks but throw an exception instead, in order to sacrifice performance for correctness even in the face of unexpected input.
I feel we're going in circles. If you define behavior for a given condition, it is not a precondition and from the viewpoint of the function it is not a logic error if it is violated (because it is designed to deal with it). Don't get me wrong, I'm not saying that it is a bad idea to write functions which throw when they receive "bad" arguments. I'm saying that in this case they should throw exceptions other than logic_error because calling the function with "bad" arguments is not necessarily logic error; it is something the caller can, correctly, rely on.
The more confident that you are (hopefully backed up by static analysis and unit tests with coverage analysis) that the code doesn't contain such logic errors, the more inclined you might be to lean towards the performance end rather than the checking end. But this argument can't apply to public APIs of a library, since by definition you cannot know all callers so cannot prove they all get it "right".
This is not about how likely it is for the condition to occur, but what _kind_ of condition it is. By definition, logic errors can not be reasoned about, because the programmer, for whatever reason, did not consider them and, by definition, you can't know what is the best way to deal with them. Consider an engine control module for an airplane which has a logic error. If I understand your point, you're saying that you should try to deal with it in hopes of saving lives. What I'm saying is that you might end up killing more people.
To bring this back to Outcome:
Some people would like .error() to be assert/UB if called on an object that has no error for performance reasons (since they will "guarantee" that they don't ever call it in any other case).
That would be assert(!error()), with the appropriate syntax. The meaning is: I know that in this program it is impossible for this error to occur; if it does occur this indicates a logic error. And this is not a matter of preference, it's a matter of defining the correct semantics. It either is a logic error or it isn't.
Other people would prefer that this throws (or does something assert-like that still works in release builds), so that it always has non-UB failure characteristics in the case that some programmer fails to meet that "guarantee".
If you want asserts in release builds, don't define NDEBUG. Again, note that this will abort in case of logic errors, rather than throw.
Debug iterators exist to help find logic errors, not to define behavior in
case of logic errors.
Those are the same thing.
Nope, you don't write programs that recover from dereferencing invalid debug iterators. The behavior is still undefined, it's just that now you know you've reached undefined behavior.
(I'm using "undefined" in the English "it could be in one of many
intermediate states" sense, not the Standard "dogs and cats living together" sense. Mutexes might be broken, the data might be silly, and the class invariant might have been violated, but it is probably still destructible.)
And I'm using its domain-specific meaning: moved-from objects don't have undefined state, they have well-defined but unspecified state.
If you prefer, then, what I was saying is that in the absence of outright memory corruption (double free, writes out of bounds, etc), then all objects should at all times be in *unspecified* but destructible states -- even after logic errors.
What does "should" mean? How can you know that an object is destructible in all cases? I submit that this is true only if you don't have logic errors.
They may contain incorrect results, or unexpected nulls, or otherwise not be in intended or expected states, but that shouldn't prevent destruction.
How can you guarantee this? In some other language, maybe. In C/C++, you can't.
More to the point, this situation is NOT a logic error, because the object
can be safely destroyed. Logic error would be if the object ends up in undefined state, which may or may not be destructible. You most definitely don't want to throw a C++ exception in this case.
If the invalid-parameter and out-of-bounds classes of logic errors are rigorously checked at all points before the bad behaviour even happens then the object won't ever end up in an undefined state to begin with -- merely an unexpected state from the caller's perspective.
If it is a logic error for the caller to not expect this state, you have no idea what the caller will end up doing and you most definitely can't expect you can safely throw. Obviously (it's turtles all the way down) if the checks themselves have
incorrect logic then this doesn't really help; but that's what the unit tests are for.
Yes. The only way you can deal with logic errors is by finding them and removing them. Emil
On 24/01/2018 19:33, Emil Dotchevski wrote:
Yes. Notably, assert does not throw.
BOOST_ASSERT can be configured to throw. This is useful.
I feel we're going in circles. If you define behavior for a given condition, it is not a precondition and from the viewpoint of the function it is not a logic error if it is violated (because it is designed to deal with it).
Don't get me wrong, I'm not saying that it is a bad idea to write functions which throw when they receive "bad" arguments. I'm saying that in this case they should throw exceptions other than logic_error because calling the function with "bad" arguments is not necessarily logic error; it is something the caller can, correctly, rely on.
I'm not talking about documented checks where the method expressly promises to throw on certain inputs in all cases. As you said, they're not preconditions. (They're also at higher risk of creating exceptions-as-control-flow, which is also a bad idea.) I'm talking about a method or class that can be configured in "performance mode" where methods have rigid preconditions that are not checked at all (and thus cause UB if violated by the caller), vs. "safety mode" where the preconditions do get checked and something well-defined happens on failure (typically one of abort(), throw, return error; ideally caller-selectable). Often classes will implement these things but tied to assert() and debug builds. Sometimes you want an "instrumented" build, which is optimised like release builds but still contains these checks. Sometimes you want the process to abort the current operation but still continue running as a whole if a check fails rather than instantly halt. Sometimes you want to be able to log as many failures as possible at once because the cycle time of fixing one thing and re-testing takes too long. There are many reasons why this can be useful.
How can you guarantee this? In some other language, maybe. In C/C++, you can't.
I cannot think of a case where it would be false that does not require some kind of memory corruption as a prerequisite. Granted, memory corruption is vastly easier to achieve in C/C++ than in some other languages, but modern C++ is trying to discourage practices that lead to it.
If it is a logic error for the caller to not expect this state, you have no idea what the caller will end up doing and you most definitely can't expect you can safely throw.
You can always safely throw. Sometimes it will have poor consequences (eg. leaking memory, leaving bad data, abandoning locks, aborting the process), but those are all well-defined consequences and are themselves side effects of poor coding practices that can be corrected. So that doesn't make any sense. If you call r.error() on an r that doesn't have an error, there are four choices: 1. No checking. This is performance optimised and leads to UB. (In Ye Olde Outcome, it would have returned a garbage bit pattern that could cause all sorts of weird logic, though probably not any far-reaching harm due to the way error_code itself works. In Outcome v2 it's most likely going to just return a default-initialised one, which is entirely harmless.) 2. Check and assert. This aborts the process if you do something wrong. This is great for developer builds. Not so great if it happens in production (due to insufficient testing). 3. Check and throw. This will *also* abort the process if left uncaught, but otherwise admits the possibility of unwinding an operation and moving on to the next operation, which will perhaps not encounter the same problem (as if the problem were commonly exercised it would have been found sooner). 4. Check and return. This makes it a defined behaviour of the method and thus isn't a logic error any more, so doesn't need further consideration. #1 has the potential to ruin everybody's data and corrupt all the things. But its faster as long as *all* callers obey the rules. #2 is "abandon ship!" mode. It's a good default behaviour for most applications, but is often an over-reaction to things that were prevented before they caused memory corruption. Worse, if implemented using assert() then it will devolve to #1 in release builds. #3 also defaults to "abandon ship!" mode, but allows for the possibility of a controlled shutdown or of abandoning a subtask and moving on, in the cases where tasks are reasonably separated and don't depend on each other. (And even in cases where they do depend on each other, you can detect that a task depends on a task that failed and mark it as errored as well.) #3 is always the safest option. #2 has gained some popularity because it allows quickly finding issues during development, but it has the wrong fallback behaviour -- it should fall back to #3 by default, and require explicit opt-in for #1 for areas identified as performance-critical in profiling (and have been extensively tested). #2 has also gained some popularity because of people abusing exceptions-as-control-flow and catching and ignoring all exceptions, which is itself a misuse of the tools. But tools being misused does not necessarily mean they are bad tools. The right balance, I think, is to default to #2 in debug builds with a fallback to *#3* in release builds, then get code into a state where *no exceptions are ever actually thrown* (except when exercising unit tests, of course), including when run in production on real data. Then, gradually, as and when *their callers* are proved safe, the checks can be omitted (#1) in the performance-critical paths only.
On Thu, Jan 25, 2018 at 12:34 AM, Gavin Lambert via Boost
On 24/01/2018 19:33, Emil Dotchevski wrote:
If it is a logic error for the caller to not expect this state, you have no idea what the caller will end up doing and you most definitely can't expect you can safely throw.
You can always safely throw. Sometimes it will have poor consequences (eg. leaking memory, leaving bad data, abandoning locks, aborting the process), but those are all well-defined consequences and are themselves side effects of poor coding practices that can be corrected.
So that doesn't make any sense. If you call r.error() on an r that doesn't have an error, there are four choices:
1. No checking. This is performance optimised and leads to UB. (In Ye Olde Outcome, it would have returned a garbage bit pattern that could cause all sorts of weird logic, though probably not any far-reaching harm due to the way error_code itself works. In Outcome v2 it's most likely going to just return a default-initialised one, which is entirely harmless.)
2. Check and assert. This aborts the process if you do something wrong. This is great for developer builds. Not so great if it happens in production (due to insufficient testing).
3. Check and throw. This will *also* abort the process if left uncaught, but otherwise admits the possibility of unwinding an operation and moving on to the next operation, which will perhaps not encounter the same problem (as if the problem were commonly exercised it would have been found sooner).
4. Check and return. This makes it a defined behaviour of the method and thus isn't a logic error any more, so doesn't need further consideration.
#1 has the potential to ruin everybody's data and corrupt all the things. But its faster as long as *all* callers obey the rules.
#2 is "abandon ship!" mode. It's a good default behaviour for most applications, but is often an over-reaction to things that were prevented before they caused memory corruption. Worse, if implemented using assert() then it will devolve to #1 in release builds.
#3 also defaults to "abandon ship!" mode, but allows for the possibility of a controlled shutdown or of abandoning a subtask and moving on, in the cases where tasks are reasonably separated and don't depend on each other. (And even in cases where they do depend on each other, you can detect that a task depends on a task that failed and mark it as errored as well.)
#3 is always the safest option. #2 has gained some popularity because it allows quickly finding issues during development, but it has the wrong fallback behaviour -- it should fall back to #3 by default, and require explicit opt-in for #1 for areas identified as performance-critical in profiling (and have been extensively tested).
#2 has also gained some popularity because of people abusing exceptions-as-control-flow and catching and ignoring all exceptions, which is itself a misuse of the tools. But tools being misused does not necessarily mean they are bad tools.
The right balance, I think, is to default to #2 in debug builds with a fallback to *#3* in release builds, then get code into a state where *no exceptions are ever actually thrown* (except when exercising unit tests, of course), including when run in production on real data. Then, gradually, as and when *their callers* are proved safe, the checks can be omitted (#1) in the performance-critical paths only.
That can be a simple rule of thumb for many applications and errors. However, it still depends on the application and the kind of error. For example, there are applications where the risk of unintended side-effects like "leaving bad data" after a logic error might not be acceptable and therefore throwing "for cleaning up" might be deemed too risky vs. just terminating. For other applications, there might be security ramification as well, e.g. if you start unwinding when you know your state is corrupted, you could actually be helping an attacker rather than just dying. Therefore, one can easily argue that in the case of logic errors the rule of thumb should be to terminate, rather than to throw; unless you have considered the risks very carefully. You can do the same reasoning on the topic of uncaught exceptions. Miguel
On Wed, Jan 24, 2018 at 3:34 PM, Gavin Lambert via Boost < boost@lists.boost.org> wrote:
On 24/01/2018 19:33, Emil Dotchevski wrote:
How can you guarantee this? In some other language, maybe. In C/C++, you
can't.
I cannot think of a case where it would be false that does not require some kind of memory corruption as a prerequisite. Granted, memory corruption is vastly easier to achieve in C/C++ than in some other languages, but modern C++ is trying to discourage practices that lead to it.
What you're saying is that in many cases, undefined behavior may not crash your program -- but this is not a good thing, because it might do something worse than that. If you consider your web browser example, it might lead to an attacker accessing the user's file system, all because you thought it's safe to throw.
If it is a logic error for the caller to not expect this state, you have no
idea what the caller will end up doing and you most definitely can't expect you can safely throw.
You can always safely throw. Sometimes it will have poor consequences (eg. leaking memory, leaving bad data, abandoning locks, aborting the process), but those are all well-defined consequences and are themselves side effects of poor coding practices that can be corrected.
Not only it is not safe to throw after you've detected a logic error, throwing can itself be a logic error. I honestly don't know where to take the discussion from here, it seems like in your mind logic errors are the same as any failure, which is simply not true. Emil
1. No checking. This is performance optimised and leads to UB. (In Ye Olde Outcome, it would have returned a garbage bit pattern that could cause all sorts of weird logic, though probably not any far-reaching harm due to the way error_code itself works. In Outcome v2 it's most likely going to just return a default-initialised one, which is entirely harmless.)
Not to nitpick, but as https://ned14.github.io/outcome/tutorial/default-actions/happens1/#fn:1 describes, calling .error() when there is no error there results in *hard* UB. As in, the compiler doesn't generate code to handle the situation where there is no error, no branches, no stack handling, nothing. The consequence of that *might* be to proceed as if there is no error, but sometimes it is not. The Tutorial gives an example of where it segfaults. You can't really say, depends on what the optimiser generates for some given situation. Now, in terms of storage, yes we do always place default initialised E in there. That's for the C compatibility. But we don't place a default initialised T there, that can be random bits, so .value() where there is no value can yield the same random input as you mentioned Outcome v1 had. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On 25/01/2018 21:32, Niall Douglas wrote:
Not to nitpick, but as https://ned14.github.io/outcome/tutorial/default-actions/happens1/#fn:1 describes, calling .error() when there is no error there results in *hard* UB. As in, the compiler doesn't generate code to handle the situation where there is no error, no branches, no stack handling, nothing.
The consequence of that *might* be to proceed as if there is no error, but sometimes it is not. The Tutorial gives an example of where it segfaults. You can't really say, depends on what the optimiser generates for some given situation.
Now, in terms of storage, yes we do always place default initialised E in there. That's for the C compatibility. But we don't place a default initialised T there, that can be random bits, so .value() where there is no value can yield the same random input as you mentioned Outcome v1 had.
Sure. I didn't mean to imply that just because some UB *might* be relatively benign that you should ever intentionally allow it to happen. We're talking about logic errors, where the call was made unintentionally.
Sure. I didn't mean to imply that just because some UB *might* be relatively benign that you should ever intentionally allow it to happen. We're talking about logic errors, where the call was made unintentionally.
Something really interesting for the future of C++ is that Outcome based code which never throws exceptions is amenable to formal verification a la CompCert for C. A few other things would also need to be dropped e.g. virtual inheritance, RTTI, non-final virtual and so on. And only the proposed freestanding standard library could be used (https://github.com/ben-craig/freestanding_proposal/blob/eef741b6b1b8960e9e2e...), so most of the STL is not available. But if you could tick those boxes, formal verification is within reach. No idea who'd pay for CompCert to be extended to cover C++ though (Airbus paid for CompCert to be created I believe). Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On 24/01/2018 10:25, Niall Douglas wrote:
I'd now go so far as to assert - not even claim - that if you're throwing logic-error-type-exceptions in brand new C++ 14 or 17 library code, you're doing something very wrong. It's a design pattern which imposes considerable costs on your users, and for very little gain when there are so many better alternatives available in today's tooling and support libraries.
Cue now some of said sizeable number of people who think that I am dead wrong on that assertion ...
I don't think you're wrong, but I do think you have the rose-tinted glasses on a little too tightly. For libraries that have the luxury of building cross-platform and running UBSan, sure. For large applications that want to use those libraries and contain too many Windows-isms to build with UBSan, *especially* in domains where crashing the process has significant (though not life threatening to the point of requiring certification) consequences, having the option to throw an exception on precondition violation seems preferable to more subtle UB issues. BOOST_ASSERT seems like a good compromise on that front; it can be configured to assert in debug mode and either vanish entirely in release mode (for the folks who can rely on UBSan) or throw exceptions (for everyone else). Policies like Outcome has can be even better, but take more work to get right, and sometimes don't put the knobs in the right places when used indirectly.
participants (7)
-
Emil Dotchevski
-
Gavin Lambert
-
Miguel Ojeda
-
Niall Douglas
-
Peter Dimov
-
Richard Hodges
-
Vinícius dos Santos Oliveira