Reputation: 413
Is there an alternative to std::optional
where I don't have to pass the result
as a parameter to the function. I want the function to not modify its arguments (to be more pure/immutable).
TL;DR
The problem with std::optional
seems to be that we lose information about errors. The function returns a value or something empty, so you cannot tell what went wrong.
using std::optional
std::optional<std::string> doSomething() {
std::string value;
int rc = callApi(value);
if (rc == 0) {
//do some processing on value
return value;
}
return std::nullopt;
}
//calling the function seems much more pure/cleaner than when passing a result parameter.
without std::optional
int doSomething(std::string& result) {
std::string value;
int rc = callApi(value);
if (rc == 0) { //if no error
//do some processing on value and set result = value
result = value;
}
return rc;
}
Upvotes: 2
Views: 1869
Reputation: 131666
It seems like what you're looking for is std::expected
- which is in the C++ standard library as of C++23.
But - what is it?
Well... in a nutshell: When you want to return either some value, or some kind of failure/error descriptor, you just template those, say into T, E, and those are the template parameters: std::expected<T, E>
. And since T and E are disjoint types, you know which of them you got back from the function.
Here's your function, adapted for an error type your API seems to have:
namespace my_api {
using error_t = int;
enum : error_t { success = 0, invalid_input = 1, /* etc. */ };
} // namespace my_api
std::expected<std::string, my_api::error_t> doSomething() {
std::string value;
my_api::error_t rc = callApi(value);
if (rc != success) { return rc; }
//do some processing on value
return value;
}
Of course the my_api
namespace is not part of my suggestion, it's merely an illustration, since you did not indicate how you would like to communicate errors. Actually, you might even add:
namespace my_api {
template <typename T>
using expected = std::expected<T, error_t>;
} // namespace my_api
and then your function signature becomes:
my_api::expected<std::string> doSomething();
See also:
std::expected
proposal, P0323 revision 10.Upvotes: 5
Reputation: 3640
std::optional
is not intended to return an error. It is a tool for a very simple concept of "having a value or not". From cppreference:
Any instance of optional at any given point in time either contains a value or does not contain a value.
If you are writing a C++ function and an error is not a part of your business logic (i.e. you are not going to proceed when you receive an error instead of a usable result), just throw an exception. std::runtime_error
will suit you fine. Or you can use std::error_code
.
Upvotes: 1
Reputation: 198
I'll present three options, though I'm sure somebody will come up with a more clever approach. In short: you could throw an error, you could return an instance of a custom struct or class, or you could use a std::variant.
Option 1: Throw an Error
Here, you can throw the error value (i.e. the return code from callApi
) on failure, then wrap any calls to doSomething()
in a try ... catch
block.
std::string doSomething(){
std::string value;
int rc = callApi(value);
if(rc) throw rc;
//do some processing on value
return value;
}
Option 2: Return a Struct
Here, you create a struct that holds both a std::string
and a return code; this allows the encoding of both error information and a successfully-returned string.
struct ReturnString {
std::string value;
int rc;
}
ReturnString doSomething(){
ReturnString return_value;
std::string & value = return_value.value;
int & rc = return_value.rc;
rc = callApi(value);
if(rc) return return_value;
//do some processing on value
return return_value;
}
Then, calling code can check the value of rc
, process the error appropriately if nonzero, or use the string if 0.
You could even get fancy and use a generic struct, with the type of value being a template parameter. You could additionally add helper functions that check the value of rc
to give you the string contents if appropriate. Rather than doing all that, though, you can use something similar that already exists in the standard library:
Option 3: Use a Variant
The std::variant
represents a type-safe union, and thus can hold either a return code or a string. Helper functions can be used to determine which one it holds. So, you might do something like:
std::variant<int, std::string> doSomething(){
std::variant<int, std::string> return_value;
std::string value;
int rc = callApi(value);
if(rc) return rc;
//do some processing on value
return return_value;
}
Then, from your calling function, you can check the variant to see which type it holds, and proceed accordingly:
auto v = doSomething();
int rc;
if(std::holds_alternative<int>(v)) {
rc = std::get<int>(v);
//Process error accordingly
if(std::holds_alternative<std::string>(v)) {
std::cout << "String returned was " << std::get<std::string>(v) << std::endl;
}
Upvotes: 0
Reputation: 509
std::variant
is a type-safe union that allows you to return one of a fixed set of types. In this case you'd want a std::variant<std::string, int>
Using std::variant
std::variant<std::string, int>doSomething(){
std::string value;
int rc = callApi(value);
if(rc == 0){ //if no error
//do some processing on value
return value;
}
return rc;
}
Upvotes: 1