Re: [boost] expected/result/etc
On Wed, Jan 27, 2016 at 10:49 PM, Michael Marcin
So looking a few of these libraries/proposals I have a few things I'm interested in.
- Is it at least as good as a simple hand-rolled solution? - Can you transport error codes through layers without knowing specifics of an error?
For the second Niall's outcome/result handles very well I think. std::error_code and std::exception_ptr transport is good enough for almost any reasonable type agnostic error transport.
Have you considered using Boost Exception? It lets you add arbitrary data to exception objects, even after they have been thrown, regardless of their type -- see http://www.boost.org/doc/libs/release/libs/exception/doc/error_info.html. The general idea is to adopt a strategy where the exception objects are augmented with any, even platform-specific relevant data, which then the catch site can analyze to choose the correct handling. Cheers, Emil
On 28.1.2016. 21:02, Emil Dotchevski wrote:
Have you considered using Boost Exception? It lets you add arbitrary data to exception objects, even after they have been thrown, regardless of their type -- see http://www.boost.org/doc/libs/release/libs/exception/doc/error_info.html. The general idea is to adopt a strategy where the exception objects are augmented with any, even platform-specific relevant data, which then the catch site can analyze to choose the correct handling.
Hopefully these efforts will bring us to a solution that will do away
with the, IMO, false dichotomy between "errors" and "exceptions"
(
On Thu, Jan 28, 2016 at 3:44 PM, Domagoj Saric
On 28.1.2016. 21:02, Emil Dotchevski wrote:
Have you considered using Boost Exception? It lets you add arbitrary data to exception objects, even after they have been thrown, regardless of their type -- see http://www.boost.org/doc/libs/release/libs/exception/doc/error_info.html. The general idea is to adopt a strategy where the exception objects are augmented with any, even platform-specific relevant data, which then the catch site can analyze to choose the correct handling.
Hopefully these efforts will bring us to a solution that will do away with the, IMO, false dichotomy between "errors" and "exceptions" (
vs <exception>) - rather we will start thinking only about "errors"/error objects which can be transfered in two different ways - like a ball in a rugby game - it can be passed 'on foot' hand to hand or it can be thrown over intermediate players to its final destination (only with errors its in reverse - they are mostly passed/returned or thrown 'backwards':). IOW things like error_info will be applicable to 'the' standardized error type(s) that functions also return, not just throw.
You could theoretically pass exceptions function-to-function by
exception_ptr but that seems backwards, no pun intended. :)
Anyway, my point is that there are two ways one can go about solving the
problem of transporting error codes. One is what seems to be the path
you're taking, where you design a common API that can fit all error codes
on all platforms. On the other hand, the Boost Exception approach sidesteps
this difficult problem.
Let's take socket errors for example, which on POSIX are communicated by
the OS through errno, while on Windows there is the GetLastError function.
Using the Boost Exception approach, you would always store the errno into
any socket exception object, and on Windows you'd also store the
GetLastError code. At the catch site, regardless of the platform, you can
put in logic that takes correct action based on the presence or absence of
any particular error_info:
catch( socket_error & e )
{
int const * e1=get_error_info
On 28 Jan 2016 at 16:57, Emil Dotchevski wrote:
Let's take socket errors for example, which on POSIX are communicated by the OS through errno, while on Windows there is the GetLastError function. Using the Boost Exception approach, you would always store the errno into any socket exception object, and on Windows you'd also store the GetLastError code. At the catch site, regardless of the platform, you can put in logic that takes correct action based on the presence or absence of any particular error_info:
Doesn't std::system_error already do this for us, and it's already in the standard and it also allows arbitrary domain specific error coding? Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On Fri, Jan 29, 2016 at 5:37 AM, Niall Douglas
On 28 Jan 2016 at 16:57, Emil Dotchevski wrote:
Let's take socket errors for example, which on POSIX are communicated by the OS through errno, while on Windows there is the GetLastError function. Using the Boost Exception approach, you would always store the errno into any socket exception object, and on Windows you'd also store the GetLastError code. At the catch site, regardless of the platform, you can put in logic that takes correct action based on the presence or absence of any particular error_info:
Doesn't std::system_error already do this for us, and it's already in the standard and it also allows arbitrary domain specific error coding?
My point is that perhaps it isn't arbitrary enough because there may be a lot more than errno and GetLastError that is relevant to a given failure. In the case of sockets, it may be necessary to transport a relevant URL to the catch point, yet information like this isn't always available at the point of the throw. Obviously this is beyond the scope of std::system_error, I was just saying that really exceptions or other error-reporting objects need to be able to transport anything at all, especially in library-level code. Emil
On 29 Jan 2016 at 15:47, Emil Dotchevski wrote:
Doesn't std::system_error already do this for us, and it's already in the standard and it also allows arbitrary domain specific error coding?
My point is that perhaps it isn't arbitrary enough because there may be a lot more than errno and GetLastError that is relevant to a given failure. In the case of sockets, it may be necessary to transport a relevant URL to the catch point, yet information like this isn't always available at the point of the throw.
It's a fair point. I simply subclassed the exception type in question and made it happy to get type sliced, but I'll grant you that probably isn't best provided to library end users who tend to get type slicing wrong as it is a bit brittle.
Obviously this is beyond the scope of std::system_error, I was just saying that really exceptions or other error-reporting objects need to be able to transport anything at all, especially in library-level code.
Something I've always wished for is for std::exception to be able to transport one or more stack backtraces. I rolled my own for AFIO v1 where it captures the stack both inside the engine and the stack where end user code called AFIO code (both were always in different threads), but it's a lot of code and is not efficient, and sadly will not be present in AFIO v2 which is 98% noexcept and single threaded throughout. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On Sat, Jan 30, 2016 at 6:51 AM, Niall Douglas
On 29 Jan 2016 at 15:47, Emil Dotchevski wrote:
Obviously this is beyond the scope of std::system_error, I was just saying that really exceptions or other error-reporting objects need to be able to transport anything at all, especially in library-level code.
Something I've always wished for is for std::exception to be able to transport one or more stack backtraces. I rolled my own for AFIO v1 where it captures the stack both inside the engine and the stack where end user code called AFIO code (both were always in different threads), but it's a lot of code and is not efficient, and sadly will not be present in AFIO v2 which is 98% noexcept and single threaded throughout.
Backtraces aren't enough. It is necessary for exception objects to be able to transport arbitrary data, though of course one of the things that would make sense to transport is a stack trace. I had proposed http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3757.html, which was turned down because it was ruled that it requires a change in the (std::exception) ABI. I am pretty sure that thread local storage can be used to implement N3757 without ABI changes but I haven't got around to try to implement it. If exception objects can transport any data whatsoever, then there is no need to worry whether individual data, like system error codes, are sufficiently flexible. Emil
On 31/01/2016 03:51, Niall Douglas wrote:
Something I've always wished for is for std::exception to be able to transport one or more stack backtraces. I rolled my own for AFIO v1 where it captures the stack both inside the engine and the stack where end user code called AFIO code (both were always in different threads), but it's a lot of code and is not efficient, and sadly will not be present in AFIO v2 which is 98% noexcept and single threaded throughout.
FWIW, through some compiler-specific chicanery you can recover a stack trace (for the latest throw site) from any in-flight exception (including system exceptions such as access violations) in MSVC+Windows at least. I've found that to be incredibly helpful in the past, even if only applied to crashes during unit tests. ;) I haven't checked if it's possible to do something similar in gcc/clang and/or Linux, but I would hope so.
On 29.1.2016. 1:57, Emil Dotchevski wrote:
On Thu, Jan 28, 2016 at 3:44 PM, Domagoj Saric
wrote: Hopefully these efforts will bring us to a solution that will do away the, IMO, false dichotomy between "errors" and "exceptions" (
vs <exception>) - rather we will start thinking only about "errors"/error objects which can be transfered in two different ways - like a ball in a rugby game - it can be passed 'on foot' hand to hand or it can be thrown over intermediate players to its final destination (only with errors its in reverse - they are mostly passed/returned or thrown 'backwards':). IOW things like error_info will be applicable to 'the' standardized error type(s) that functions also return, not just throw. You could theoretically pass exceptions function-to-function by exception_ptr but that seems backwards, no pun intended. :)
And we could theoretically and practically ;) return actual error objects (even those considered 'exceptions', e.g. std::runtime_error) w/o necessarily stuffing them into some type erasing mechanism like exception_ptr (i.e. you pass the ball as a ball until you need to interact with someone who only understands abstract UFOs;)
Anyway, my point is that there are two ways one can go about solving the problem of transporting error codes. One is what seems to be the path you're taking, where you design a common API that can fit all error codes on all platforms. On the other hand, the Boost Exception approach sidesteps this difficult problem.
That's a separate issue. The "expected/result/etc" debate&proposals are first and foremost about the ('all new and improved') mechanism for passing errors around. The problem of how to abstract semantically 'similar' but syntactically 'not the same' error reporting mechanisms across platforms (such as your errno vs win32 last error code example) is orthogonal to this. However I do think that that problem should also be solved by the same library as the first one, i.e. that an err-like library would offer a std::result-like 'synthesis' of the 'error codes' vs 'exceptions' dialectic _and_ offer wrappers, abstractions and utilities to work with platform errors (e.g. NTSTATUS, HRESULT, win32 last error, errno[1], OSX OSStatus, ObjC exceptions, JNI etc.) and on top of that generalized Boost.Exception-like functionality - IOW a complete "error library"... [1] https://github.com/psiha/err/blob/master/include/boost/err/errno.hpp https://github.com/psiha/err/blob/master/include/boost/err/win32.hpp -- "What Huxley teaches is that in the age of advanced technology, spiritual devastation is more likely to come from an enemy with a smiling face than from one whose countenance exudes suspicion and hate." Neil Postman
On 4 Feb 2016 at 23:02, Domagoj Saric wrote:
You could theoretically pass exceptions function-to-function by exception_ptr but that seems backwards, no pun intended. :)
And we could theoretically and practically ;) return actual error objects (even those considered 'exceptions', e.g. std::runtime_error) w/o necessarily stuffing them into some type erasing mechanism like exception_ptr (i.e. you pass the ball as a ball until you need to interact with someone who only understands abstract UFOs;)
exception_ptr is like shared_ptr - the potential use of atomics forces the compiler to emit code just in case. Any potential use of atomics is like calling fsync() on the compiler's AST.
https://github.com/psiha/err/blob/master/include/boost/err/errno.hpp https://github.com/psiha/err/blob/master/include/boost/err/win32.hpp
Firstly I just found and read your RFC on this potential Boost.Err at
http://lists.boost.org/Archives/boost/2015/11/226558.php. You posted
this during my vacation away from Boost, so I didn't see it till now.
I'd like to thank you for this contribution to the debate, it
certainly is original.
There are some very interesting - perhaps even debatable - feature
choices in this object. I'll skip commenting on most of those, but I
will say one thing - I've found in AFIO v2 the ability to use
outcome::result<T> as a receiving container surprisingly useful. The
pattern looks like this:
1. User hands some scatter buffers to be filled by a
file_handle::async_read() to AFIO v2. These probably are a
std::vector
On 5.2.2016. 10:25, Niall Douglas wrote:
On 4 Feb 2016 at 23:02, Domagoj Saric wrote:
You could theoretically pass exceptions function-to-function by exception_ptr but that seems backwards, no pun intended. :)
And we could theoretically and practically ;) return actual error objects (even those considered 'exceptions', e.g. std::runtime_error) w/o necessarily stuffing them into some type erasing mechanism like exception_ptr (i.e. you pass the ball as a ball until you need to interact with someone who only understands abstract UFOs;)
exception_ptr is like shared_ptr - the potential use of atomics forces the compiler to emit code just in case. Any potential use of atomics is like calling fsync() on the compiler's AST.
That's (in line with/part of) my point?
Firstly I just found and read your RFC on this potential Boost.Err at http://lists.boost.org/Archives/boost/2015/11/226558.php. You posted this during my vacation away from Boost, so I didn't see it till now. I'd like to thank you for this contribution to the debate, it certainly is original.
There are some very interesting - perhaps even debatable - feature choices in this object. I'll skip commenting on most of those, but I will say one thing - I've found in AFIO v2 the ability to use outcome::result<T> as a receiving container surprisingly useful. The pattern looks like this: <snip> In other words, I found my result<T> very useful as a state accumulator which can "go errored" in a natural way.
Can you elaborate more on what you mean by "state accumulator" and
"going errored in a natural way" in this context? + you seem to be using
'container' and 'accumulator' interchangeably while AFAIK those are not
eqivalent concepts...
If by 'state accumulation' you imply the requirement that the result
Your Err object can't do value semantics - it cannot be a container in its own right. I personally think that is unfortunate for the reasons above. I think there is much more usefulness in these objects having full value semantics BUT with a restricted variant content,
I don't know what you mean here: * there is no 'Err object', there are two types - fallible_result and result_or_error[1] * both of those carry Ts and Es by value
so basically either it's the value you want OR it's why there isn't a value you expect there.
AFAICT that's exactly how the two Err objects work..? [1] it 'dawned' on me a few days ago that the langauge change/addition I already 'requested'/wished for (rvalue destructor 'overloads') would also enable that the two types (fallible_result and result_or_error), could be merged (as esentially their only major difference is on-destruction behaviour)... -- "What Huxley teaches is that in the age of advanced technology, spiritual devastation is more likely to come from an enemy with a smiling face than from one whose countenance exudes suspicion and hate." Neil Postman
Le 04/02/2016 23:02, Domagoj Saric a écrit :
On 29.1.2016. 1:57, Emil Dotchevski wrote:
On Thu, Jan 28, 2016 at 3:44 PM, Domagoj Saric
wrote: Hopefully these efforts will bring us to a solution that will do away the, IMO, false dichotomy between "errors" and "exceptions" (
vs <exception>) - rather we will start thinking only about "errors"/error objects which can be transfered in two different ways - like a ball in a rugby game - it can be passed 'on foot' hand to hand or it can be thrown over intermediate players to its final destination (only with errors its in reverse - they are mostly passed/returned or thrown 'backwards':). IOW things like error_info will be applicable to 'the' standardized error type(s) that functions also return, not just throw. You could theoretically pass exceptions function-to-function by exception_ptr but that seems backwards, no pun intended. :)
And we could theoretically and practically ;) return actual error objects (even those considered 'exceptions', e.g. std::runtime_error) w/o necessarily stuffing them into some type erasing mechanism like exception_ptr (i.e. you pass the ball as a ball until you need to interact with someone who only understands abstract UFOs;)
Anyway, my point is that there are two ways one can go about solving the problem of transporting error codes. One is what seems to be the path you're taking, where you design a common API that can fit all error codes on all platforms. On the other hand, the Boost Exception approach sidesteps this difficult problem.
That's a separate issue. The "expected/result/etc" debate&proposals are first and foremost about the ('all new and improved') mechanism for passing errors around. The problem of how to abstract semantically 'similar' but syntactically 'not the same' error reporting mechanisms across platforms (such as your errno vs win32 last error code example) is orthogonal to this. However I do think that that problem should also be solved by the same library as the first one, i.e. that an err-like library would offer a std::result-like 'synthesis' of the 'error codes' vs 'exceptions' dialectic _and_ offer wrappers, abstractions and utilities to work with platform errors (e.g. NTSTATUS, HRESULT, win32 last error, errno[1], OSX OSStatus, ObjC exceptions, JNI etc.) and on top of that generalized Boost.Exception-like functionality - IOW a complete "error library"...
Hi, in case this could help. There were two reports on this subject. "Handling Disappointment in C++" by L. Crowl [1] and the other in the draft state "Survey of Error Handling" by N. Bolas [2]. Best, Vicente [1] Handling Disappointment in C++ http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0157r0.html [2] Survey of Error Handling https://11080372623597421729.googlegroups.com/attach/a8bf61b2beb10bec/Survey%20of%20Error%20Handling.html?part=0.1&view=1&vt=ANaJVrGXbT5TGHEPTZWW_iduAUmJBNCyB6yvlV3L0LQk5eZ3dsCSBPSx0hql8fZ3MMCGjw-xBx8bIt6e4-4A0q_fE1U_7jREulA6RbE2Roh4sHOov8TUMtw
On Sat, Feb 6, 2016 at 3:53 AM, Vicente J. Botet Escriba < vicente.botet@wanadoo.fr> wrote:
Le 04/02/2016 23:02, Domagoj Saric a écrit :
On 29.1.2016. 1:57, Emil Dotchevski wrote:
On Thu, Jan 28, 2016 at 3:44 PM, Domagoj Saric
wrote: Hopefully these efforts will bring us to a solution that will do away the, IMO, false dichotomy between "errors" and "exceptions" (
vs <exception>) - rather we will start thinking only about "errors"/error objects which can be transfered in two different ways - like a ball in a rugby game - it can be passed 'on foot' hand to hand or it can be thrown over intermediate players to its final destination (only with errors its in reverse - they are mostly passed/returned or thrown 'backwards':). IOW things like error_info will be applicable to 'the' standardized error type(s) that functions also return, not just throw. You could theoretically pass exceptions function-to-function by exception_ptr but that seems backwards, no pun intended. :)
And we could theoretically and practically ;) return actual error objects (even those considered 'exceptions', e.g. std::runtime_error) w/o necessarily stuffing them into some type erasing mechanism like exception_ptr (i.e. you pass the ball as a ball until you need to interact with someone who only understands abstract UFOs;)
Anyway, my point is that there are two ways one can go about solving
the problem of transporting error codes. One is what seems to be the path you're taking, where you design a common API that can fit all error codes on all platforms. On the other hand, the Boost Exception approach sidesteps this difficult problem.
That's a separate issue. The "expected/result/etc" debate&proposals are first and foremost about the ('all new and improved') mechanism for passing errors around. The problem of how to abstract semantically 'similar' but syntactically 'not the same' error reporting mechanisms across platforms (such as your errno vs win32 last error code example) is orthogonal to this. However I do think that that problem should also be solved by the same library as the first one, i.e. that an err-like library would offer a std::result-like 'synthesis' of the 'error codes' vs 'exceptions' dialectic _and_ offer wrappers, abstractions and utilities to work with platform errors (e.g. NTSTATUS, HRESULT, win32 last error, errno[1], OSX OSStatus, ObjC exceptions, JNI etc.) and on top of that generalized Boost.Exception-like functionality - IOW a complete "error library"...
<snip> [1] Handling Disappointment in C++ http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0157r0.html
Generally I take issue with claims that exception handling overhead can cause performance problems. The linked paper mentions one of the problems being variance in execution time between the error-no error execution paths, which can't be addressed anyway since detecting an error early on in an attempted operation may skip significant amount of code. Another stated concern is that exception handling introduces overhead throughout the program even when exceptions are not being propagated, however such overhead (if any) is limited to function call and exit points. In performance-critical contexts function calls are too expensive with or without the added overhead of exception handling, and the solution is to inline the function. In that case all function call overhead disappears, including the exception handling overhead. Emil
On 6.2.2016. 21:48, Emil Dotchevski wrote:
Another stated concern is that exception handling introduces overhead throughout the program even when exceptions are not being propagated, however such overhead (if any) is limited to function call and exit points. In performance-critical contexts function calls are too expensive with or without the added overhead of exception handling, and the solution is to inline the function. In that case all function call overhead disappears, including the exception handling overhead.
However * this reasoning is only valid (and even then not fully) in synthetic tests/non-real world analysis - i.e. 'my program is the only one on the system' - on a real system, this reasoning is merely the number one licence to produce bloatware as there everything is 'performance critical' - your 'non critical' (i.e. fatter and slower than necessary) code (in addition to contributing to OTA and flash storage costs, IO, fragmentation...) might be loaded and/or executed (or simply occupy the virtual address space) at the same time that another app is trying to "squeeze the most of a user's device" (i.e. running its 'critical' part) * inlining is, in general, a primitve bruteforce solution that can only exacerbate the problem of bloatware (discussed this in more detail relatively recently with Andrey Semashev) * even the very latest MSVC compiler cannot inline functions that contain certain types of EH (even something simple as having a by-value parameter that has a non-trivial destructor)... -- "What Huxley teaches is that in the age of advanced technology, spiritual devastation is more likely to come from an enemy with a smiling face than from one whose countenance exudes suspicion and hate." Neil Postman
On Sun, Feb 7, 2016 at 11:02 AM, Domagoj Saric
On 6.2.2016. 21:48, Emil Dotchevski wrote:
Another stated concern is that exception handling introduces overhead throughout the program even when exceptions are not being propagated, however such overhead (if any) is limited to function call and exit points. In performance-critical contexts function calls are too expensive with or without the added overhead of exception handling, and the solution is to inline the function. In that case all function call overhead disappears, including the exception handling overhead.
However * this reasoning is only valid (and even then not fully) in synthetic tests/non-real world analysis - i.e. 'my program is the only one on the system' - on a real system, this reasoning is merely the number one licence to produce bloatware as there everything is 'performance critical' - your 'non critical' (i.e. fatter and slower than necessary) code (in addition to contributing to OTA and flash storage costs, IO, fragmentation...) might be loaded and/or executed (or simply occupy the virtual address space) at the same time that another app is trying to "squeeze the most of a user's device" (i.e. running its 'critical' part)
There are upsides to "slower" code. Are you saying that *your* code can't be optimized any further? :) Or is it that you've chosen to draw the line arbitrarily at "exception handling is too slow"? Code size does matter on a system with, say, 32 megs of ram, like the Playstation 2. If, on a modern system, the size of your code is the reason why you're running out of memory or address space, exception handling overhead is the least of your worries. :)
* inlining is, in general, a primitve bruteforce solution that can only exacerbate the problem of bloatware (discussed this in more detail relatively recently with Andrey Semashev)
Indeed, inlining shouldn't be used throughout the code, to avoid bloat and for many other reasons. When you can afford the cost of a function call you should opt to pay it because this reduces physical coupling.
* even the very latest MSVC compiler cannot inline functions that contain certain types of EH (even something simple as having a by-value parameter that has a non-trivial destructor)...
So? Either you can afford the function call, or you must tweak the code until the call disappears. The hard fact in discussions about exception handling overhead is that either 1) you can afford it or 2) you can easily remove it selectively, by using inline. Emil
On 2/7/2016 3:21 PM, Emil Dotchevski wrote:
On Sun, Feb 7, 2016 at 11:02 AM, Domagoj Saric
wrote: On 6.2.2016. 21:48, Emil Dotchevski wrote:
Another stated concern is that exception handling introduces overhead throughout the program even when exceptions are not being propagated, however such overhead (if any) is limited to function call and exit points. In performance-critical contexts function calls are too expensive with or without the added overhead of exception handling, and the solution is to inline the function. In that case all function call overhead disappears, including the exception handling overhead.
However * this reasoning is only valid (and even then not fully) in synthetic tests/non-real world analysis - i.e. 'my program is the only one on the system' - on a real system, this reasoning is merely the number one licence to produce bloatware as there everything is 'performance critical' - your 'non critical' (i.e. fatter and slower than necessary) code (in addition to contributing to OTA and flash storage costs, IO, fragmentation...) might be loaded and/or executed (or simply occupy the virtual address space) at the same time that another app is trying to "squeeze the most of a user's device" (i.e. running its 'critical' part)
There are upsides to "slower" code. Are you saying that *your* code can't be optimized any further? :) Or is it that you've chosen to draw the line arbitrarily at "exception handling is too slow"?
Code size does matter on a system with, say, 32 megs of ram, like the Playstation 2. If, on a modern system, the size of your code is the reason why you're running out of memory or address space, exception handling overhead is the least of your worries. :)
* inlining is, in general, a primitve bruteforce solution that can only exacerbate the problem of bloatware (discussed this in more detail relatively recently with Andrey Semashev)
Indeed, inlining shouldn't be used throughout the code, to avoid bloat and for many other reasons. When you can afford the cost of a function call you should opt to pay it because this reduces physical coupling.
* even the very latest MSVC compiler cannot inline functions that contain certain types of EH (even something simple as having a by-value parameter that has a non-trivial destructor)...
So? Either you can afford the function call, or you must tweak the code until the call disappears.
The hard fact in discussions about exception handling overhead is that either 1) you can afford it or 2) you can easily remove it selectively, by using inline.
I'm not sure why the topic has moved into a discussion of the overhead and performance of exception handling. I think the performance characteristics of EH are pretty well understood (at least around these hallowed halls). Users who want a result<T> abstraction however are not interested in a solution like EH which is heavily biased in performance towards the non-error control flow. Errors reported through a result<T> abstraction are usually not exceptional and I would expect comparable performance between the error and non-error control flow. Using result<T> doesn't preclude exception handling. I think they work very well together. i.e. in the case of text entry you might return a resultstd::string valid text would return directly invalid text would return an error code failure to allocate memory for the std::string would throw an exception
On Mon, Feb 8, 2016 at 6:21 PM, Michael Marcin
Using result<T> doesn't preclude exception handling. I think they work very well together.
If you mean that they can work together within the same program, that's obviously true. At some level it's all C and it's all error codes, so arguably you can't escape them. But of course the two approaches use completely different strategies. In one case the programmer has to write if( error ) return error, like a neanderthal. In the other, he has to write exception-safe code but the upside is less error handling bugs, which is important as error handling code is notoriously difficult to test. Yes, with exceptions you do get different performance between the error-no error paths, and that is critical in some applications, but these are extremely specialized. In these cases one can't use dynamic memory allocations either, which renders a lot of standard C and C++ functions off limits, not only exception handling.
i.e. in the case of text entry you might return a resultstd::string valid text would return directly invalid text would return an error code failure to allocate memory for the std::string would throw an exception
In the wild I've never seen a bad_alloc from std::string, except when it was caused by a logic error in my program. If exceptions are used just so that std::string can throw bad_alloc, that's 1) not worth the overhead of exception handling and 2) extremely difficult to test.. Besides, in the case of text entry I doubt very much that the difference in performance between the error-no error paths would matter in practice. Of course sometimes you may need to distinguish between "nothing entered" and "empty string entered" without signalling an illegal entry. In this case you can simply return shared_ptrstd::string, and throw only in case of illegal entry. Cheers, Emil
On 8 Feb 2016 at 23:27, Emil Dotchevski wrote:
But of course the two approaches use completely different strategies. In one case the programmer has to write if( error ) return error, like a neanderthal. In the other, he has to write exception-safe code but the upside is less error handling bugs, which is important as error handling code is notoriously difficult to test.
In practice if you're returning an important outcome from a function then you'll always be using it, so you don't notice the fact you're checking return values as much. Still, I agree that result<void> is as easy to forget about as an int return code, and propagating errored outcomes up the call stack is unavoidably manual (though macro helpers make it a one line exercise). It's still vastly more powerful and convenient to write than int error code returns a la C, and future clang-tidy tooling can help out a lot on avoiding mistakes unlike C int error code returns which could mean anything, and therefore cannot be AST rewritten. Something missing from the discussion so far is that expected/result/outcome MUST throw exceptions! If you try to fetch a value from a result and it contains an error, there is no alternative to throwing an exception. This fact is why I don't worry about static function initialisers and just go ahead and use error-code-via-system_error throwing constructors, ultimately you need try...catch in there one way or another. I would therefore not draw as thick a line between the two approaches as some here are doing. For systems programming where you are a thin veneer around OS calls a 98% result based noexcept programming style is the optimal way of doing this sort of C++ programming - tiny runtime overhead, powerful abstraction over C error code programming, highly maintainable and extremely amenable to static analysis to catch logic errors. With just a little extra libclang tooling (some of which I plan to write) this style idiom ought to be mathematically provable as correct in the functional programming sense, which would be cool, not least for those programming nuclear reactors etc. The further you get away from errno/GetLastError() though the more you end up exclusively on C++ exceptions which is no bad thing. Large teams of engineers make better code more cheaply when the language does exceptions, though the skills demands of writing exception safe C++ is non trivial. Still, as one can see in LLVM's universal exception handling framework, C++ exception throws ought to seamlessly convert into other LLVM based languages, so potentially a big future win there: write just enough in C++, write the rest in some language average programmers do better with, both layers seamlessly work together. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
Le 09/02/2016 10:16, Niall Douglas a écrit :
On 8 Feb 2016 at 23:27, Emil Dotchevski wrote:
But of course the two approaches use completely different strategies. In one case the programmer has to write if( error ) return error, like a neanderthal. In the other, he has to write exception-safe code but the upside is less error handling bugs, which is important as error handling code is notoriously difficult to test. In practice if you're returning an important outcome from a function then you'll always be using it, so you don't notice the fact you're checking return values as much. Still, I agree that result<void> is as easy to forget about as an int return code, and propagating errored outcomes up the call stack is unavoidably manual (though macro helpers make it a one line exercise). It's still vastly more powerful and convenient to write than int error code returns a la C, and future clang-tidy tooling can help out a lot on avoiding mistakes unlike C int error code returns which could mean anything, and therefore cannot be AST rewritten.
Something missing from the discussion so far is that expected/result/outcome MUST throw exceptions! If you try to fetch a value from a result and it contains an error, there is no alternative to throwing an exception. Yes there is one, to don't try to obtain the value directly with a get/value function. If you don't want to use exceptions, you should use visitation, functor/monad interfaces which are always safe.
No need to write anymore neanderthal code like if(error) ;-) Vicente
On Tue, Feb 9, 2016 at 1:16 AM, Niall Douglas
On 8 Feb 2016 at 23:27, Emil Dotchevski wrote:
But of course the two approaches use completely different strategies. In one case the programmer has to write if( error ) return error, like a neanderthal. In the other, he has to write exception-safe code but the upside is less error handling bugs, which is important as error handling code is notoriously difficult to test. <deleted> I would therefore not draw as thick a line between the two approaches as some here are doing. For systems programming where you are a thin veneer around OS calls a 98% result based noexcept programming style is the optimal way of doing this sort of C++ programming - tiny runtime overhead, powerful abstraction over C error code programming, highly maintainable and extremely amenable to static analysis to catch logic errors.
Lots of adjectives, perhaps we should discuss each one of them? :) The use of error codes in low level APIs is a fact of life, since most of that code is written in C anyway. I wouldn't call it optimal in C++, I personally convert error codes into exceptions at the earliest possible opportunity. As for runtime overhead, like I said already, where it matters it can be easily deleted, by inlining; and, in such contexts you need to inline with or without exception handling: I've never seen a case where I could afford the overhead of a function call but could not afford the exception handling overhead in the same function. It feels strange to have to defend the use of exceptions for reporting errors in C++, on the boost development board of all places. There are many other advantages, for example when returning errors there is no such thing as error-neutral contexts in your program, which increases coupling. Yes, in some contexts one can't afford to use exceptions, but all general complains that exception handling causes performance or any other problems are theoretical, at best.
With just a little extra libclang tooling (some of which I plan to write) this style idiom ought to be mathematically provable as correct in the functional programming sense, which would be cool, not least for those programming nuclear reactors etc.
Could you prove anything mathematically in the presence of side effects and pointers? Emil
But of course the two approaches use completely different strategies. In one case the programmer has to write if( error ) return error, like a neanderthal. In the other, he has to write exception-safe code but the upside is less error handling bugs, which is important as error handling code is notoriously difficult to test. <deleted> I would therefore not draw as thick a line between the two approaches as some here are doing. For systems programming where you are a thin veneer around OS calls a 98% result based noexcept programming style is the optimal way of doing this sort of C++ programming - tiny runtime overhead, powerful abstraction over C error code programming, highly maintainable and extremely amenable to static analysis to catch logic errors.
Lots of adjectives, perhaps we should discuss each one of them? :)
The use of error codes in low level APIs is a fact of life, since most of that code is written in C anyway. I wouldn't call it optimal in C++, I personally convert error codes into exceptions at the earliest possible opportunity. As for runtime overhead, like I said already, where it matters it can be easily deleted, by inlining; and, in such contexts you need to inline with or without exception handling: I've never seen a case where I could afford the overhead of a function call but could not afford the exception handling overhead in the same function.
It feels strange to have to defend the use of exceptions for reporting errors in C++, on the boost development board of all places. There are many other advantages, for example when returning errors there is no such thing as error-neutral contexts in your program, which increases coupling. Yes, in some contexts one can't afford to use exceptions, but all general complains that exception handling causes performance or any other problems are theoretical, at best.
I couldn't agree more. It is questionable at best to base design decisions on the pseudo argument of 'exceptions incur too much overhead'. In my experience, the benefits of using exceptions clearly outweighs any disadvantages. Regards Hartmut --------------- http://boost-spirit.com http://stellar.cct.lsu.edu
On 10/02/2016 09:04, Emil Dotchevski wrote:
The use of error codes in low level APIs is a fact of life, since most of that code is written in C anyway. I wouldn't call it optimal in C++, I personally convert error codes into exceptions at the earliest possible opportunity. As for runtime overhead, like I said already, where it matters it can be easily deleted, by inlining; and, in such contexts you need to inline with or without exception handling: I've never seen a case where I could afford the overhead of a function call but could not afford the exception handling overhead in the same function.
The overhead of exception-handling plumbing that is not actually exercised is typically minimal and not worth worrying about unless you're in one of those contexts where you begrudge every method call, as you've said. The overhead of a thrown exception is larger, particularly on those systems where throwing an exception also captures a stack trace (which is incredibly helpful for debugging, but sadly often quite expensive). The overhead of dealing with error return codes for uncommon failures is also fairly large, with the risk of ignoring them and ending up in weird states. The trick is to find the balance between them, with the complication that it may not be known at the library level which failures are exceptional and which are expected. As an example, I can imagine that most applications don't want a "delete file" method to throw an exception if the file is already absent -- but there might be some transactional systems in which that would be a serious problem, and checking for existence beforehand might not be sufficient as it would still be a race condition. This sort of thing could be a good argument for "degrees of success", where eg. the above delete call returns success in both cases but can also indicate that the file was already missing and it did nothing. (Which is trivial in this case since it can be done with a simple bool return value, but you can probably imagine other cases where it might make sense to return both a T and a non-error status value of some kind.)
On Tue, Feb 9, 2016 at 3:41 PM, Gavin Lambert
On 10/02/2016 09:04, Emil Dotchevski wrote:
The use of error codes in low level APIs is a fact of life, since most of that code is written in C anyway. I wouldn't call it optimal in C++, I personally convert error codes into exceptions at the earliest possible opportunity. As for runtime overhead, like I said already, where it matters it can be easily deleted, by inlining; and, in such contexts you need to inline with or without exception handling: I've never seen a case where I could afford the overhead of a function call but could not afford the exception handling overhead in the same function.
The overhead of exception-handling plumbing that is not actually exercised is typically minimal and not worth worrying about unless you're in one of those contexts where you begrudge every method call, as you've said.
The overhead of a thrown exception is larger, particularly on those systems where throwing an exception also captures a stack trace (which is incredibly helpful for debugging, but sadly often quite expensive).
True, but I'm yet to encounter a case where I'd care about that overhead. And I don't particularly care how that time compares to the amount of time the successful path takes, since both of them typically vary significantly from one platform to another. Assuming we're dealing with an exceptional situation (something went wrong), that's not a problem. Besides, I'd happily agree -- in the presence of profiler evidence -- that if in a given use case exception handling overhead causes problems which can not be alleviated by inlining a few functions, it shouldn't be used. This is not because returning error codes is "better": in some cases we also do need to resort to writing hand-crafted platform-specific assembly, but we shouldn't argue that we can't use C++ in general because of overhead.
The overhead of dealing with error return codes for uncommon failures is also fairly large, with the risk of ignoring them and ending up in weird states.
The trick is to find the balance between them, with the complication that it may not be known at the library level which failures are exceptional and which are expected. As an example, I can imagine that most applications don't want a "delete file" method to throw an exception if the file is already absent -- but there might be some transactional systems in which that would be a serious problem, and checking for existence beforehand might not be sufficient as it would still be a race condition.
This reasoning is completely orthogonal to the error reporting design -- it has to do with correctly defining the postconditions of the function, which as you point out is domain-specific. Exceptions are used specifically to enforce postconditions. If the correct postcondition of delete_file is "the file does not exist", then it is incorrect to throw if the file was missing to begin with.
This sort of thing could be a good argument for "degrees of success", where eg. the above delete call returns success in both cases but can also indicate that the file was already missing and it did nothing. (Which is trivial in this case since it can be done with a simple bool return value, but you can probably imagine other cases where it might make sense to return both a T and a non-error status value of some kind.)
Assuming the correct postcondition for delete_file is "the file does not exist", and assuming that knowing if the file existed is either important or trivial for delete_file to detect, the correct interface of that function is: //Postconditions: the specified file does not exist. //Returns: true if the file was actually deleted, false if it did not exist //Throws: delete_file_error bool delete_file( char const * name ); This is not "degrees of success", as the condition in which the file did not exist is not an error. Emil
On 10/02/2016 15:41, Emil Dotchevski wrote:
This sort of thing could be a good argument for "degrees of success", where eg. the above delete call returns success in both cases but can also indicate that the file was already missing and it did nothing. (Which is trivial in this case since it can be done with a simple bool return value, but you can probably imagine other cases where it might make sense to return both a T and a non-error status value of some kind.)
Assuming the correct postcondition for delete_file is "the file does not exist", and assuming that knowing if the file existed is either important or trivial for delete_file to detect, the correct interface of that function is:
//Postconditions: the specified file does not exist. //Returns: true if the file was actually deleted, false if it did not exist //Throws: delete_file_error bool delete_file( char const * name );
This is not "degrees of success", as the condition in which the file did not exist is not an error.
As I said, that was a trivial case. For a less trivial case, imagine a function that fills in a variable-size data structure, but can also indicate that some of it was truncated and supplying a larger buffer would get a more complete result. (Sort of like strncpy, but more complex.) Or a cursor enumeration function that wants to simultaneously return a pageful of data and how many records are left unreported, so these don't have to be checked separately, but the latter could be ignored in contexts where it's not interesting. Of course, these can be handled by making a larger return type T (perhaps as a tuple), but this implies they have equal weight, which is not necessarily the case. It's also brittle if a future version of the method wants to add more information. Having said that, I'm not entirely sure where I was going with this topic. I might be rambling a bit.
On Tue, Feb 9, 2016 at 7:43 PM, Gavin Lambert
On 10/02/2016 15:41, Emil Dotchevski wrote:
This sort of thing could be a good argument for "degrees of success",
where eg. the above delete call returns success in both cases but can also indicate that the file was already missing and it did nothing. (Which is trivial in this case since it can be done with a simple bool return value, but you can probably imagine other cases where it might make sense to return both a T and a non-error status value of some kind.)
Assuming the correct postcondition for delete_file is "the file does not exist", and assuming that knowing if the file existed is either important or trivial for delete_file to detect, the correct interface of that function is:
//Postconditions: the specified file does not exist. //Returns: true if the file was actually deleted, false if it did not exist //Throws: delete_file_error bool delete_file( char const * name );
This is not "degrees of success", as the condition in which the file did not exist is not an error.
As I said, that was a trivial case.
For a less trivial case, imagine a function that fills in a variable-size data structure, but can also indicate that some of it was truncated and supplying a larger buffer would get a more complete result. (Sort of like strncpy, but more complex.)
Or a cursor enumeration function that wants to simultaneously return a pageful of data and how many records are left unreported, so these don't have to be checked separately, but the latter could be ignored in contexts where it's not interesting.
It doesn't matter what your use case is: while the success/failure vocabulary isn't incorrect, it is more precise to think in terms of postconditions. You call a function, it will either satisfy its postconditions or it won't return. Sure, you can and should return all information that the caller needs in order to proceed with whatever operation is being attempted, but there is no need to return anything if the operation can't proceed. So all you need to do is correctly identify the conditions that indicate that the operation can't proceed, and throw an exception. Emil
On 11/02/2016 08:40, Emil Dotchevski wrote:
It doesn't matter what your use case is: while the success/failure vocabulary isn't incorrect, it is more precise to think in terms of postconditions. You call a function, it will either satisfy its postconditions or it won't return. Sure, you can and should return all information that the caller needs in order to proceed with whatever operation is being attempted, but there is no need to return anything if the operation can't proceed.
So all you need to do is correctly identify the conditions that indicate that the operation can't proceed, and throw an exception.
That's still a fuzzy line though. In the delete file case, the operation of deleting the file cannot proceed because the file is already absent. However the post-condition of "the file no longer exists" is still met. Is that success or failure? There are other cases where an operation might fail, or (if you prefer that term) be unable to proceed (eg. "queue is full, cannot push new item"), but throwing an exception in this case is not a good design choice unless you *know* that the queue is never supposed to be full -- which is not something that the queue itself can know unless it's a supposedly unbounded queue and so the only way it can be full is if it cannot allocate more memory. But for bounded queues, the library generally has to assume that it could reach the bound at some point and have to refuse to add new items, so that should be a status return rather than an exception. And yet the application could choose to use such a queue with a really large bound or with a producer that's massively slower than the consumer, and so the assumptions the library made about the balance of success vs. failure weren't correct.
On Wed, Feb 10, 2016 at 1:30 PM, Gavin Lambert
On 11/02/2016 08:40, Emil Dotchevski wrote:
It doesn't matter what your use case is: while the success/failure vocabulary isn't incorrect, it is more precise to think in terms of postconditions. You call a function, it will either satisfy its postconditions or it won't return. Sure, you can and should return all information that the caller needs in order to proceed with whatever operation is being attempted, but there is no need to return anything if the operation can't proceed.
So all you need to do is correctly identify the conditions that indicate that the operation can't proceed, and throw an exception.
That's still a fuzzy line though. In the delete file case, the operation of deleting the file cannot proceed because the file is already absent. However the post-condition of "the file no longer exists" is still met. Is that success or failure?
"Proceed", I mean the caller. Let's say you have code which opens the file then reads from it. The caller can not proceed to reading if the file couldn't be opened. So, the postcondition of the open operation is that the file was successfully opened. Similarly, the postcondition of delete_file is that the file does not exist, because presumably the caller of delete_file can't proceed if the file still exists.
There are other cases where an operation might fail, or (if you prefer that term) be unable to proceed (eg. "queue is full, cannot push new item"), but throwing an exception in this case is not a good design choice unless you *know* that the queue is never supposed to be full -- which is not something that the queue itself can know unless it's a supposedly unbounded queue and so the only way it can be full is if it cannot allocate more memory.
If you know that the queue is never supposed to be full, that is, if the full queue indicates a bug in your code, then you should assert rather than throw. Throwing is when you expect the program to successfully recover from an anticipated (by the programmer) failure.
But for bounded queues, the library generally has to assume that it could reach the bound at some point and have to refuse to add new items, so that should be a status return rather than an exception.
It depends. For example, if this is a keyboard buffer queue, then probably you'd return a status to indicate that there's no more space, and the caller will "beep" to tell the user to slow down with the typing. In this case it would be annoying for the caller to have to catch an exception. On the other hand, if it's a job queue, where the caller expects all submitted jobs to be queued and at some point completed, then an exception is more appropriate, so the immediate caller wouldn't have to care (push won't return), while some context higher up the call stack can perhaps retry the whole sequence of jobs, or notify the user that the operation failed. We can't discuss this stuff in the abstract. Defining correct postconditions depends very much on the specific API being designed.
And yet the application could choose to use such a queue with a really large bound or with a producer that's massively slower than the consumer, and so the assumptions the library made about the balance of success vs. failure weren't correct.
In this case either we have a user error or a library design error. You have to get the postconditions right, or else the users will end up having to deal with exceptions in what they consider non-failure situations, or end up writing (and forgetting to write) if( error ) return error. I personally have better things to do. :) Emil
On 11/02/2016 12:23, Emil Dotchevski wrote:
That's still a fuzzy line though. In the delete file case, the operation of deleting the file cannot proceed because the file is already absent. However the post-condition of "the file no longer exists" is still met. Is that success or failure?
"Proceed", I mean the caller. Let's say you have code which opens the file then reads from it. The caller can not proceed to reading if the file couldn't be opened. So, the postcondition of the open operation is that the file was successfully opened. Similarly, the postcondition of delete_file is that the file does not exist, because presumably the caller of delete_file can't proceed if the file still exists.
As I said before though, there could be some applications (eg. certain types of databases) which can't proceed if that particular deletion action was not the cause of the file ceasing to exist (ie. if some other process deleted it in advance). It's not as clear-cut as you seem to be suggesting.
If you know that the queue is never supposed to be full, that is, if the full queue indicates a bug in your code, then you should assert rather than throw. Throwing is when you expect the program to successfully recover from an anticipated (by the programmer) failure.
The library can't know that, though (unless it was designed to be unbounded, so can't be full unless memory allocation fails), so it must pass this state out to the application to deal with, and an exception is *not necessarily* the right way to do so. (And neither is not-an-exception, for that matter.) If you *had* to pick one or the other, though, in cases where failure is unusual (such as failing to push to an unbounded queue) an exception makes more sense, and in cases where failure is not unusual (such as failing to push to a bounded queue) a return status makes more sense. I think it's been well established that exceptions are great for dealing with unexpected failure (since you have to actively ignore them), but not so great for expected failure (since they can do heavy things like capturing call stacks and stack unwinding).
It depends. For example, if this is a keyboard buffer queue, then probably you'd return a status to indicate that there's no more space, and the caller will "beep" to tell the user to slow down with the typing. In this case it would be annoying for the caller to have to catch an exception. On the other hand, if it's a job queue, where the caller expects all submitted jobs to be queued and at some point completed, then an exception is more appropriate, so the immediate caller wouldn't have to care (push won't return), while some context higher up the call stack can perhaps retry the whole sequence of jobs, or notify the user that the operation failed.
We can't discuss this stuff in the abstract. Defining correct postconditions depends very much on the specific API being designed.
Except that for things like generic data structures, the implementing library can't possibly know what the application is going to put in them or do with them. So you have to deal in the abstract, which is why you often need both throwing and non-throwing API in those sorts of cases -- only at the application level can you finally decide which one to call.
On Wed, Feb 10, 2016 at 5:02 PM, Gavin Lambert
On 11/02/2016 12:23, Emil Dotchevski wrote:
That's still a fuzzy line though. In the delete file case, the operation
of deleting the file cannot proceed because the file is already absent. However the post-condition of "the file no longer exists" is still met. Is that success or failure?
"Proceed", I mean the caller. Let's say you have code which opens the file then reads from it. The caller can not proceed to reading if the file couldn't be opened. So, the postcondition of the open operation is that the file was successfully opened. Similarly, the postcondition of delete_file is that the file does not exist, because presumably the caller of delete_file can't proceed if the file still exists.
As I said before though, there could be some applications (eg. certain types of databases) which can't proceed if that particular deletion action was not the cause of the file ceasing to exist (ie. if some other process deleted it in advance). It's not as clear-cut as you seem to be suggesting.
I didn't say that all possible functions from all possible libraries that may delete files should have the same postconditions.
If you know that the queue is never supposed to be full, that is, if the
full queue indicates a bug in your code, then you should assert rather than throw. Throwing is when you expect the program to successfully recover from an anticipated (by the programmer) failure.
The library can't know that, though (unless it was designed to be unbounded, so can't be full unless memory allocation fails), so it must pass this state out to the application to deal with, and an exception is *not necessarily* the right way to do so. (And neither is not-an-exception, for that matter.)
Here is an example: initializing a shared_ptr from an expired weak_ptr throws. The designer recognized that this is not always practical, so he gave us weak_ptr::lock, which does not throw. So the user has both options available. On the other hand, you might want dereferencing a shared_ptr to throw an exception in case it's empty, but that's not an option. Why? Because the shared_ptr designer said so. Dereferencing an empty shared_ptr is undefined behavior, and that's that. Designing correct preconditions and postconditions for each function isn't easy, and the answer isn't "oh I don't know, you might need 57 different versions".
If you *had* to pick one or the other, though, in cases where failure is unusual (such as failing to push to an unbounded queue) an exception makes more sense, and in cases where failure is not unusual (such as failing to push to a bounded queue) a return status makes more sense.
I think it's been well established that exceptions are great for dealing with unexpected failure (since you have to actively ignore them), but not so great for expected failure (since they can do heavy things like capturing call stacks and stack unwinding).
It depends. For example, if this is a keyboard buffer queue, then probably
you'd return a status to indicate that there's no more space, and the caller will "beep" to tell the user to slow down with the typing. In this case it would be annoying for the caller to have to catch an exception. On the other hand, if it's a job queue, where the caller expects all submitted jobs to be queued and at some point completed, then an exception is more appropriate, so the immediate caller wouldn't have to care (push won't return), while some context higher up the call stack can perhaps retry the whole sequence of jobs, or notify the user that the operation failed.
We can't discuss this stuff in the abstract. Defining correct postconditions depends very much on the specific API being designed.
Except that for things like generic data structures, the implementing library can't possibly know what the application is going to put in them or do with them.
So you have to deal in the abstract, which is why you often need both throwing and non-throwing API in those sorts of cases -- only at the application level can you finally decide which one to call.
The shared_ptr functions I just mentioned are counter-examples for the claim that only the application programmer can make these decisions. If you need another example, consider std::vector: if you attempt to push_back, and it needs to grow its buffer, but it fails to grow its buffer, it throws, even if the value_type has no-throw copy constructor. There is no no-throw option. Emil
On 11/02/2016 12:23, Emil Dotchevski wrote:
If you know that the queue is never supposed to be full, that is, if the full queue indicates a bug in your code, then you should assert rather than throw. Throwing is when you expect the program to successfully recover from an anticipated (by the programmer) failure.
Sorry, I missed responding to this part. No, you should not assert in this case. Filling a queue is something that easily could happen at runtime in release mode (even if the programmer thinks it's not supposed to), where asserts are disabled and valueless. You could assert *and* throw (or abort, if you don't know how to recover from it), although arguably the assert is less useful if you're repeating the same condition outside the assert anyway. But you can't just slap an assert in and call it a day, at least not for this sort of condition.
On Wed, Feb 10, 2016 at 5:11 PM, Gavin Lambert
On 11/02/2016 12:23, Emil Dotchevski wrote:
If you know that the queue is never supposed to be full, that is, if the full queue indicates a bug in your code, then you should assert rather than throw. Throwing is when you expect the program to successfully recover from an anticipated (by the programmer) failure.
Sorry, I missed responding to this part.
No, you should not assert in this case. Filling a queue is something that easily could happen at runtime in release mode (even if the programmer thinks it's not supposed to), where asserts are disabled and valueless.
Dereferencing a null shared_ptr is something that easily could happen at runtime. Are you saying that it shouldn't assert in this case? Are you saying that that assert is useless? Are you saying that the correct design is to throw? Or is the correct design to assert and throw? Emil
On 11/02/2016 15:08, Emil Dotchevski wrote:
Dereferencing a null shared_ptr is something that easily could happen at runtime. Are you saying that it shouldn't assert in this case? Are you saying that that assert is useless? Are you saying that the correct design is to throw? Or is the correct design to assert and throw?
I answered that in the next paragraph: On 11/02/2016 14:11, I wrote:
You could assert *and* throw (or abort, if you don't know how to recover from it), although arguably the assert is less useful if you're repeating the same condition outside the assert anyway. But you can't just slap an assert in and call it a day, at least not for this sort of condition.
Or you could just throw. I'm not convinced that the assert itself adds any particular value, aside from possibly being more in-your-face when running a debug build. (But thrown exceptions can be equally in your face in both debug and release builds if you have a debugger attached, and if throwing exceptions is unusual so you haven't disabled "break on exception throw".)
On Wed, Feb 10, 2016 at 9:23 PM, Gavin Lambert
On 11/02/2016 15:08, Emil Dotchevski wrote:
Dereferencing a null shared_ptr is something that easily could happen at runtime. Are you saying that it shouldn't assert in this case? Are you saying that that assert is useless? Are you saying that the correct design is to throw? Or is the correct design to assert and throw?
I answered that in the next paragraph:
On 11/02/2016 14:11, I wrote:
You could assert *and* throw (or abort, if you don't know how to recover from it), although arguably the assert is less useful if you're repeating the same condition outside the assert anyway. But you can't just slap an assert in and call it a day, at least not for this sort of condition.
Or you could just throw. I'm not convinced that the assert itself adds any particular value, aside from possibly being more in-your-face when running a debug build. (But thrown exceptions can be equally in your face in both debug and release builds if you have a debugger attached, and if throwing exceptions is unusual so you haven't disabled "break on exception throw".)
It appears that you think that C++ exceptions are "unusual" in the same way OS exceptions are unusual: the OS detected something bad going on and raises an exception, as many OSes do for example in the case of dereferencing a null pointer. That's not at all what C++ exceptions are. They are in fact specifically designed to replace the need to return error codes, so that handling errors is simpler, safer and more testable. As for shared_ptr::operator*, I know it could be made to throw, but that would be incorrect design. I mentioned shared_ptr to make the point that you're wrong that this kind of design decision can not be made in generic C++ code. STL too is full of generic functions that throw to indicate a failure, and others that do not, without giving the user a choice. Even at the language level, consider that in C++ constructors don't give you the option to return an error code, the only way for them to fail is by throwing. Why? Because that is the correct design. Emil
On 12/02/2016 11:57, Emil Dotchevski wrote:
It appears that you think that C++ exceptions are "unusual" in the same way OS exceptions are unusual: the OS detected something bad going on and raises an exception, as many OSes do for example in the case of dereferencing a null pointer. That's not at all what C++ exceptions are. They are in fact specifically designed to replace the need to return error codes, so that handling errors is simpler, safer and more testable.
Perhaps I'm biased by mostly developing on Windows+MSVC, but since that implements C++ exceptions by raising OS exceptions, and OS exceptions can be caught and processed just like C++ exceptions, I don't see any distinction between the two. (Though I'm aware that the OS exception handling mechanism is much more broken on non-Windows.) And yes, I think that exceptions should be reserved for unexpected cases. If a method has a case where it is expected to sometimes not produce a value, then it should use optional<T> or similar.
As for shared_ptr::operator*, I know it could be made to throw, but that would be incorrect design. I mentioned shared_ptr to make the point that you're wrong that this kind of design decision can not be made in generic C++ code. STL too is full of generic functions that throw to indicate a failure, and others that do not, without giving the user a choice. Even at the language level, consider that in C++ constructors don't give you the option to return an error code, the only way for them to fail is by throwing. Why? Because that is the correct design.
The only reason that shared_ptr::operator* does not throw is that the class author decided that this is likely a hot path and the calling code has *probably* already checked for null, so it is more *efficient* to omit the check entirely (and cause undefined behavior if called in violation of that assumption). As such an assert is used to *hopefully* catch those violations, but there are only limited cases in which it will do so. Checking and throwing will always be more *safe/correct*, but for this particular method the author chose to prioritise efficiency over safety. (Although shared_ptr has an additional bias -- if the operator* precondition is violated then even if the assert is omitted it's almost certainly going to immediately cause an OS fault anyway due to a null (or nearly-null) pointer access -- which is arguably equivalent to always-throw behaviour. Methods on other classes typically won't get that for free.) Don't get me wrong, I'm not saying that this is a bad thing. But it can also get applications into trouble and it is useful to provide both options. Just look at all the discussion recently about safe numeric libraries -- that's another case where the language has chosen to value efficiency over safety, and some applications wish for the reverse.
On Thu, Feb 11, 2016 at 3:32 PM, Gavin Lambert
On 12/02/2016 11:57, Emil Dotchevski wrote:
It appears that you think that C++ exceptions are "unusual" in the same way OS exceptions are unusual: the OS detected something bad going on and raises an exception, as many OSes do for example in the case of dereferencing a null pointer. That's not at all what C++ exceptions are. They are in fact specifically designed to replace the need to return error codes, so that handling errors is simpler, safer and more testable.
Perhaps I'm biased by mostly developing on Windows+MSVC, but since that implements C++ exceptions by raising OS exceptions, and OS exceptions can be caught and processed just like C++ exceptions, I don't see any distinction between the two. (Though I'm aware that the OS exception handling mechanism is much more broken on non-Windows.)
It's criminal that MSVC can translate OS exceptions into C++ exceptions. :) It's not that it's broken on other OSes, other compilers don't do this because it's wrong. I do not recommend turning that MSVC option on.
And yes, I think that exceptions should be reserved for unexpected cases. If a method has a case where it is expected to sometimes not produce a value, then it should use optional<T> or similar.
I can see that from your responses but you're wrong. In C++, you can not get an exception unexpectedly. The only way to throw an exception in C++ is to use the throw keyword. Contrast this with hardware exceptions, which may be raised by virtually any CPU instruction and have nothing to do with C++ exceptions.
As for shared_ptr::operator*, I know it could be made to throw, but that
would be incorrect design. I mentioned shared_ptr to make the point that you're wrong that this kind of design decision can not be made in generic C++ code. STL too is full of generic functions that throw to indicate a failure, and others that do not, without giving the user a choice. Even at the language level, consider that in C++ constructors don't give you the option to return an error code, the only way for them to fail is by throwing. Why? Because that is the correct design.
The only reason that shared_ptr::operator* does not throw is that the class author decided that this is likely a hot path and the calling code has *probably* already checked for null, so it is more *efficient* to omit the check entirely (and cause undefined behavior if called in violation of that assumption).
He can speak for himself, but I bet that his motivation was to avoid overhead in the extremely common use case when the programmer *knows* that the shared_ptr isn't null. If I have a shared_ptr (that I didn't get from weak_ptr::lock), more often than not it would be a logic error for it to be null, so it would be dumb to check if it is null when dereferencing it.
(Although shared_ptr has an additional bias -- if the operator* precondition is violated then even if the assert is omitted it's almost certainly going to immediately cause an OS fault anyway due to a null (or nearly-null) pointer access -- which is arguably equivalent to always-throw behaviour.
Absolutely not. Dereferencing a null pointer in C++ is undefined behavior, which is not at all the same as throwing exceptions or raising OS exceptions. When you throw an exception, the standard specifies exactly what is going to happen, but in the case of undefined behavior all bets are off. Maybe your program will crash, maybe it'll send a nasty email to your boss. :) Emil
On 12/02/2016 12:56, Emil Dotchevski wrote:
Perhaps I'm biased by mostly developing on Windows+MSVC, but since that implements C++ exceptions by raising OS exceptions, and OS exceptions can be caught and processed just like C++ exceptions, I don't see any distinction between the two. (Though I'm aware that the OS exception handling mechanism is much more broken on non-Windows.)
It's criminal that MSVC can translate OS exceptions into C++ exceptions. :) It's not that it's broken on other OSes, other compilers don't do this because it's wrong. I do not recommend turning that MSVC option on.
It's not an option. That's just what it does. There are some good things about this, as it makes it easier to recover a stack trace from a thrown exception -- though the corollary bad thing is that exceptions are also more expensive to throw. (Although there *is* an option which decides how aggressively it inserts the plumbing to catch OS exceptions, and leaving that off can improve performance at the cost of reducing the places that you can catch them.)
The only reason that shared_ptr::operator* does not throw is that the class author decided that this is likely a hot path and the calling code has *probably* already checked for null, so it is more *efficient* to omit the check entirely (and cause undefined behavior if called in violation of that assumption).
He can speak for himself, but I bet that his motivation was to avoid overhead in the extremely common use case when the programmer *knows* that the shared_ptr isn't null. If I have a shared_ptr (that I didn't get from weak_ptr::lock), more often than not it would be a logic error for it to be null, so it would be dumb to check if it is null when dereferencing it.
Which is exactly what I said.
Absolutely not. Dereferencing a null pointer in C++ is undefined behavior, which is not at all the same as throwing exceptions or raising OS exceptions. When you throw an exception, the standard specifies exactly what is going to happen, but in the case of undefined behavior all bets are off. Maybe your program will crash, maybe it'll send a nasty email to your boss. :)
On pretty much any platform with an MPU the memory around address 0 is left unmapped precisely such that such accesses generate an OS fault. Yes, this is technically undefined behaviour at the language level, but it is not undefined behaviour at the platform level (though it may still be "defined but unintended behaviour"). And yes, platforms do exist that do not have that characteristic, and they do have C++ compilers, and in that environment you won't get a fault in that case, you will get defined-but-almost-certainly-incorrect behaviour. So truly portable applications cannot *rely* on getting a fault, but there is a decent chance that they will experience it at some point nevertheless. You're taking this out of context, though; it was an aside only peripherally related to the main point.
On Thu, Feb 11, 2016 at 4:57 PM, Gavin Lambert
On 12/02/2016 12:56, Emil Dotchevski wrote:
Perhaps I'm biased by mostly developing on Windows+MSVC, but since that
implements C++ exceptions by raising OS exceptions, and OS exceptions can be caught and processed just like C++ exceptions, I don't see any distinction between the two. (Though I'm aware that the OS exception handling mechanism is much more broken on non-Windows.)
It's criminal that MSVC can translate OS exceptions into C++ exceptions. :) It's not that it's broken on other OSes, other compilers don't do this because it's wrong. I do not recommend turning that MSVC option on.
It's not an option. That's just what it does.
Not true, it's an option. They call it "structural exception handling".
There are some good things about this, as it makes it easier to recover a stack trace from a thrown exception -- though the corollary bad thing is that exceptions are also more expensive to throw.
You can love it or hate it, but as a matter of fact it's not standard behavior, and I am not aware of any compiler other than MSVC that implements it. Basically, long time ago someone at Microsoft read the word "exception" and interpreted the standard incorrectly, and now they can't remove this broken behavior for legacy reasons.
Absolutely not. Dereferencing a null pointer in C++ is undefined behavior,
which is not at all the same as throwing exceptions or raising OS exceptions. When you throw an exception, the standard specifies exactly what is going to happen, but in the case of undefined behavior all bets are off. Maybe your program will crash, maybe it'll send a nasty email to your boss. :)
On pretty much any platform with an MPU the memory around address 0 is left unmapped precisely such that such accesses generate an OS fault.
Yes, but there is no requirement for the program to be able to unwind the stack and call destructors, as it does when throwing C++ exceptions. It's makes no sense to expect a program to be able to recover from undefined behavior because at that point the state of the program is undefined, by definition.
You're taking this out of context, though; it was an aside only peripherally related to the main point.
If we're talking about the problems of C++ exception handling and when it is and it isn't appropriate to use it, it's important to be on the same page about what exception handling is. And it most definitely is NOT the "structural exception handling" behavior in MSVC. That should never be used, except if you're dealing with an old code base that relies on it. Emil
On 12/02/2016 14:27, Emil Dotchevski wrote:
It's criminal that MSVC can translate OS exceptions into C++ exceptions. :) It's not that it's broken on other OSes, other compilers don't do this because it's wrong. I do not recommend turning that MSVC option on.
It's not an option. That's just what it does.
Not true, it's an option. They call it "structural exception handling".
As I mentioned, as far as I am aware that just disables the ability to catch such exceptions (in part, by only inserting the logic for unwinding in places that it expects C++ exceptions). I don't think it stops the compiler transporting exceptions that way. I could be wrong about that though.
On pretty much any platform with an MPU the memory around address 0 is left unmapped precisely such that such accesses generate an OS fault.
Yes, but there is no requirement for the program to be able to unwind the stack and call destructors, as it does when throwing C++ exceptions. It's makes no sense to expect a program to be able to recover from undefined behavior because at that point the state of the program is undefined, by definition.
Except it's not. If a particular platform generates a bus error on access to protected memory (including null memory), it typically further guarantees how to recover from such a hardware exception (typically by adjusting the return address), and indeed all OSes do so in some fashion or another. This is not really any different from platforms like Java or .NET that throw a NullReferenceException or other AccessViolationException or whatever in this case. (Except that those languages typically make it slightly harder to stomp completely roughshod over memory.) On Windows that translates to a structured exception which is thread-specific and (if async exceptions are enabled) can be caught, logged, and recovered from in some sane fashion *if* the application can know what is sane -- but especially in state-free applications (eg. web servers) that is also well defined, usually by abandoning the operation and logging or returning an error. On POSIX that translates to a signal that is not thread-specific and can't really be dealt with in any sane way except terminating the entire process. While terminating the process is *often* the best reaction to this sort of failure, it is not exclusively so, and POSIX is dumb for not permitting the application the choice. (Admittedly Windows is also dumb for permitting the application the choice, since there are a lot of people who do completely the wrong thing. You can't really argue that either way is the best.) Again, though, this is a platform-level definition and not a language-level definition. C++ itself does not define what happens when you access memory out of bounds -- the platform does, and if you're writing portable code that's platform-agnostic then you cannot rely on the behaviour of a particular platform, obviously. But if you aren't, then you can.
On Thu, Feb 11, 2016 at 6:36 PM, Gavin Lambert
On 12/02/2016 14:27, Emil Dotchevski wrote:
It's criminal that MSVC can translate OS exceptions into C++ exceptions.
:) It's not that it's broken on other OSes, other compilers don't do this because it's wrong. I do not recommend turning that MSVC option on.
It's not an option. That's just what it does.
Not true, it's an option. They call it "structural exception handling".
As I mentioned, as far as I am aware that just disables the ability to catch such exceptions (in part, by only inserting the logic for unwinding in places that it expects C++ exceptions). I don't think it stops the compiler transporting exceptions that way. I could be wrong about that though.
The __try/__except and __try/__finally statements are C (not C++) extensions of MSVC. See https://msdn.microsoft.com/en-us/library/swezty51.aspx. Optionally the C++ compiler can generate code that treats these exceptions as C++ exceptions. Microsoft does not recommend using that option, as indicated by the first paragraph from the linked article: "Although Windows and Visual C++ support structured exception handling (SEH), we recommend that you use ISO-standard C++ exception handling because it makes code more portable and flexible." So, don't enable this behavior, it leads to too much overhead and has nothing to do with how C++ exceptions are supposed to work. Emil
On 11 Feb 2016 at 17:27, Emil Dotchevski wrote:
You can love it or hate it, but as a matter of fact it's not standard behavior, and I am not aware of any compiler other than MSVC that implements it. Basically, long time ago someone at Microsoft read the word "exception" and interpreted the standard incorrectly, and now they can't remove this broken behavior for legacy reasons.
Win32 structured exception handling has nothing to do with C++, it was how the NT kernel implementors decided to fix the extremely broken POSIX signals design. And it is a *superb* solution to the problem of handling unexpected failure at the system level, I really wish POSIX would mark signals entirely as deprecated and adopt pretty much exactly structured exception handling as the POSIX standard. LLVM, for the record, has implemented a very similar "universal" exception handling mechanism which allows exception throws to traverse language boundaries within LLVM, and my great hope is that once LLVM becomes universal on POSIX platforms someone will go ahead and replace signals with structured exception handling. Microsoft simply got to the same point much earlier with less practical experience and much worse tooling, and made some decision designs which with the benefit of hindsight look poor, but at the time a universal exception framework looked as valuable to Microsoft as it does today to LLVM. Microsoft's C++ implementation has a ton of warts, but you've got to remember it is the oldest C++ implementation in the world. They are *still* ABI compatible with the earliest 32 bit binaries on DOS. The fact it works as well as it does for modern C++ is an astounding engineering achievement. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
Gavin Lambert wrote:
The only reason that shared_ptr::operator* does not throw is that the class author decided that this is likely a hot path and the calling code has *probably* already checked for null, so it is more *efficient* to omit the check entirely (and cause undefined behavior if called in violation of that assumption).
That's not true. The class author decided that calling operator* on a NULL pointer is a logic error and therefore a correct program should never do so. This doesn't have anything to do with performance; it's a question of design.
On 12/02/2016 13:02, Peter Dimov wrote:
Gavin Lambert wrote:
The only reason that shared_ptr::operator* does not throw is that the class author decided that this is likely a hot path and the calling code has *probably* already checked for null, so it is more *efficient* to omit the check entirely (and cause undefined behavior if called in violation of that assumption).
That's not true. The class author decided that calling operator* on a NULL pointer is a logic error and therefore a correct program should never do so. This doesn't have anything to do with performance; it's a question of design.
Then why doesn't it throw a logic_error? Or call abort()? Again, an assert is a useful tool in debug mode but it disappears in release mode. The only reason to elide this check is for performance. Much as it might be considered "bad practice", there *is* software that is only ever built and tested in release mode (or at least only rarely in debug mode). For this software, the assert might as well not be there -- because it isn't.
Gavin Lambert wrote:
On 12/02/2016 13:02, Peter Dimov wrote:
That's not true. The class author decided that calling operator* on a NULL pointer is a logic error and therefore a correct program should never do so. This doesn't have anything to do with performance; it's a question of design.
Then why doesn't it throw a logic_error?
Because throwing a logic_error is an oxymoron. Logic errors should not throw.
Or call abort()?
That's basically what it does. Or a crash when asserts are disabled.
Again, an assert is a useful tool in debug mode but it disappears in release mode. The only reason to elide this check is for performance.
That's correct.
On 12/02/2016 13:50, Peter Dimov wrote:
Then why doesn't it throw a logic_error?
Because throwing a logic_error is an oxymoron. Logic errors should not throw.
And yet it was defined in the standard library for that purpose. :)
Or call abort()?
That's basically what it does. Or a crash when asserts are disabled.
Again, an assert is a useful tool in debug mode but it disappears in release mode. The only reason to elide this check is for performance.
That's correct.
Then it appears we are in violent agreement after all. :)
Gavin Lambert wrote:
Again, an assert is a useful tool in debug mode but it disappears in release mode. The only reason to elide this check is for performance.
That's correct.
Then it appears we are in violent agreement after all. :)
No, we aren't. Performance is a legitimate reason to elide the check, but it's not the reason that op* doesn't throw (by contract). Again, throwing on a logic error is an oxymoron. If the function is specified to throw under such-and-such conditions, it's not an error to call it under those conditions. All that seems to deviate a bit from the actual topic of the thread though.
On 02/11/2016 11:57 PM, Emil Dotchevski wrote:
failure, and others that do not, without giving the user a choice. Even at the language level, consider that in C++ constructors don't give you the option to return an error code, the only way for them to fail is by throwing. Why? Because that is the correct design.
True. However, there are situations where throwing exceptions is the wrong design, like continuations (e.g. future<T>::then) or asynchronous return values (e.g. Boost.Asio handlers.) For both the situations a value-or-error type, like excepted<T>, is a correct design.
On Thu, Feb 11, 2016 at 3:17 PM, Bjorn Reese
On 02/11/2016 11:57 PM, Emil Dotchevski wrote:
failure, and others that do not, without giving the user a choice. Even at
the language level, consider that in C++ constructors don't give you the option to return an error code, the only way for them to fail is by throwing. Why? Because that is the correct design.
True. However, there are situations where throwing exceptions is the wrong design, like continuations (e.g. future<T>::then) or asynchronous return values (e.g. Boost.Asio handlers.) For both the situations a value-or-error type, like excepted<T>, is a correct design.
Obviously there are such situations when throwing is the wrong design. In C++ there are three options in case of errors: 1) undefined behavior (e.g. assert) 2) report the error to the caller 3) throw Choosing the correct behavior(s) is not always trivial, but "oh, I'll let the user choose what's best for him" is not good design. Emil
On 9 Feb 2016 at 12:04, Emil Dotchevski wrote:
It feels strange to have to defend the use of exceptions for reporting errors in C++, on the boost development board of all places. There are many other advantages, for example when returning errors there is no such thing as error-neutral contexts in your program, which increases coupling. Yes, in some contexts one can't afford to use exceptions, but all general complains that exception handling causes performance or any other problems are theoretical, at best.
I've noticed a lot of people taking issue with the overhead of exceptions really mean to say they take issue with the *indeterminacy* introduced by exceptions, and even that often is really a proxy for the phrase "indirect/implicit/hidden/non-obvious use of malloc() or free()" which is the main source of unpredictable exception throws. In other words, people don't mind predictable exceptions anything like as much as potential unpredictable unknowable overheads. My current contract has my coworkers highly surprised that fixed worst case latency code can be easily written using the STL. They had assumed that games and audio development banned use of the STL and exceptions due to unpredictable execution times. They are not wrong, you just need to learn off which bits of the STL could call malloc or have worse than linear execution times and which bits never will, and only use the latter in hot code paths. That's really a training/familiarity(/maintenance) problem in the end.
With just a little extra libclang tooling (some of which I plan to write) this style idiom ought to be mathematically provable as correct in the functional programming sense, which would be cool, not least for those programming nuclear reactors etc.
Could you prove anything mathematically in the presence of side effects and pointers?
It's not my field so everything I'm about to say next is hearsay, but back during the nuclear reactors certification for QNX (which is written in C) I noticed you must always assume that functions you call behave as specified and the only goal is to prove the current function you are proving is no worse than the things it calls. From what I saw, you can't prove a program, but you can prove a program if you assume everything it calls is correct and you don't do a long list of things in C which would break the proof. They had LLVM based tooling which generated the proofs from the AST or flagged code where you were doing something not permitted, it appeared to work very well. Obviously C++ is orders of magnitude harder, but with a restrictive enough list of things you can't do I'm sure it's achievable. Whether such a program would still qualify as C++ is an open question. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On Wed, Feb 10, 2016 at 10:57 AM, Niall Douglas
On 9 Feb 2016 at 12:04, Emil Dotchevski wrote:
It feels strange to have to defend the use of exceptions for reporting errors in C++, on the boost development board of all places. There are many other advantages, for example when returning errors there is no such thing as error-neutral contexts in your program, which increases coupling. Yes, in some contexts one can't afford to use exceptions, but all general complains that exception handling causes performance or any other problems are theoretical, at best.
I've noticed a lot of people taking issue with the overhead of exceptions really mean to say they take issue with the *indeterminacy* introduced by exceptions, and even that often is really a proxy for the phrase "indirect/implicit/hidden/non-obvious use of malloc() or free()" which is the main source of unpredictable exception throws.
In other words, people don't mind predictable exceptions anything like as much as potential unpredictable unknowable overheads.
My current contract has my coworkers highly surprised that fixed worst case latency code can be easily written using the STL. They had assumed that games and audio development banned use of the STL and exceptions due to unpredictable execution times. They are not wrong, you just need to learn off which bits of the STL could call malloc or have worse than linear execution times and which bits never will, and only use the latter in hot code paths. That's really a training/familiarity(/maintenance) problem in the end.
If such tricky code targets multiple platforms, you should assume that any standard C or C++ or OS function may allocate memory. Keep in mind that the standard guarantees overall complexity, rather than the performance of a specific call. For example, while std::sort is generally O(n log n), it may allocate memory, and while the speed of that specific allocation is independent of the number of elements you're sorting, it might be O(n^2) for some other (possibly much larger) n. I'd argue that in such tricky use cases you should be more concerned about asynchronous events like memory allocations and unpredictable OS hitches than about exception handling overhead. It's true that in some specific case throwing an exception may be "too slow", but it'd be too slow as a matter of fact (because the profiler said so) not because of some general reasoning; and the solution is to not throw in that case, rather than to avoid exception handling in principle.
With just a little extra libclang tooling (some of which I plan to write) this style idiom ought to be mathematically provable as correct in the functional programming sense, which would be cool, not least for those programming nuclear reactors etc.
Could you prove anything mathematically in the presence of side effects and pointers?
It's not my field so everything I'm about to say next is hearsay, but back during the nuclear reactors certification for QNX (which is written in C) I noticed you must always assume that functions you call behave as specified and the only goal is to prove the current function you are proving is no worse than the things it calls. From what I saw, you can't prove a program, but you can prove a program if you assume everything it calls is correct and you don't do a long list of things in C which would break the proof. They had LLVM based tooling which generated the proofs from the AST or flagged code where you were doing something not permitted, it appeared to work very well.
Obviously C++ is orders of magnitude harder, but with a restrictive enough list of things you can't do I'm sure it's achievable. Whether such a program would still qualify as C++ is an open question.
I'm told that to this day some programs written in Fortran outperform equivalent C programs, and that's because in the presence of pointers and side effects it is impossible for the C optimizer to find all possible ways a piece of memory can change. So, you can reason all you want about errors, but if your program is in C, you must test; and testing the error path of the code is extremely tricky. If you don't use exceptions, you're admitting the possibility for logic errors every time you call a function that may fail. Logically, the only thing exception handling does is automatically check for failures every time you call a function. It's a way to get the compiler to automatically write if( error ) return error for you. This is a Good Thing. Emil
On 2/9/2016 3:16 AM, Niall Douglas wrote:
Something missing from the discussion so far is that expected/result/outcome MUST throw exceptions! If you try to fetch a value from a result and it contains an error, there is no alternative to throwing an exception. This fact is why I don't worry about static function initialisers and just go ahead and use error-code-via-system_error throwing constructors, ultimately you need try...catch in there one way or another.
There most certainly is an alternative! I certainly don't want this behavior. There's no way that accessing the value of a result<T> without checking for an error is not a programming error. It should be a precondition that to access the value it needs to not contain an error. It should *not* be throwing an exception because you cannot fail to satisfy the postcondition until you meet the preconditions. Undefined-behavior is the appropriate specification for accessing a value from a result that contains an error.
On 11 Feb 2016 at 1:19, Michael Marcin wrote:
Something missing from the discussion so far is that expected/result/outcome MUST throw exceptions! If you try to fetch a value from a result and it contains an error, there is no alternative to throwing an exception. This fact is why I don't worry about static function initialisers and just go ahead and use error-code-via-system_error throwing constructors, ultimately you need try...catch in there one way or another.
There most certainly is an alternative! I certainly don't want this behavior.
There's no way that accessing the value of a result<T> without checking for an error is not a programming error. It should be a precondition that to access the value it needs to not contain an error. It should *not* be throwing an exception because you cannot fail to satisfy the postcondition until you meet the preconditions.
Undefined-behavior is the appropriate specification for accessing a value from a result that contains an error.
This is easy to say, but I wouldn't be happy with simply documenting "all bets are off if you do this" because it's very easy to do accidentally. Really I want some method of trapping it at compile time. The reason Outcome currently throws an exception when trying to get() from an errored/excepted outcome is because I don't know how to get it to refuse to compile instead, and the next best alternative of refusing to link is extremely non-descriptive (specifically it tells you absolutely nothing about where in your code the unchecked get() is being performed). Rather than implement something distinctly unhelpful I've left it as is for now. However rest assured Outcome will eventually work as described with RTTI and exceptions off just as AFIO v2 will. Right now it doesn't, but it's my first todo item after the ACCU conference in April and Outcome was deliberately designed to work perfectly with RTTI and exceptions off, it just currently fails due to minor unfinished problems like the above mentioned as I felt I need a lot more reflection time on what to do to fix them properly. And between now and then, simply always check your return before get() and the compiler will elide any potential exception throw e.g. outcome<int> _foo; // contains some value int foo; if(_foo) foo=_foo.get(); foo=_foo.get_or(5); // also works If compiled with optimisation you should see no exception throwing code emitted. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On 2/11/2016 5:47 AM, Niall Douglas wrote:
outcome<int> _foo; // contains some value int foo; if(_foo) foo=_foo.get(); foo=_foo.get_or(5); // also works
I'm not convinced.
I ran a comparison of a trivial implementation of result with your test
code versus your implementation.
The results weren't pretty.
Here's my test code:
http://ideone.com/JYsSYc
Here's the results (VC2015 /O2 /Ob2)
Trivial:
int main()
{
00007FF7B7053530 sub rsp,48h
00007FF7B7053534 mov rax,qword ptr [__security_cookie
(07FF7B705D020h)]
00007FF7B705353B xor rax,rsp
00007FF7B705353E mov qword ptr [rsp+38h],rax
result<int> _foo = bar();
00007FF7B7053543 lea rcx,[_foo]
00007FF7B7053548 call bar (07FF7B7051172h)
int foo = 0;
00007FF7B705354D xor eax,eax
if ( _foo ) {
00007FF7B705354F lea rcx,[instance (07FF7B705D000h)]
00007FF7B7053556 cmp qword ptr [rsp+30h],rcx
00007FF7B705355B cmove eax,dword ptr [_foo]
foo = _foo.get();
}
return foo;
}
00007FF7B7053560 mov rcx,qword ptr [rsp+38h]
00007FF7B7053565 xor rcx,rsp
00007FF7B7053568 call __security_check_cookie (07FF7B70512A3h)
00007FF7B705356D add rsp,48h
00007FF7B7053571 ret
Boost.Outcome
int main()
{
00007FF6E25C2BB0 mov rax,rsp
00007FF6E25C2BB3 push rbp
00007FF6E25C2BB4 lea rbp,[rax-58h]
00007FF6E25C2BB8 sub rsp,150h
00007FF6E25C2BBF mov qword ptr [rbp-38h],0FFFFFFFFFFFFFFFEh
00007FF6E25C2BC7 mov qword ptr [rax+8],rbx
00007FF6E25C2BCB movaps xmmword ptr [rax-18h],xmm6
00007FF6E25C2BCF mov rax,qword ptr [__security_cookie
(07FF6E25CC010h)]
00007FF6E25C2BD6 xor rax,rsp
00007FF6E25C2BD9 mov qword ptr [rbp+30h],rax
result<int> _foo = bar();
00007FF6E25C2BDD lea rcx,[rbp-50h]
00007FF6E25C2BE1 call bar (07FF6E25C109Bh)
00007FF6E25C2BE6 nop
int foo = 0;
00007FF6E25C2BE7 xor ebx,ebx
if ( _foo ) {
00007FF6E25C2BE9 movzx ecx,byte ptr [rbp-40h]
00007FF6E25C2BED cmp cl,1
00007FF6E25C2BF0 sete al
00007FF6E25C2BF3 test al,al
00007FF6E25C2BF5 je main+267h (07FF6E25C2E17h)
foo = _foo.get();
00007FF6E25C2BFB test cl,cl
00007FF6E25C2BFD setne al
00007FF6E25C2C00 test al,al
00007FF6E25C2C02 jne main+119h (07FF6E25C2CC9h)
00007FF6E25C2C08 mov dword ptr [rsp+20h],2
00007FF6E25C2C10 call boost::outcome::v1_std_std::monad_category
(07FF6E25C10DCh)
00007FF6E25C2C15 mov qword ptr [rsp+28h],rax
00007FF6E25C2C1A movaps xmm0,xmmword ptr [rsp+20h]
00007FF6E25C2C1F movdqa xmmword ptr [rsp+20h],xmm0
00007FF6E25C2C25 lea rdx,[rbp-10h]
00007FF6E25C2C29 lea rcx,[rsp+20h]
foo = _foo.get();
00007FF6E25C2C2E call std::error_code::message (07FF6E25C1190h)
00007FF6E25C2C33 cmp qword ptr [rax+18h],10h
00007FF6E25C2C38 jb main+8Dh (07FF6E25C2C3Dh)
00007FF6E25C2C3A mov rax,qword ptr [rax]
00007FF6E25C2C3D lea rcx,[std::exception::`vftable'
(07FF6E25C9D68h)]
00007FF6E25C2C44 mov qword ptr [rsp+58h],rcx
00007FF6E25C2C49 xor ecx,ecx
00007FF6E25C2C4B mov qword ptr [rsp+60h],rcx
00007FF6E25C2C50 mov qword ptr [rsp+68h],rcx
00007FF6E25C2C55 mov qword ptr [rbp-80h],rax
00007FF6E25C2C59 mov byte ptr [rbp-78h],1
00007FF6E25C2C5D lea rdx,[rsp+60h]
00007FF6E25C2C62 lea rcx,[rbp-80h]
00007FF6E25C2C66 call qword ptr [__imp___std_exception_copy
(07FF6E25CE1C0h)]
00007FF6E25C2C6C lea rax,[std::logic_error::`vftable'
(07FF6E25C9DA0h)]
00007FF6E25C2C73 mov qword ptr [rsp+58h],rax
00007FF6E25C2C78 mov r8,qword ptr [rbp+8]
00007FF6E25C2C7C cmp r8,10h
00007FF6E25C2C80 jb main+0E2h (07FF6E25C2C92h)
00007FF6E25C2C82 inc r8
00007FF6E25C2C85 mov rdx,qword ptr [rbp-10h]
00007FF6E25C2C89 lea rcx,[rbp-10h]
00007FF6E25C2C8D call std::_Wrap_alloc ::deallocate (07FF6E25C126Ch)
00007FF6E25C2C92 mov qword ptr [rbp+8],0Fh
00007FF6E25C2C9A mov qword ptr [rbp],rbx
00007FF6E25C2C9E mov byte ptr [rbp-10h],bl
00007FF6E25C2CA1 lea
rax,[boost::outcome::v1_std_std::monad_error::`vftable' (07FF6E25C9F08h)]
00007FF6E25C2CA8 mov qword ptr [rsp+58h],rax
00007FF6E25C2CAD movaps xmm0,xmmword ptr [rsp+20h]
00007FF6E25C2CB2 movups xmmword ptr [rsp+70h],xmm0
00007FF6E25C2CB7 lea
rdx,[_TI3?AVmonad_error@v1_std_std@outcome@boost@@ (07FF6E25CBAB0h)]
00007FF6E25C2CBE lea rcx,[rsp+58h]
00007FF6E25C2CC3 call _CxxThrowException (07FF6E25C4DDEh)
00007FF6E25C2CC8 int 3
00007FF6E25C2CC9 cmp cl,2
00007FF6E25C2CCC sete al
00007FF6E25C2CCF test al,al
00007FF6E25C2CD1 jne main+137h (07FF6E25C2CE7h)
00007FF6E25C2CD3 sub cl,2
00007FF6E25C2CD6 cmp cl,1
00007FF6E25C2CD9 ja main+264h (07FF6E25C2E14h)
00007FF6E25C2CDF test al,al
00007FF6E25C2CE1 je main+264h (07FF6E25C2E14h)
00007FF6E25C2CE7 mov qword ptr [rbp-18h],0Fh
00007FF6E25C2CEF mov qword ptr [rbp-20h],rbx
00007FF6E25C2CF3 mov byte ptr [rbp-30h],bl
00007FF6E25C2CF6 xor r8d,r8d
00007FF6E25C2CF9 lea rdx,[string "" (07FF6E25C9E5Ch)]
00007FF6E25C2D00 lea rcx,[rbp-30h]
00007FF6E25C2D04 call
std::basic_string
On 11 Feb 2016 at 11:57, Michael Marcin wrote:
On 2/11/2016 5:47 AM, Niall Douglas wrote:
outcome<int> _foo; // contains some value int foo; if(_foo) foo=_foo.get(); foo=_foo.get_or(5); // also works
I'm not convinced.
I ran a comparison of a trivial implementation of result with your test code versus your implementation. The results weren't pretty.
I'll caveat everything I'm about to say with a dose of salt as from past experience my memory of what Stephan told me varies from what he actually told me. So bearing that in mind as my memory recalls faulty ... Stephan took a very conservative interpretation of error categories on the Dinkumware shipped with MSVC, and so you get a large quantity of "splosh" on MSVC because touching an error category forces emission of code. You'll see vastly tighter code on GCC especially, and pretty good on clang. In the op code counting unit tests there are something like 1000 opcodes on MSVC for six instructions on GCC. It's that bad. When I raised just how much opcode splosh MSVC generates with Stephan, he asked "is it any slower?" I benchmarked it, and the answer is not much, barely measurable, maybe only a few dozen CPU cycles. It's basically binary bloat, other than that fairly benign. Stephan has promised to look into the very strict interpretation of error categories in a future MSVC if the less strict interpretation on libc++/libstdc++ proves benign in real world usage. tl;dr; a fix for the bloat generated by MSVC when compiling Outcome is many years out, if at all. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On Wed, Feb 10, 2016 at 11:19 PM, Michael Marcin
On 2/9/2016 3:16 AM, Niall Douglas wrote:
Something missing from the discussion so far is that expected/result/outcome MUST throw exceptions! If you try to fetch a value from a result and it contains an error, there is no alternative to throwing an exception. This fact is why I don't worry about static function initialisers and just go ahead and use error-code-via-system_error throwing constructors, ultimately you need try...catch in there one way or another.
There most certainly is an alternative! I certainly don't want this behavior.
There's no way that accessing the value of a result<T> without checking for an error is not a programming error. It should be a precondition that to access the value it needs to not contain an error. It should *not* be throwing an exception because you cannot fail to satisfy the postcondition until you meet the preconditions.
Undefined-behavior is the appropriate specification for accessing a value from a result that contains an error.
It depends what's your goal. If you want to avoid logic errors in error handling code (as you should), then you don't want accessing the result without checking for error to be undefined behavior. Consider the following C code: int * p=(int *)malloc(sizeof(int)); *p=42; Of course the correct code is: int * p=(int *)malloc(sizeof(int)); if( !p ) return error; *p=42; Niall's motivation is to make code like this safer without resorting to exceptions. But the best way such code is made safe is by using exceptions. In C++ you can write: int * p=new int; *p=42; without invoking undefined behavior, because the compiler will automatically generate code that effectively does: int * p=new int; if( !p ) return error; *p=42; Emil
On 11 Feb 2016 at 15:20, Emil Dotchevski wrote:
Niall's motivation is to make code like this safer without resorting to exceptions. But the best way such code is made safe is by using exceptions.
Sorry, I have to correct you here as this is an incorrect statement, and it's my own fault for me not correcting an earlier reply to a post of mine where you got a false understanding of my position. Fixed: I *like* exceptions, a lot. Indeed I default to them, as I did in AFIO v1 for handling all unexpected outcomes. In any new general purpose C++ I write either now or in the future will exclusively use exceptions. However, some of the AFIO v1 peer reviewers wanted a hard semantic distinction between catastrophic errors and benign failures (I can see a point here). Other peer reviewers wanted no more than a few hundred cycles overhead around the raw syscall (I think this daft personally). Quite a few took issue with the use of shared_ptr to manage lifetimes (also a daft criticism), and the more insightful reviewers took particular issue with the high number of allocations and free per operation performed due to the many stages of type erasure (I very much agree). Even though AFIO v2 probably won't go in for peer review on it own, I have implemented almost all of the peer review feedback. v2 throws no exceptions, allocates and frees exactly one malloc per operation, uses no threads and therefore doesn't need shared_ptr, and yes there are even less than a few hundred cycles overhead around the syscalls on Windows (not so on POSIX, AIO is shockingly awful and you need to spend a ton of cycles to make it sane). All possible outcomes from any AFIO v2 function are as tightly specified as POSIX itself right down to every potential error emitted. Outcome is a very large part of achieving this very lightweight AFIO v2 whilst leveraging as much of the power of C++ as possible in doing so. I personally speaking think such lightweightness is overkill for the use cases as file i/o is enormously heavy, but I defer to Boost peer review judgement. You'll get what you collectively asked for. I don't personally agree with it, but if one *has* to write this sort of lightweight C++, then I think Outcome a great help in doing so. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On Thu, Feb 11, 2016 at 4:30 PM, Niall Douglas
On 11 Feb 2016 at 15:20, Emil Dotchevski wrote:
Niall's motivation is to make code like this safer without resorting to exceptions. But the best way such code is made safe is by using exceptions.
Sorry, I have to correct you here as this is an incorrect statement, and it's my own fault for me not correcting an earlier reply to a post of mine where you got a false understanding of my position.
Fixed: I *like* exceptions, a lot. Indeed I default to them, as I did in AFIO v1 for handling all unexpected outcomes. In any new general purpose C++ I write either now or in the future will exclusively use exceptions.
However, some of the AFIO v1 peer reviewers wanted a hard semantic distinction between catastrophic errors and benign failures (I can see a point here). Other peer reviewers wanted no more than a few hundred cycles overhead around the raw syscall (I think this daft personally). Quite a few took issue with the use of shared_ptr to manage lifetimes (also a daft criticism), and the more insightful reviewers took particular issue with the high number of allocations and free per operation performed due to the many stages of type erasure (I very much agree).
Even though AFIO v2 probably won't go in for peer review on it own, I have implemented almost all of the peer review feedback. v2 throws no exceptions, allocates and frees exactly one malloc per operation, uses no threads and therefore doesn't need shared_ptr, and yes there are even less than a few hundred cycles overhead around the syscalls on Windows (not so on POSIX, AIO is shockingly awful and you need to spend a ton of cycles to make it sane). All possible outcomes from any AFIO v2 function are as tightly specified as POSIX itself right down to every potential error emitted.
Outcome is a very large part of achieving this very lightweight AFIO v2 whilst leveraging as much of the power of C++ as possible in doing so. I personally speaking think such lightweightness is overkill for the use cases as file i/o is enormously heavy, but I defer to Boost peer review judgement. You'll get what you collectively asked for. I don't personally agree with it, but if one *has* to write this sort of lightweight C++, then I think Outcome a great help in doing so.
I'm not qualified to judge these design decisions, but I'm very skeptical. You as the author of AFIO are an expert in this domain, or else you wouldn't be writing this library. It's okay to defer to peer review judgement as long as it is substantiated. That said, whether or not a function throws exceptions is a design decision, nothing to do with run-time overhead. The reason, again, is that any exception handling overhead of calling a function can be easily eliminated, by inlining. It is true that this will not make the function work faster in the case when it does throw, but at that time we're usually dealing with aborting an operation, it's an exceptional condition. I understand that this may be a problem in some weird cases but that's when I'd demand strong evidence rather than conspiracy theories and toy benchmark programs. Emil
On 11 Feb 2016 at 18:00, Emil Dotchevski wrote:
Outcome is a very large part of achieving this very lightweight AFIO v2 whilst leveraging as much of the power of C++ as possible in doing so. I personally speaking think such lightweightness is overkill for the use cases as file i/o is enormously heavy, but I defer to Boost peer review judgement. You'll get what you collectively asked for. I don't personally agree with it, but if one *has* to write this sort of lightweight C++, then I think Outcome a great help in doing so.
I'm not qualified to judge these design decisions, but I'm very skeptical. You as the author of AFIO are an expert in this domain, or else you wouldn't be writing this library. It's okay to defer to peer review judgement as long as it is substantiated.
As I said during the peer review, people who think they want async file i/o are generally disappointed. What people really want is *control* which normally means low worst case latencies, but in the filing system synchronous i/o is almost always lower worst case latency than async so firing 10,000 threads at a file i/o problem might actually be sensible. AFIO v1 delivered oodles of control, but packed very substantial work into file open and close (both of which are horribly slow anyway so it made sense) in order to keep everything else fixed overhead. Now, I can totally see that if you want to open and close a lot of files, AFIO v1 is going to be not great for you, and v2 is going to be a lot better. People got upset with the find regex in files performance for example, and that was understandable: AFIO v1 is a poor choice for that use case. In this sense the v2 design is much more flexible and reusable, but the end user programmer will have to work far harder than with v1 and have a far deeper understanding of filing systems to get reasonable results. The single biggest reason I won't be presenting v2 for peer review is the enormous documentation which would be required, it's at least six months of work and probably more, and I just can't afford such an enormous loss of income. Still, what I learned from the Boost peer review is people here hate to be hand held, and I get that. And let me be very clear, the AFIO v2 design has a lot of merit, I don't disagree with any of it, I just think it excessive for its likely use cases - it is going to be several times lighter weight than ASIO which makes zero sense for file i/o vs socket i/o.
That said, whether or not a function throws exceptions is a design decision, nothing to do with run-time overhead. The reason, again, is that any exception handling overhead of calling a function can be easily eliminated, by inlining.
If the compiler can see all possible throw sites, and it is not called MSVC, then yes. I don't think people's issue is about what happens in practice under optimisation. They want *guarantees*. They want to see noexcept on every function because it's a *guarantee* that no matter what the programmer does, no unbounded execution times will occur. This is why games, audio and HFT shops ban the STL and disable RTTI and exceptions. It's about assurance, not reality in practice.
It is true that this will not make the function work faster in the case when it does throw, but at that time we're usually dealing with aborting an operation, it's an exceptional condition. I understand that this may be a problem in some weird cases but that's when I'd demand strong evidence rather than conspiracy theories and toy benchmark programs.
A lot of this debate isn't about empirical reality, if C++ programmers were managed by reality everyone would be using the STL with exceptions and RTTI turned on for years now. As I mentioned, it's about *assurance*: given a pool of average programmers, how can you (technical leads and management) be assured they will write on average low worst case latency C++? This is why I expect exceptions disabled C++ to remain around for a long time to come, and why AFIO v2 and Outcome will eventually support exceptions disabled. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On Fri, Feb 12, 2016 at 2:22 AM, Niall Douglas
That said, whether or not a function throws exceptions is a design decision, nothing to do with run-time overhead. The reason, again, is
On 11 Feb 2016 at 18:00, Emil Dotchevski wrote: that
any exception handling overhead of calling a function can be easily eliminated, by inlining.
If the compiler can see all possible throw sites, and it is not called MSVC, then yes.
If you inline a function, the compiler can see when it throws and when it doesn't. Every compiler, even MSVC, will remove the exception handling overhead, as long as you inline, though of course you will incur overhead if an exception is actually thrown.
I don't think people's issue is about what happens in practice under optimisation. They want *guarantees*. They want to see noexcept on every function because it's a *guarantee* that no matter what the programmer does, no unbounded execution times will occur.
I know what people want, I've had my share of arguments with people who want noexcept no matter what.
This is why games, audio and HFT shops ban the STL and disable RTTI and exceptions. It's about assurance, not reality in practice.
I know that too. It's a free country, anyone can disable anything they want.
It is true that this will not make the function work faster in the case when it does throw, but at that time we're usually dealing with aborting an operation, it's an exceptional condition. I understand that this may be a problem in some weird cases but that's when I'd demand strong evidence rather than conspiracy theories and toy benchmark programs.
A lot of this debate isn't about empirical reality, if C++ programmers were managed by reality everyone would be using the STL with exceptions and RTTI turned on for years now.
Yep. Recently we shipped a game on Nintendo 3DS, 60 fps stereo, exception handling and RTTI enabled, STL used throughout. Peter helped me get boost::shared_ptr work on that platform, evidently we were the first developer to use Boost on that platform. :)
As I mentioned, it's about *assurance*: given a pool of average programmers, how can you (technical leads and management) be assured they will write on average low worst case latency C++? This is why I expect exceptions disabled C++ to remain around for a long time to come, and why AFIO v2 and Outcome will eventually support exceptions disabled.
Such arguments do push my buttons so I get involved, but I have very low tolerance for how much I'd compromise my library designs to accommodate incompetent technical leads. If you think that you've made your library harder to use without empirical improvements in performance, isn't that a problem? Emil
On 12 Feb 2016 at 13:28, Emil Dotchevski wrote:
Yep. Recently we shipped a game on Nintendo 3DS, 60 fps stereo, exception handling and RTTI enabled, STL used throughout. Peter helped me get boost::shared_ptr work on that platform, evidently we were the first developer to use Boost on that platform. :)
Impressive. That platform has an unhelpfully limited dev environment.
As I mentioned, it's about *assurance*: given a pool of average programmers, how can you (technical leads and management) be assured they will write on average low worst case latency C++? This is why I expect exceptions disabled C++ to remain around for a long time to come, and why AFIO v2 and Outcome will eventually support exceptions disabled.
Such arguments do push my buttons so I get involved, but I have very low tolerance for how much I'd compromise my library designs to accommodate incompetent technical leads. If you think that you've made your library harder to use without empirical improvements in performance, isn't that a problem?
Well, I look at this way: give the customer what they want. Especially if you want to pass a peer review. As you saw during the AFIO peer review, rationally arguing with people has no effect. They want what they want, and that's that, so if you want them to use your library, give them what they want. Niall -- ned Productions Limited Consulting http://www.nedproductions.biz/ http://ie.linkedin.com/in/nialldouglas/
On 2/11/2016 5:20 PM, Emil Dotchevski wrote:
On Wed, Feb 10, 2016 at 11:19 PM, Michael Marcin
wrote: On 2/9/2016 3:16 AM, Niall Douglas wrote:
Something missing from the discussion so far is that expected/result/outcome MUST throw exceptions! If you try to fetch a value from a result and it contains an error, there is no alternative to throwing an exception. This fact is why I don't worry about static function initialisers and just go ahead and use error-code-via-system_error throwing constructors, ultimately you need try...catch in there one way or another.
There most certainly is an alternative! I certainly don't want this behavior.
There's no way that accessing the value of a result<T> without checking for an error is not a programming error. It should be a precondition that to access the value it needs to not contain an error. It should *not* be throwing an exception because you cannot fail to satisfy the postcondition until you meet the preconditions.
Undefined-behavior is the appropriate specification for accessing a value from a result that contains an error.
It depends what's your goal. If you want to avoid logic errors in error handling code (as you should), then you don't want accessing the result without checking for error to be undefined behavior.
Consider the following C code:
int * p=(int *)malloc(sizeof(int)); *p=42;
Of course the correct code is:
int * p=(int *)malloc(sizeof(int)); if( !p ) return error; *p=42;
Niall's motivation is to make code like this safer without resorting to exceptions. But the best way such code is made safe is by using exceptions. In C++ you can write:
int * p=new int; *p=42;
without invoking undefined behavior, because the compiler will automatically generate code that effectively does:
int * p=new int; if( !p ) return error; *p=42;
I disagree with your premise that throwing exceptions avoids logic errors or makes code "safe". To borrow from Andrzej's blog: -- One thing that I want to point out is that just checking every possible “invalid function input” and throwing an exception on it does not necessarily make the program more correct or safe. It only prevents crashes, but not bugs. Bugs, in turn, are a sort of UB on the higher level of abstraction. -- You likely routinely use "unsafe" code that can lead to UB in the presence of programming errors. Do you use std::vector operator[] in your code? Do you use std::vector iterators? Maybe you always use std::vector at() to access elements. If so then I understand your reluctance to deal specify UB. Writing high performance code requires you to *think*. In C++ we embrace UB when it is "The Right Thing". You can certainly add a debug layer to result that can help diagnose problems (mark if the result was tested for containing a value or error is some way and assert if it was accessed without such a test). Much as Microsoft's STL debug iterators are a helpful tool. But I don't want to pay for them in retail builds. There's no way I'm going to throw an exception because a raytrace hit an intersection limit when I need to maintain a consistent 90hz stereo rendering or risk making my users literally ill. Still it's an error and needs to be handled gracefully. If I can't the interface and implementation I want and need out of a standard or pseudo standard library then I'll have to roll my own but it's a shame how often I have to do that. It's a large part of why STL and Boost have terrible reputation in my industry and are outright banned in many companies.
On Thu, Feb 11, 2016 at 9:02 PM, Michael Marcin
You likely routinely use "unsafe" code that can lead to UB in the presence of programming errors.
Do you use std::vector operator[] in your code? Do you use std::vector iterators?
Maybe you always use std::vector at() to access elements. If so then I understand your reluctance to deal specify UB.
This is not about safe or unsafe code, it's about preventing logic errors. I'm not arguing that vector::op[] should throw. Actually I'd say that it's a bug to call vector::at. :) Writing high performance code requires you to *think*.
In C++ we embrace UB when it is "The Right Thing".
I embrace it where it is appropriate, yes.
You can certainly add a debug layer to result that can help diagnose problems (mark if the result was tested for containing a value or error is some way and assert if it was accessed without such a test). Much as Microsoft's STL debug iterators are a helpful tool.
But I don't want to pay for them in retail builds.
Good, I don't, either.
There's no way I'm going to throw an exception because a raytrace hit an intersection limit when I need to maintain a consistent 90hz stereo rendering or risk making my users literally ill. Still it's an error and needs to be handled gracefully.
And I'm arguing that whether you throw or not should rarely be a question of performance. If hitting the intersection limit in a raytracing program leads to an invalid frame, you should report that error by throwing an exception. This has zero performance cost if functions are being inlined, which they are at the lowest levels of a raytracer. Generally, if you can afford the "if" statement to detect the error, you can afford to throw.
If I can't the interface and implementation I want and need out of a standard or pseudo standard library then I'll have to roll my own but it's a shame how often I have to do that. It's a large part of why STL and Boost have terrible reputation in my industry and are outright banned in many companies.
In ~20 years in the video game industry I have never needed to implement my own STL, and modern STL implementations are a lot better than in the old days. On the other hand years ago I did have to refactor a company's home-grown STL replacement to fold templates to reduce bloat, since otherwise the executable was over 20 megs on a platform with 32 megs of RAM. Ironically, the STL on that platform was folding the templates automatically. Too bad they had it banned. Emil
On 2/12/2016 1:39 AM, Emil Dotchevski wrote:
On Thu, Feb 11, 2016 at 9:02 PM, Michael Marcin
wrote: But I don't want to pay for them in retail builds.
Good, I don't, either.
Sorry, I don't follow at all then. If .get() isn't specified as UB when a value is not ready how are you going to avoid the overhead? Specifically .get() must do *at least* an if statement before returning the value. Are you relying on the compiler to optimize away the check and throw as unreachable given an earlier check for an error?
And I'm arguing that whether you throw or not should rarely be a question of performance. If hitting the intersection limit in a raytracing program leads to an invalid frame, you should report that error by throwing an exception. This has zero performance cost if functions are being inlined, which they are at the lowest levels of a raytracer. Generally, if you can afford the "if" statement to detect the error, you can afford to throw.
Sorry I don't follow. Are you suggesting that the raytracer should be inlined into every call site? What I think you're saying is that where I want to write code that looks like: result<int> bar() { return std::make_error_code( std::errc::bad_address ); } int main() { int foo; auto _foo = bar(); if ( _foo ) { foo = _foo.get(); } else { foo = 0; } return foo; } I should instead write code that looks like: int bar() { throw std::system_error( std::make_error_code( std::errc::bad_address ) ); } int main() { int foo; try { foo = bar(); } catch ( std::system_error& e ) { foo = 0; } return foo; } And I should expect equal performance?
In ~20 years in the video game industry I have never needed to implement my own STL, and modern STL implementations are a lot better than in the old days. On the other hand years ago I did have to refactor a company's home-grown STL replacement to fold templates to reduce bloat, since otherwise the executable was over 20 megs on a platform with 32 megs of RAM. Ironically, the STL on that platform was folding the templates automatically. Too bad they had it banned.
Your experience differs from my own. EASTL just (officially) released to much fanfare. https://github.com/electronicarts/EASTL It's very easy to find performance problems in even current shipping standard libraries. 2 examples: See what happens when you insert a duplicate key into a std::map VS2015. Or look towards the 17x perf increase in iostreams float serialization coming in VS2015 update 2.
On Fri, Feb 12, 2016 at 1:25 AM, Michael Marcin
On 2/12/2016 1:39 AM, Emil Dotchevski wrote:
On Thu, Feb 11, 2016 at 9:02 PM, Michael Marcin
wrote: But I don't want to pay for them in retail builds.
Good, I don't, either.
Sorry, I don't follow at all then.
If .get() isn't specified as UB when a value is not ready how are you going to avoid the overhead?
Specifically .get() must do *at least* an if statement before returning the value.
Calling shared_ptr::get() is always well defined. It contains no ifs.
And I'm arguing that whether you throw or not should rarely be a question
of performance. If hitting the intersection limit in a raytracing program leads to an invalid frame, you should report that error by throwing an exception. This has zero performance cost if functions are being inlined, which they are at the lowest levels of a raytracer. Generally, if you can afford the "if" statement to detect the error, you can afford to throw.
Sorry I don't follow. Are you suggesting that the raytracer should be inlined into every call site?
I avoid using inline except if the profiler tells me it's needed. If it is needed, then I inline regardless of whether the function emits exceptions or not.
What I think you're saying is that where I want to write code that looks like:
result<int> bar() { return std::make_error_code( std::errc::bad_address ); }
int main() { int foo;
auto _foo = bar(); if ( _foo ) { foo = _foo.get(); } else { foo = 0; }
return foo; }
I should instead write code that looks like:
int bar() { throw std::system_error( std::make_error_code( std::errc::bad_address ) ); }
int main() { int foo;
try { foo = bar(); } catch ( std::system_error& e ) { foo = 0; }
return foo; }
And I should expect equal performance?
Obviously not, because you're always throwing. Throw may incur overhead, certainly does on Windows. What I'm saying is that when you don't actually throw, exception handling overhead occurs exactly where function call overhead occurs, and can be eliminated completely by inlining. In ~20 years in the video game industry I have never needed to implement my
own STL, and modern STL implementations are a lot better than in the old days. On the other hand years ago I did have to refactor a company's home-grown STL replacement to fold templates to reduce bloat, since otherwise the executable was over 20 megs on a platform with 32 megs of RAM. Ironically, the STL on that platform was folding the templates automatically. Too bad they had it banned.
Your experience differs from my own.
EASTL just (officially) released to much fanfare. https://github.com/electronicarts/EASTL
It's very easy to find performance problems in even current shipping standard libraries. 2 examples: See what happens when you insert a duplicate key into a std::map VS2015. Or look towards the 17x perf increase in iostreams float serialization coming in VS2015 update 2.
Yes I'm familiar with EASTL. I generally don't care how slow STL is, except when something shows up on my radar (profiler), in which case usually the reason is user error. Regardless, consider that if a particular use of std::map is too slow, you don't need a faster std::map, you need a faster implementation only for the operations needed in this particular use of std::map. Such implementation can be a lot faster than STL or EASTL because it isn't generic. Emil
On 2/12/2016 2:48 PM, Emil Dotchevski wrote:
On Fri, Feb 12, 2016 at 1:25 AM, Michael Marcin
wrote: If .get() isn't specified as UB when a value is not ready how are you going to avoid the overhead?
Specifically .get() must do *at least* an if statement before returning the value.
Calling shared_ptr::get() is always well defined. It contains no ifs.
Okay. I don't see how that's relevant at all.
Sorry I don't follow. Are you suggesting that the raytracer should be inlined into every call site?
I avoid using inline except if the profiler tells me it's needed. If it is needed, then I inline regardless of whether the function emits exceptions or not.
Let me be more explicit. You certainly wouldn't inline a raytracer (a heavy operation) at every call site, you would just horribly bloat your executable for no benefit and likely achieve a degradation of performance. However the premise is that the code can have errors in cases which are not unexpected (aka exceptional), which are not the result of programming errors (logic errors), which occur because of limits specifically put in place to constrain time and memory consumption of the operation (performance is important even in the error control flow). Note also that this is just an example, there are many other similar scenarios that could be explored instead.
What I think you're saying is that where I want to write code that looks like:
result<int> bar() { return std::make_error_code( std::errc::bad_address ); }
int main() { int foo;
auto _foo = bar(); if ( _foo ) { foo = _foo.get(); } else { foo = 0; }
return foo; }
I should instead write code that looks like:
int bar() { throw std::system_error( std::make_error_code( std::errc::bad_address ) ); }
int main() { int foo;
try { foo = bar(); } catch ( std::system_error& e ) { foo = 0; }
return foo; }
And I should expect equal performance?
Obviously not, because you're always throwing. Throw may incur overhead, certainly does on Windows. What I'm saying is that when you don't actually throw, exception handling overhead occurs exactly where function call overhead occurs, and can be eliminated completely by inlining.
I thought it was obvious that that code is trying to illustrate a difference in interface. Obviously bar() is meant to do work that returns an int but sometimes can't and results in an error. If you're going to choose exceptions to report your errors you're not going to be able to avoid actually throwing because errors *will* occur (as that is the whole premise of this thread).
On Fri, Feb 12, 2016 at 9:02 PM, Michael Marcin
Sorry I don't follow.
Are you suggesting that the raytracer should be inlined into every call site?
I avoid using inline except if the profiler tells me it's needed. If it is needed, then I inline regardless of whether the function emits exceptions or not.
Let me be more explicit.
You certainly wouldn't inline a raytracer (a heavy operation) at every call site, you would just horribly bloat your executable for no benefit and likely achieve a degradation of performance.
Yes, so don't inline the whole thing, do pay the exception handling overhead at the point when you call this high level function. The overhead at this call point will be negligible, because as you say it's a heavy operation. As you call deeper into the raytracer you will get to levels where the exception handling and function call overhead will be significant, and you'll inline those functions.
However the premise is that the code can have errors in cases which are not unexpected (aka exceptional), which are not the result of programming errors (logic errors), which occur because of limits specifically put in place to constrain time and memory consumption of the operation (performance is important even in the error control flow).
Yes.
What I think you're saying is that where I want to write code that looks
like:
result<int> bar() { return std::make_error_code( std::errc::bad_address ); }
int main() { int foo;
auto _foo = bar(); if ( _foo ) { foo = _foo.get(); } else { foo = 0; }
return foo; }
I should instead write code that looks like:
int bar() { throw std::system_error( std::make_error_code( std::errc::bad_address ) ); }
int main() { int foo;
try { foo = bar(); } catch ( std::system_error& e ) { foo = 0; }
return foo; }
And I should expect equal performance?
Obviously not, because you're always throwing. Throw may incur overhead, certainly does on Windows. What I'm saying is that when you don't actually throw, exception handling overhead occurs exactly where function call overhead occurs, and can be eliminated completely by inlining.
I thought it was obvious that that code is trying to illustrate a difference in interface. Obviously bar() is meant to do work that returns an int but sometimes can't and results in an error.
If you're going to choose exceptions to report your errors you're not going to be able to avoid actually throwing because errors *will* occur (as that is the whole premise of this thread).
Let's say in your example, the function throws 50% of the times you call it. Assuming this isn't an anomaly, it means that you're not really throwing to indicate a failure, but to report a result. Obviously that's not a good design. If your function throws to indicate a failure, then the error path will have some overhead, but the normal execution will have zero overhead, as long as the function is inlined. Emil
On 2/12/2016 11:36 PM, Emil Dotchevski wrote:
Let's say in your example, the function throws 50% of the times you call it. Assuming this isn't an anomaly, it means that you're not really throwing to indicate a failure, but to report a result. Obviously that's not a good design.
And this is exactly the sort of situation I would want to use result<T> in instead of throwing an exception. I want discriminated union type that is optimized in interface and implementation specifically for transporting a T or an error code. And, if in the design a choice must be made, favors T slightly.
On Fri, Feb 12, 2016 at 9:53 PM, Michael Marcin
On 2/12/2016 11:36 PM, Emil Dotchevski wrote:
Let's say in your example, the function throws 50% of the times you call it. Assuming this isn't an anomaly, it means that you're not really throwing to indicate a failure, but to report a result. Obviously that's not a good design.
And this is exactly the sort of situation I would want to use result<T> in instead of throwing an exception.
I want discriminated union type that is optimized in interface and implementation specifically for transporting a T or an error code. And, if in the design a choice must be made, favors T slightly.
If your point is that in case a function "fails" 50% of the times it's called, one should not be throwing an exception to discriminate between success and failure, I agree; OTOH in this case the "error code" vocabulary doesn't seem appropriate. Emil
On 2/13/2016 12:08 AM, Emil Dotchevski wrote:
If your point is that in case a function "fails" 50% of the times it's called, one should not be throwing an exception to discriminate between success and failure, I agree; OTOH in this case the "error code" vocabulary doesn't seem appropriate.
If instead I told you I had a function that is called 100k times per frame and that function returns an error code 1% of the time, would it make more sense? It returns a result<T> that holds a T 99% of the time. I don't think you would advocate throwing 1000 exceptions a frame.
On Fri, Feb 12, 2016 at 11:50 PM, Michael Marcin
On 2/13/2016 12:08 AM, Emil Dotchevski wrote:
If your point is that in case a function "fails" 50% of the times it's called, one should not be throwing an exception to discriminate between success and failure, I agree; OTOH in this case the "error code" vocabulary doesn't seem appropriate.
If instead I told you I had a function that is called 100k times per frame and that function returns an error code 1% of the time, would it make more sense? It returns a result<T> that holds a T 99% of the time.
I don't think you would advocate throwing 1000 exceptions a frame.
I don't normally base my decision to throw or to not throw on performance considerations, and it's not a matter of statistical analysis. Let's say you have a function that returns objects, and that the objects can be one of two colors, red and white. The function probably shouldn't use exceptions to communicate red color even if in a given sample 99% of the objects are white. On the other hand, if attempting to use a red object is always an error, then red should be communicated by throwing, even if most of the objects are red, so that the caller can assume he will get a good object, or the function won't return. Emil
On 6.2.2016. 12:53, Vicente J. Botet Escriba wrote:
Hi,
in case this could help. There were two reports on this subject. "Handling Disappointment in C++" by L. Crowl [1] and the other in the draft state "Survey of Error Handling" by N. Bolas [2].
Thanks Vicente ;) -- "What Huxley teaches is that in the age of advanced technology, spiritual devastation is more likely to come from an enemy with a smiling face than from one whose countenance exudes suspicion and hate." Neil Postman
participants (9)
-
Bjorn Reese
-
Domagoj Saric
-
Emil Dotchevski
-
Gavin Lambert
-
Hartmut Kaiser
-
Michael Marcin
-
Niall Douglas
-
Peter Dimov
-
Vicente J. Botet Escriba