Open utterances-bot opened 9 months ago
It is very inconvienient if a function returning std::expected is not marked as noexcept. Because that means - you have implemented two error handling strategies in that functions - it either returns and error std::expected or returns as error by throwing exception.
Sy Brand developed the reference implementation of expected
. Sy's version supports C++11 or newer; it is available at https://github.com/TartanLlama/expected. It supports the monadic extensions, too.
Hmm... std:: expected is worthy effort, but without the language support offered by Rust it is just one of many error handling strategies. The power of Rust's Result<...> type is that it is the ubiquitous mechanism and that Rust demands all enumerations are considered.
Yes, the following are nitpicks, but I thot you would want to know:
1) your first two examples differ in a non-helpful way: record.isValid()
vs record.valid
2) second example prints record.value().valid
, but I think you mean record.value()
Thanks @dtowell. I improved the example! value().valid
is ok, as there;'s no operator <<
for the DatabaseRecord
structure.
good point @PiotrNycz ! so you're saying that all functions that return std::expected
should be noexcept
? But what if we use something that might throw - like std::string - and allocation fails... should we then use try/catch
inside the function to catch it and don't leak outside?
IMO exceptions should be for the unexpected and std::exception should be for the expected. There are many circumstances where an error can be expected. The conversion of a string to a number is one. If the conversion fails, should an exception be thrown? IMO if it's from user input then probably not and in that case std::exception is the way to go. Does that mean that functions that return std::exception should always be noexcept? IMO no. The two can be used together. Return std::exception for valid and expected issues and exceptions for unexpected issues - and especially those from which can't ( be easily) recovered.
"I don’t think we can/should use it as data members"
Hmm. Isn't std::exception basically just a 'pair of optional' with different ways of accessing the first and second elements? and can't it be treated as just that in its usage?
IMO exceptions should be for the unexpected and
std::exception
should be for the expected.
A function should never return expected results via any kind of exception.
When you want to throw an exception, it could be:
std::runtime_error
std::runtime_error
, such as std::system_error
. See cppreference for a list of these.std::runtime_error
std::exception
std::exception
(generally not recommended)Does that mean that functions that return
std::exception
should always benoexcept
?
A function that is capable of returning any exception should never be marked noexcept
; otherwise, the program will terminate immediately if an exception occurs (unless you play with the handler for this).
But what if we use something that might throw - like
std::string
- and allocation fails... should we then usetry/catch
inside the function to catch it and don't leak outside?
Yes, you can wrap the std::string
instantiation within try..catch
to handle the exception and ensure it does not escape the function you've marked noexcept
. This can make it challenging to write functions that are both noexcept
and well-designed. You have to know what needs to be wrapped in try..catch
and how to handle every possible caught exception. You have to do this in any destructor that calls a function that could throw, since destructors are implicitly noexcept
by default. Sometimes in a destructor I simply 'swallow' the exception and don't do anything with it, but that is probably bad practice.
Bartłomiej, please write a blog article to dive into the craft of writing proper noexcept
functions. Handling or avoiding heap allocation failures would be one kind of exception to deal with; perhaps this would be a good starting point.
I've considered creating a memory pool allocated from the heap early on (which could throw an exception), then using custom allocators to allocate memory for std::string
and other objects using that pre-allocated pool. These subsequent allocations would be guaranteed not to throw, because the underlying heap memory has already been allocated from the system and reserved. Doing this would have several benefits:
noexcept
Custom allocators can also be used to avoid heap fragmentation, and can allow freeing multiple resources at once from a given (arena) pool. These benefits may justify the effort and added complexity of implementing a custom memory pool and allocator(s).
But I've never been able to get my head around all of the requirements needed to do this. Your article PMR Hacking touches some of this, but does not cover the idea of using a memory pool to avoid subsequent allocation exceptions.
I still have lots of fundamental questions:
thread_local
variables when using a memory pool?noexcept
-safe functions?Trying to understand this by reading StackOverflow posts is not easy.
While I've yet to experiment myself with std::expected, I don't see why functions returning it should be noexcept.
It's generally ill-advised to declare functions as
noexcept
, other than move constructors and similar constructs (and also destructors which are implicitlynoexcept
). Read up on the Lakos Rule for more on that.
Thank you, @cppal, for bringing up the Lakos Rule in this thread. I took your advice and read a few sections from The Lakos Rule - Narrow Contracts and noexcept are Inherently Incompatible. I now have a much better sense of both noexcept
and the Lakos Rule -- and why the former should be applied in limited circumstances in accordance with the latter. Thanks also to @jslakos for formulating the Lakos Rule and for writing this document.
@fenbf I am still interested in learning more about memory pools an allocators as a way to (1) reduce the number of heap allocations in my applications, (2) reduce the amount of error handling code needed to test for memory allocation failures, and (3) allow numerous objects to be deallocated in bulk -- irrespective of whether such changes would impact a function's suitability to be marked noexcept
.
I don't follow. std::expected::value() throws a bad_expected_access exception which contains a copy of the unexpected error object. In which cases do you think that information cannot be examined?
I don't follow. std::expected::value() throws a bad_expected_access exception which contains a copy of the unexpected error object. In which cases do you think that information cannot be examined?
My error. I'll remove or revise my previous comment re: std::expected::value()
Using std::expected from C++23 - C++ Stories
In this article, we’ll go through a new vocabulary type introduced in C++23. std::expected is a type specifically designed to return results from a function, along with the extra error information. Motivation Imagine you’re expecting a certain result from a function, but oops… things don’t always go as planned.
https://www.cppstories.com/2024/expected-cpp23/