2

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;
}
Chris
  • 26,361
  • 5
  • 21
  • 42
raneshu
  • 363
  • 2
  • 16
  • 46
  • 1
    If you want to return 2 or more things, then return a `pair` or `tuple` of those things. The standard library does this with `std::unordered_map::insert` for instance. – NathanOliver Jan 03 '22 at 19:52
  • You could use a `pair` returning an error code and the result (or nothing). – rturrado Jan 03 '22 at 19:52
  • Do you really need to return the error code from the underlying API? Wouldn't be better to throw an exception in case of error? – Daniel Langr Jan 03 '22 at 19:53
  • Thank you! In c++ (modern cpp if you will), is it recommended to use `std::variant`/`std::pairt`/etc or to pass the `result` as a parameter. Or is it just personal preference? – raneshu Jan 03 '22 at 19:53
  • 1
    My opinion is to use `std::pair`. There are already algorithm functions that work this way, for example `std::map::insert()`. – PaulMcKenzie Jan 03 '22 at 19:54
  • 3
    `variant` lets you return 1 value of N different types. `pair`/`tuple` lets you return 2/N different values of 1 or more types – NathanOliver Jan 03 '22 at 19:56
  • Thanks. I think I'll go with `std::pair` – raneshu Jan 03 '22 at 20:11
  • It sounds like you are putting prettiness over code simplicity. – Dúthomhas Jan 03 '22 at 20:17
  • what would be the simple solution? passing `result` as parameter to the function? – raneshu Jan 03 '22 at 20:18
  • It just doesn't feel natural to return an `int` from every function because we need to account for the rc_code. So I'm thinking of something like `std::pair>` – raneshu Jan 03 '22 at 20:25

4 Answers4

4

It seems like what you're looking for is std::expected.

... it's not actually in the standard already, but:

  • You can read a recent version of the proposal, P0323 revision 10
  • You can use it! Or rather, an implemented of it by the talented Sy Brand / TartanLlama, including some nice extensions which I won't go into.

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: What is std::expected in C++?

einpoklum
  • 118,144
  • 57
  • 340
  • 684
  • Should probably note that this is insanily overkill (as long as it's not standardized) for anything in which the pattern in question doesn't pervade the entire codebase. – Salvage Jan 03 '22 at 20:12
  • I like this - but would prefer `std::pair` as it is in the current standard. – raneshu Jan 03 '22 at 20:13
  • 1
    @raneshu: A pair of what? – einpoklum Jan 03 '22 at 20:18
  • 1
    `std::pair> result_pair;` – raneshu Jan 03 '22 at 20:20
  • 4
    Ah, pair _and_ optional. Yes, that will work quite well and is basically "poor man's `std::expected`". But you would probably need to write a long file of helper functions to work with these, as otherwise your readers would be rather confused. – einpoklum Jan 03 '22 at 20:23
1

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;
 }
Salvage
  • 448
  • 1
  • 4
  • 13
1

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.
Sergey Kolesnik
  • 3,009
  • 1
  • 8
  • 28
0

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;
}
Marion
  • 198
  • 6
  • Thanks! re: option 1 - is it ok to simply throw int `rc` and not an actual `Error` or `Exception`? – raneshu Jan 03 '22 at 20:16
  • Yes; in C++ you can throw any type that is not (1) incomplete or (2) an rvalue reference. In this case, since you're throwing an int, you catch it with `catch (const int & e)`. You could, of course, throw an error or other exception with the `what` attribute set as the value of rc, but this would require casting rc to a string, then presumably back to an integer to handle it. – Marion Jan 03 '22 at 20:27