第25.2章:结构化绑定

English

1. 不写仪式地解包

结构化绑定一次声明多个名字,并把它们绑定到一个对象的元素上。它把这段:

auto result = try_insert(map, key, value);
auto it = result.first;
bool inserted = result.second;

变成:

auto [it, inserted] = try_insert(map, key, value);

语法是带方括号标识符列表的声明:

auto [a, b] = pair;
auto &[x, y] = pair;
auto const &[x, y] = pair;
auto &&[x, y] = make_pair();

这个声明不是 tuple 类型,名字也不是通常意义上的独立变量。编译器引入一个隐藏对象,用右侧初始化它, 然后让每个名字引用这个隐藏对象的一个元素。

2. 什么可以被分解

种类 元素 例子
数组 数组元素,按顺序 int a[3]; auto [x, y, z] = a;
类 tuple 类型 std::tuple、std::pair、std::array,或任何提供 tuple_size、tuple_element 和 get 的类型 auto [x, y] = std::pair{1, 2};
聚合体 公开的非静态数据成员,按声明顺序 struct point { int x, y; }; auto [x, y] = point{1, 2};

名字数量必须与元素数量匹配。不匹配是编译错误。没有“跳过某个”的语法;只需要部分值时,用 _ 这样的占位名。

struct point
{
    int x;
    int y;
};

point p{3, 4};
auto [x, y] = p;          // x == 3, y == 4
auto &[rx, ry] = p;       // rx and ry refer to p.x and p.y
rx = 10;                   // p.x is now 10

对聚合体来说,只有公开的直接非静态数据成员参与。有私有成员、虚函数或用户声明构造函数的类, 不会按聚合体分解。基类不是扁平的成员列表;涉及继承时,不要依赖某个特定顺序。

3. auto、auto&、auto&&

声明上的限定符作用于隐藏对象,而不是分别作用于每个名字。这是生命周期和可变性 bug 的来源。

声明 隐藏对象 名字
auto [a, b] = expr; expr 的副本 引用副本;修改 a 不会修改 expr
auto &[a, b] = expr; 对 expr 的引用 引用 expr;修改 a 会修改 expr
auto const &[a, b] = expr; const 引用 只读;延长临时对象的生命周期
auto &&[a, b] = expr; 转发引用 像 auto const & 一样延长临时对象生命周期,但保留可变性和值类别
std::pair<int, int> make();

auto [a, b] = make();             // copies the pair
auto &[c, d] = make();            // error: cannot bind a non-const lvalue reference to a temporary
auto const &[e, f] = make();      // OK: lifetime extended
auto &&[g, h] = make();          // OK: rvalue reference to the temporary

最后两个看起来相似,但不一样。auto const & 和 auto && 都会延长临时对象的生命周期到作用域结束。区别是 const 和值类别:auto && 保留可变性,也保留它绑定的是右值这一事实;auto const & 是只读视图。 如果你需要之后使用这些值,直接用 auto 或 auto const &。

4. map 的场景

最常见的真实用途是遍历 map 和向 map 插入:

std::map<std::string, int> counts;

for (auto const &[key, value] : counts)
{
    std::print("{} -> {}\n", key, value);
}

if (auto [it, inserted] = counts.try_emplace("apple", 1); inserted)
{
    std::print("new key: {}\n", it->first);
}

在 map 里,key 是 const:容器拥有排序,不允许你通过迭代器修改键。 绑定为非 const 时,value 可修改。这不是结构化绑定的怪癖;这是 map 迭代器类型本来的样子。

if 初始化语句让 it 和 inserted 在条件和函数体里可见。 这是使用 try_emplace 结果而不把变量泄漏到函数其余部分的惯用方式。

5. 类 tuple 类型与定制

对类 tuple 类型,编译器使用 std::tuple_size、std::tuple_element 和 get。标准类型已经提供。用户类型可以加入:

struct version
{
    int major;
    int minor;
};

namespace std
{
template <>
struct tuple_size<version> : integral_constant<std::size_t, 2>
{
};

template <std::size_t I>
struct tuple_element<I, version>
{
    using type = int;
};
}

template <std::size_t I>
int get(version const &v)
{
    if constexpr (I == 0)
    {
        return v.major;
    }
    else
    {
        return v.minor;
    }
}

version v{1, 2};
auto [major, minor] = v;

对一个两字段结构体来说,这套机制很多。聚合体路径已经处理公开数据成员。只有当类型不是聚合体, 或者分解顺序不是声明顺序时,才用 tuple 协议。

get 通过参数依赖查找找到。它必须在结构化绑定处可见。该类型命名空间里的自由函数是 可移植形式。

6. 会咬人的细节

7. 小例子:解析结果

struct parse_result
{
    bool ok;
    int value;
};

parse_result parse(std::string_view text)
{
    if (text == "42")
    {
        return {true, 42};
    }
    return {false, 0};
}

int main()
{
    if (auto [ok, value] = parse("42"); ok)
    {
        std::print("value = {}\n", value);
    }
    else
    {
        std::print("parse failed\n");
    }
}

初始化语句不是结构化绑定的一部分;它是第3.3章 的普通 if 初始化语句。两个特性自然组合:绑定声明名字,初始化语句限定它们的作用域。

要点

下一页从解包值转向决定值住在哪里的声明:存储期、链接和命名空间。