第25.2章:结构化绑定
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. 会咬人的细节
-
不能给单个名字加
const、volatile或static。限定符作用于隐藏对象;用auto const &, 或者把值复制到单独变量。 - 结构化绑定不是一个能整体取地址的变量。每个名字引用隐藏对象的子对象。 名字是左值时可以取它的地址,但绑定整体没有地址。
-
位域不能绑定到非 const 引用。复制可以;
auto &绑定不行。 -
生命周期是隐藏对象的生命周期。引用临时对象内部的绑定需要临时对象活着;
auto const &是常见修复。 -
constexpr结构化绑定是 C++26。更早的标准会拒绝constexpr auto [x, y] = ...,即使初始化器是常量。 - lambda 捕获是按名字的。捕获单个名字比试图整体捕获绑定更清晰,也能避免生命周期意外。
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 初始化语句。两个特性自然组合:绑定声明名字,初始化语句限定它们的作用域。
要点
- 结构化绑定解包数组、类 tuple 类型和公开聚合体成员。
- 声明创建一个隐藏对象;名字引用它内部。
auto复制,auto &别名,auto const &延长临时对象,auto &&转发。- 名字数量必须等于元素数量。
- map 的键是 const;值可变。
- 类 tuple 定制可用,但聚合体通常不需要。
- 注意生命周期、位域和仅 C++26 的
constexpr形式。
下一页从解包值转向决定值住在哪里的声明:存储期、链接和命名空间。