F.21 Don't return tuples (#2166)

* F.21 don't return tuples

* F.21 implement Herb's suggestions

* Mini-rebase for spell check fix

* elaborate on optional/expected

Co-authored-by: Jonathan Wakely <github@kayari.org>

* improve wording in one sentence

Co-authored-by: Jonathan Wakely <github@kayari.org>

* fix incorrect code transformation

Co-authored-by: Jonathan Wakely <github@kayari.org>

* fix missing word

---------

Co-authored-by: Herb Sutter <herb.sutter@gmail.com>
Co-authored-by: Jonathan Wakely <github@kayari.org>
This commit is contained in:
Jan Schultke 2024-01-25 21:27:47 +01:00 committed by GitHub
parent e784212ef0
commit 631eccd444
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -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.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.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.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) * [F.60: Prefer `T*` over `T&` when "no argument" is a valid option](#Rf-ptr-ref)
Parameter passing semantic rules: Parameter passing semantic rules:
@ -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. * 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 ##### Reason
A return value is self-documenting as an "output-only" value. 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. Note that C++ does have multiple return values, by convention of using tuple-like types (`struct`, `array`, `tuple`, etc.),
Prefer using a named struct where there are semantics to the returned value. Otherwise, a nameless `tuple` is useful in generic code. 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 ##### Example
@ -3247,30 +3249,29 @@ Prefer using a named struct where there are semantics to the returned value. Oth
} }
// GOOD: self-documenting // 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()}; 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 used this style in places, by returning `pair` in some functions.
For example, given a `set<string> my_set`, consider: For example, given a `set<string> my_set`, consider:
// C++98 // C++98
result = my_set.insert("Hello"); pair<set::iterator, bool> result = my_set.insert("Hello");
if (result.second) do_something_with(result.first); // workaround if (result.second)
do_something_with(result.first); // workaround
With C++11 we can write this, putting the results directly in existing local variables: With C++17 we are able to use "structured bindings" to give each member a name:
Sometype iter; // default initialize if we haven't already if (auto [ iter, success ] = my_set.insert("Hello"); success)
Someothertype success; // used these variables for some other purpose do_something_with(iter);
tie(iter, success) = my_set.insert("Hello"); // normal return value A `struct` with meaningful names is more common in modern C++.
if (success) do_something_with(iter); See for example `ranges::min_max_result`, `from_chars_result`, and others.
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);
##### Exception ##### Exception
@ -3292,17 +3293,19 @@ By reusing `s` (passed by reference), we allocate new memory only when we need t
This technique is sometimes called the "caller-allocated out" pattern and is particularly useful for types, This technique is sometimes called the "caller-allocated out" pattern and is particularly useful for types,
such as `string` and `vector`, that needs to do free store allocations. 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: To compare, if we passed out all values as return values, we would write 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; string s;
in >> 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) { for (auto [in, s] = get_string(cin); in; s = get_string(in).s) {
// do something with p.second // do something with string
} }
We consider that significantly less elegant with significantly less performance. We consider that significantly less elegant with significantly less performance.
@ -3313,7 +3316,7 @@ However, we prefer to be explicit, rather than subtle.
##### Note ##### 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: For example:
struct Distance { struct Distance {
@ -3327,13 +3330,14 @@ For example:
// to people who know measure() // to people who know measure()
auto [x, y] = measure(obj4); // don't; it's likely to be confusing 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`.
When used appropriately these types convey more information about what the members mean than `pair<T, bool>` or `pair<T, error_code>` do.
##### Note ##### 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: explicit `move` may be helpful to avoid copying:
pair<LargeObject, LargeObject> f(const string& input) pair<LargeObject, LargeObject> f(const string& input)
@ -3358,6 +3362,8 @@ Note this is different from the `return move(...)` anti-pattern from [ES.56](#Re
* Output parameters should be replaced by return values. * 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`. 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 ### <a name="Rf-ptr-ref"></a>F.60: Prefer `T*` over `T&` when "no argument" is a valid option