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 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
// BAD: output-only parameter documented in a comment
int f(const string& input, /*output only*/ string& output_data)
{
// ...
output_data = something();
return status;
}
// GOOD: self-documenting
struct f_result { int status; string data; };
f_result f(const string& input)
{
// ...
return {status, something()};
}
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:
// C++98
pair<set::iterator, bool> result = my_set.insert("Hello");
if (result.second)
do_something_with(result.first); // workaround
With C++17 we are able to use "structured bindings" to give each member a name:
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
Sometimes, we need to pass an object to a function to manipulate its state. In such cases, passing the object by reference T& is usually the right technique. Explicitly passing an in-out parameter back out again as a return value is often not necessary. For example:
istream& operator>>(istream& in, string& s); // much like std::operator>>()
for (string s; in >> s; ) {
// do something with line
}
Here, both s and in are used as in-out parameters. We pass in by (non-const) reference to be able to manipulate its state. We pass s to avoid repeated allocations. By reusing s (passed by reference), we allocate new memory only when we need to expand s's capacity. This technique is sometimes called the "caller-allocated out" pattern and is particularly useful for types, such as string and vector, that need to do free store allocations.
To compare, if we passed out all values as return values, we would write something like this:
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) };
}
for (auto [in, s] = get_string(cin); in; s = get_string(in).s) {
// do something with string
}
We consider that significantly less elegant with significantly less performance.
For a truly strict reading of this rule (F.21), the exception isn't really an exception because it relies on in-out parameters, rather than the plain out parameters mentioned in the rule. However, we prefer to be explicit, rather than subtle.
Note
In most cases, it is 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 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
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)
{
LargeObject large1 = g(input);
LargeObject large2 = h(input);
// ...
return { move(large1), move(large2) }; // no copies
}
Alternatively,
pair<LargeObject, LargeObject> f(const string& input)
{
// ...
return { g(input), h(input) }; // no copies, no moves
}
Note this is different from the return move(...) anti-pattern from ES.56.
Enforcement
An output parameter is one that the function writes to, invokes a non-const member function, or passes on as a non-const.
In variadic templates, tuple is often unavoidable.
- Output parameters should be replaced by return values.
pairortuplereturn types should be replaced bystruct, if possible.