Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

F.21 Don't return tuples #2166

Merged
merged 7 commits into from
Jan 25, 2024
Merged
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 32 additions & 27 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.
Eisenwave marked this conversation as resolved.
Show resolved Hide resolved
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) {
Eisenwave marked this conversation as resolved.
Show resolved Hide resolved
// do something with string
}

We consider that significantly less elegant with significantly less performance.
Expand All @@ -3313,7 +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.
In most cases, it is useful to return a specific, user-defined type.
For example:

struct Distance {
Expand All @@ -3327,13 +3330,13 @@ For example:
// 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.
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`.
Another option is to use `optional<T>` or `expected<T, error_code>`, rather than `pair` or `tuple`.
Eisenwave marked this conversation as resolved.
Show resolved Hide resolved

##### 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 @@ -3358,6 +3361,8 @@ Note this is different from the `return move(...)` anti-pattern from [ES.56](#Re

* 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 Expand Up @@ -3613,8 +3618,8 @@ Using `std::shared_ptr` is the standard way to represent shared ownership. That
std::thread t2 {shade, args2, bottom_left, im};
std::thread t3 {shade, args3, bottom_right, im};

// detaching threads requires extra care (e.g., to join
// before main ends), but even if we do detach t0..3 here ...
// detaching threads requires extra care (e.g., to join before
// main ends), but even if we do detach the four threads here ...
}
// ... shared_ptr ensures that eventually the last thread to
// finish safely deletes the image
Expand Down