I want my functions to return either an indication of success or an object that describes the nature of the failure. I'd normally use exceptions for this but I've been told not to use them for common code paths and this set of functions can be expected to fail fairly often for various reasons.
My thought is to use C++17's std::optional
as I don't have to return a full error object when it's not needed. So with optional, if the function doesn't succeed it returns the error object, otherwise the optional is empty. The issue with that is it reverses the expectation of a returned value (i.e. true
usually indicates success not failure).
I could get people to use an is_success
function, which would be used like this, assuming Error
is my error class:
auto result = do_stuff();
if (!is_success(result)) {
Error err = *result;
// ...
}
Or would a result class be more robust?
class MaybeError {
std::optional<Error> _error;
public:
MaybeError(const Error& error) : _error(error) {}
constexpr MaybeError() : _error({}) {}
explicit operator bool const() {
return !(_error.operator bool());
}
constexpr bool has_error() const {
return !(_error.has_value());
}
constexpr Error& error() & { return _error.value(); }
constexpr const Error & error() const & { return _error.value(); }
constexpr Error&& error() && { return std::move(_error.value()); }
constexpr const Error&& error() const && { return std::move(_error.value()); }
constexpr const Error* operator->() const { return _error.operator->(); }
constexpr Error* operator->() { return _error.operator->(); }
constexpr const Error& operator*() const& { return _error.operator*(); }
constexpr Error& operator*() & { return _error.operator*(); }
constexpr const Error&& operator*() const&& { return std::move(_error.operator*()); }
constexpr Error&& operator*() && { return std::move(_error.operator*()); }
};
Which can be used similarly to the first example:
auto result = do_stuff();
if (!result) {
Error err = *result;
// ...
}
What is the best option? Am I going about this the right way?
Edit To be clear, the do_stuff
function that's being called doesn't return an object if it succeeds. If it always succeeded without error it'd just be void do_stuff()
.
Edit 2 In the comments Christian Hackl suggests a less over-engineered solution using a simple struct.
struct MaybeError {
std::optional<Error> error;
};
This would be both simpler and address my concern that people would expect functions to return true if successful by making it explicit that it is the error condition that is being tested for. E.g.:
auto result = do_stuff();
if (result.error) {
Error e = *t.error;
// ...
}
I played around a bit with boost::expected that was suggested by Nicol Bolas. It seems to be really nice for this use case:
#include <iostream>
#include <system_error>
#include <boost/expected/expected.hpp>
using ExpectedVoid = boost::expected< void, std::error_code >;
using ExpectedInt = boost::expected< int, std::error_code >;
ExpectedVoid do_stuff( bool wantSuccess ) {
if( wantSuccess )
return {};
return boost::make_unexpected( std::make_error_code( std::errc::operation_canceled ) );
}
ExpectedInt do_more_stuff( bool wantSuccess ) {
if( wantSuccess )
return 42;
return boost::make_unexpected( std::make_error_code( std::errc::operation_canceled ) );
}
int main()
{
for( bool wantSuccess : { false, true } )
{
if( auto res = do_stuff( wantSuccess ) )
std::cout << "do_stuff successful!\n";
else
std::cout << "do_stuff error: " << res.error() << "\n";
}
std::cout << "\n";
for( bool wantSuccess : { false, true } )
{
if( auto res = do_more_stuff( wantSuccess ) )
std::cout << "do_more_stuff successful! Result: " << *res << "\n";
else
std::cout << "do_more_stuff error: " << res.error() << "\n";
}
return 0;
}
Output:
do_stuff error: generic:105
do_stuff successful!
do_more_stuff error: generic:105
do_more_stuff successful! Result: 42
You can download the source from the link at the beginning and just throw the files from the "include" directory of the source into your boost include directory ("boost" sub folder).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With