CppCoreGuidelines F.21 如果要返回多个值,可以返回一个结构
F.21: To return multiple “ out ” values, prefer returning a struct
理由
函数结果通过返回值的方式返回,意义明确,代码更容易理解。C++ 里可以用类似 tuple
的类型(包括struct
, array
, tuple
)返回多个值,还可以直接在调用处进行结构绑定(C++17);最好使用定义了类型名的结构体,这样可以通过成员名能提供语义解释(推荐)。不命名的元组在泛型模板代码中用的比较多。
例子
// BAD: output-only parameter documented in a comment int f1(const string& input, /*output only*/ string& output_data) { // ... int status = 1; output_data = input + " hello"; return status; } int main() { auto ss = string(); if(f1("bonjour,", ss)) { cout << ss << endl; } }
bonjour, hello
// GOOD: self-documenting tuple<int, string> f2(const string& input) { // ... int status = 1; return make_tuple(status, input + " hello"); } int main() { auto tt = f2("bonjour,"); if(get<int>(tt)) { cout << get<string>(tt) << endl; } }
bonjour, hello
C++98 标准库就已经在用这个方法返回值。比如下面代码中, pair
其实就是有两个元素的 tuple
:
// C++98 set<string> my_set; pair<set<string>::iterator, bool> result = my_set.insert("Hello"); if (result.second) { cout << result.second << endl; //1 cout << *result.first; // workaround }
1 Hello
在C++11中,可以直接把结果填入多个局部变量:
set<string> my_set; set<string>::iterator iter; int success; tie(iter, success) = my_set.insert("Hello"); if (success) cout << *iter;
Hello
C++17 支持以“结构绑定”的方式直接初始化多个变量:
set<string> my_set; if (auto [ iter, success ] = my_set.insert("Hello"); success) cout << *iter;
Hello
结构体中的每个变量都有名字,更好理解。比如 ranges::min_max_result
,
from_chars_result
等。
例外
有时,我们要传一个对象给函数,让函数修改其状态。这时候,最好通过引用
T&
传递对象。通常不需要显式的传一个 in-out 参数做为返回值。
例子
// much like std::operator>>() istream& operator>>(istream& is, string& s); for (string s; cin >> s; ) { // do something with line }
这里, s
和 cin
都是作为输入输出参数传递。以非 const
引用的方式传入
cin
,其状态可以改变。以引用传递 s
是避免重复分配内存资源。只有需要扩展 s
的大小时,分配新内存。这个技术叫做“caller-allocated out”(“调用者分配输出”)模式。适用于 string
和 vector
等类型。
对比的,如果全部值都要返回的话,要写成这样:
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 }
我们认为这样很不优雅,性能较差。
注意
很多情况,返回一个自定义的类型比较有用,比如:
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
当返回值是互相独立的数据实体的时候,可以用极度泛型的 pair
和 tuple
解构返回值。但不要用于封装得很好的抽象实体。
另外,也可以用optional<T>
或 expected<T, error_code>
来返回可能不同的类型值。尽量避免用 pair
和 tuple
。像pair<T, bool>
和 pair<T,
error_code>
这种类型描述不够直观。
注意
如果从局部组成的对象太大,不适合复制,可以考虑显式的 move
移动操作,从而避免产生复制。
pair<LargeObject, LargeObject> f(const string& input) { LargeObject large1 = g(input); LargeObject large2 = h(input); // ... return { move(large1), move(large2) }; // no copies }
也可以这样写:
pair<LargeObject, LargeObject> f(const string& input) { // ... return { g(input), h(input) }; // no copies, no moves }
强化
- 用返回值替换出参。出参指的是: 1 )函数会改写; 2 )函数修改参数的成员变量,且变量不是常量; 3 )以非
const
的形式传入。 - 用结构体作为返回值类型,不要用
pair
或tuple
。不过在泛型模板中,tuple
可能无法避免。