raneshu
raneshu

Reputation: 413

alternative to std::optional that can report failure reasons

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

Answers (4)

einpoklum
einpoklum

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:

Upvotes: 5

Sergey Kolesnik
Sergey Kolesnik

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.

  • Throwing an exception is perfectly fine performance-wise if the error rarelly occurrs.
  • Throwing an exception simplifies your procedure logic. Instaed of checking each result for an error code you catch one exception.

Upvotes: 1

Marion
Marion

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

Salvage
Salvage

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

Related Questions