From 21cf8b5822dba4f409458e1d24dfde288fa8f8c7 Mon Sep 17 00:00:00 2001 From: Eisenwave Date: Tue, 2 Jan 2024 14:05:43 +0100 Subject: [PATCH] F.21 don't return tuples --- CppCoreGuidelines.md | 88 +++++++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 42 deletions(-) diff --git a/CppCoreGuidelines.md b/CppCoreGuidelines.md index ff7f72b14..895ca5c11 100644 --- a/CppCoreGuidelines.md +++ b/CppCoreGuidelines.md @@ -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: @@ -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. -### F.21: To return multiple "out" values, prefer returning a struct or tuple +### 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 @@ -3247,30 +3249,29 @@ Prefer using a named struct where there are semantics to the returned value. Oth } // GOOD: self-documenting - tuple 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 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 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 @@ -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 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. @@ -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`, 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 f(const string& input) @@ -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. ### F.60: Prefer `T*` over `T&` when "no argument" is a valid option