31 May 2022

C++ 核心指南目录

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
}

这里, scin 都是作为输入输出参数传递。以非 const 引用的方式传入 cin ,其状态可以改变。以引用传递 s 是避免重复分配内存资源。只有需要扩展 s 的大小时,分配新内存。这个技术叫做“caller-allocated out”(“调用者分配输出”)模式。适用于 stringvector 等类型。

对比的,如果全部值都要返回的话,要写成这样:

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

当返回值是互相独立的数据实体的时候,可以用极度泛型的 pairtuple 解构返回值。但不要用于封装得很好的抽象实体。

另外,也可以用optional<T>expected<T, error_code> 来返回可能不同的类型值。尽量避免用 pairtuple 。像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 的形式传入。
  • 用结构体作为返回值类型,不要用 pairtuple 。不过在泛型模板中, tuple 可能无法避免。