Skip to content

Commit

Permalink
F.21 don't return tuples
Browse files Browse the repository at this point in the history
  • Loading branch information
Eisenwave committed Jan 2, 2024
1 parent e49158a commit 21cf8b5
Showing 1 changed file with 46 additions and 42 deletions.
88 changes: 46 additions & 42 deletions CppCoreGuidelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -2358,7 +2358,7 @@ Parameter passing expression rules:
* [F.18: For "will-move-from" parameters, pass by `X&&` and `std::move` the parameter](#Rf-consume)
* [F.19: For "forward" parameters, pass by `TP&&` and only `std::forward` the parameter](#Rf-forward)
* [F.20: For "out" output values, prefer return values to output parameters](#Rf-out)
* [F.21: To return multiple "out" values, prefer returning a struct or tuple](#Rf-out-multi)
* [F.21: To return multiple "out" values, prefer returning a struct](#Rf-out-multi)
* [F.60: Prefer `T*` over `T&` when "no argument" is a valid option](#Rf-ptr-ref)

Parameter passing semantic rules:
Expand Down Expand Up @@ -3228,13 +3228,15 @@ The return value optimization doesn't handle the assignment case, but the move a

* Flag reference to non-`const` parameters that are not read before being written to and are a type that could be cheaply returned; they should be "out" return values.

### <a name="Rf-out-multi"></a>F.21: To return multiple "out" values, prefer returning a struct or tuple
### <a name="Rf-out-multi"></a>F.21: To return multiple "out" values, prefer returning a struct

##### Reason

A return value is self-documenting as an "output-only" value.
Note that C++ does have multiple return values, by convention of using a `tuple` (including `pair`), possibly with the extra convenience of `tie` or structured bindings (C++17) at the call site.
Prefer using a named struct where there are semantics to the returned value. Otherwise, a nameless `tuple` is useful in generic code.
Note that C++ does have multiple return values, by convention of using tuple-like types (`struct`, `array`, `tuple`, etc.),
possibly with the extra convenience of structured bindings (C++17) at the call site.
Prefer using a named `struct` if possible.
Otherwise, a `tuple` is useful in variadic templates.

##### Example

Expand All @@ -3247,30 +3249,29 @@ Prefer using a named struct where there are semantics to the returned value. Oth
}

// GOOD: self-documenting
tuple<int, string> f(const string& input)
struct f_result { int status; string data; };

f_result f(const string& input)
{
// ...
return {status, something()};
}

C++98's standard library already used this style, because a `pair` is like a two-element `tuple`.
C++98's standard library somewhat used this style by returning `pair` in some functions.
For example, given a `set<string> my_set`, consider:

// C++98
result = my_set.insert("Hello");
if (result.second) do_something_with(result.first); // workaround

With C++11 we can write this, putting the results directly in existing local variables:
pair<set::iterator, bool> result = my_set.insert("Hello");
if (result.second)
do_something_with(result.first); // workaround

Sometype iter; // default initialize if we haven't already
Someothertype success; // used these variables for some other purpose
With C++17 we are able to use "structured bindings" to give each member a name:

tie(iter, success) = my_set.insert("Hello"); // normal return value
if (success) do_something_with(iter);
if (auto [ iter, success ] = my_set.insert("Hello"); success)
do_something_with(iter);

With C++17 we are able to use "structured bindings" to declare and initialize the multiple variables:

if (auto [ iter, success ] = my_set.insert("Hello"); success) do_something_with(iter);
A `struct` with meaningful names is more common in modern C++.
See for example `ranges::min_max_result`, `from_chars_result`, and others.

##### Exception

Expand All @@ -3294,15 +3295,17 @@ such as `string` and `vector`, that needs to do free store allocations.

To compare, if we passed out all values as return values, we would something like this:

pair<istream&, string> get_string(istream& in) // not recommended
struct get_string_result { istream& in; string s; };

get_string_result get_string(istream& in) // not recommended
{
string s;
in >> s;
return {in, move(s)};
return { in, move(s) };
}

for (auto p = get_string(cin); p.first; p.second = get_string(p.first).second) {
// do something with p.second
for (auto [in, s] = get_string(cin); in; s = get_string(stream).s) {
// do something with string
}

We consider that significantly less elegant with significantly less performance.
Expand All @@ -3313,27 +3316,7 @@ However, we prefer to be explicit, rather than subtle.

##### Note

In many cases, it can be useful to return a specific, user-defined type.
For example:

struct Distance {
int value;
int unit = 1; // 1 means meters
};

Distance d1 = measure(obj1); // access d1.value and d1.unit
auto d2 = measure(obj2); // access d2.value and d2.unit
auto [value, unit] = measure(obj3); // access value and unit; somewhat redundant
// to people who know measure()
auto [x, y] = measure(obj4); // don't; it's likely to be confusing

The overly-generic `pair` and `tuple` should be used only when the value returned represents independent entities rather than an abstraction.

Another example, use a specific type along the lines of `variant<T, error_code>`, rather than using the generic `tuple`.

##### Note

When the tuple to be returned is initialized from local variables that are expensive to copy,
When the object to be returned is initialized from local variables that are expensive to copy,
explicit `move` may be helpful to avoid copying:

pair<LargeObject, LargeObject> f(const string& input)
Expand All @@ -3354,10 +3337,31 @@ Alternatively,

Note this is different from the `return move(...)` anti-pattern from [ES.56](#Res-move)

##### Note

For `struct`s where one member represents the success of an operation, adding an explicit conversion to `bool` is helpful:

struct parse_result {
bool success;
int value;
explicit operator bool() const { return success; }
};

parse_result parse_int(string_view s) { /* ... */ }

void f()
{
if (parse_result result = parse_int("123")) { use(result.value); }
}

In most cases, C++17 `optional` and C++23 `expected` can replace this pattern.

##### Enforcement

* Output parameters should be replaced by return values.
An output parameter is one that the function writes to, invokes a non-`const` member function, or passes on as a non-`const`.
* `pair` or `tuple` return types should be replaced by `struct`, if possible.
In variadic templates, `tuple` is often unavoidable.

### <a name="Rf-ptr-ref"></a>F.60: Prefer `T*` over `T&` when "no argument" is a valid option

Expand Down

0 comments on commit 21cf8b5

Please sign in to comment.