Returning several values from a function in C++ (C++23 edition)

5 days ago 1

Many programming languages such as the Go programming language are designed to make it easy to return several values at once from a function. In Go, it is often used to return an optional error code. The C++ programming language does not have a built-in support for returning several values. However, several standard types can serve the same purpose. If you need to return two values, you can use an std::pair instance. If you need to return two or more values, an std::tuple instance will do. With recent C++ standards, it works really well!

Suppose we want to compute a division with a string error message when one is trying to divided by zero:

std::tuple<int,std::string> divide(int a, int b) { if (b == 0) { return {0, "Error: Division by zero"}; } return {a / b, "Success"}; }

This approach works nicely. The code is clear and readable.

You might be concerned that we are fixing the type (int). If you want to write one function for all integer types, you can do so with concepts, like so:

#include <concepts> template <std::integral int_type> std::tuple<int_type, std::string> divide(int_type a, int_type b) { if (b == 0) { return {0, "Error: Division by zero"}; } return {a / b, "Success"}; }

With this definition, you can call divide(10,3), but if you try to call divide(10.0, 3.1323), you will get an error message such as “‘double’ does not satisfy ‘integral'”.

So far so good.

Prior to C++17, there was no elegant way to call this function and immediately capture the values without boilerplate code. Thankfully, we now have structured bindings:

auto [result, message] = divide(10, 2); std::println("Result: {}, Message: {}", result, message);

As a bonus, in this example, I am using C++23’s std::println function which is available with GCC 14 and LLVM 19.

The structured binding approach (the keyword ‘auto’ followed by brackets) is nice, but what if you care about only one of the return values ? In C++23, you can use the underscore to indicate that you do not care about a value:

auto [quotient, _] = divide(15, 3); std::println("Quotient only: {}", quotient);

What if you have existing variables, and you do not want to create new variables? You can use std::tie:

std::string msg = "Hello"; int i = 42; std::tie(i,msg) = divide(3, 0);

You can thus write decent error handling code:

if (auto [res, err] = divide(8, 0); err != "Success") { std::println("Error occurred: {}", err); } else { std::println("Result: {}", res); }

You can do slightly better if the reason for returning two values was strictly to be able to return an error code. You can use std::expected.

Your division function might be written as…

template <std::integral int_type> std::expected<int_type, std::string> divide(int_type a, int_type b) { if (b == 0) { return std::unexpected("Error: Division by zero"); } return a / b; }

It allows you to write slightly simpler error-handling code:

if (auto res1 = divide(10, 2)) { std::println("Result: {:>4}, Message: Success", res1.value()); } else { std::println("Error: {}", res1.error()); }

Another benefit of std::expected is that you can let the calling code choose the default value (in case of error):

int quotient = divide(15, 3).value_or(0); std::println("Quotient only: {:>4}", quotient);

If you prefer, you can use std::optional, which works much the same way:

std::optional<int> divide(int a, int b) { if (b == 0) { return std::nullopt; } return a / b; }

However, std::optional is less appropriate for error handling: you cannot pass an error code or an error message. The std::optional strategy is meant for the simple case where a value can be omitted.

In C++, returning multiple values from functions has evolved significantly, offering developers flexible and elegant solutions. From using std::pair and std::tuple for general-purpose multi-value returns to leveraging std::expected for robust error handling, and std::optional for optional values. By adopting these techniques, C++ programmers can make their code easier to read and to maintain.

Further reading:

Care is needed to use C++ std::optional with non-trivial objects

Read Entire Article