https://zh.cppreference.com/w/cpp/language/structured_binding
https://www.bilibili.com/video/BV1NH4y1X7q6
结构化绑定是 C++ 17 新增的特性
简单的语法如下:
auto [a, b, c] = x;
auto& [d, e, f] = x;
const auto& [g, h, i] = x;
cppreference 中的定义是这样的:
属性 (可选) 声明说明符序列 引用限定符 (可选) [ 绑定标识符列表 ] 初始化器 ;
和普通的初始化一样,也可以拷贝初始化,也可以直接初始化,这一点和正常的类一样
在进行结构化绑定时,编译器会这样来判断是哪一种绑定:
- 检查结构化绑定的初始化表达式的类型,如果后面的类型是数组类型,那么进行数组绑定
- 如果不是数组类型,那么查看
std::tuple_size<T>
对该类型是不是合法,有没有size
常量,如果有,那么进行元组式绑定 - 如果上面都不是,那么就绑定到数据成员(优先级最低)
绑定到数组
基本的语法如下:
int arr[3] = {1, 2, 3};
auto [a, b, c] = arr;
实际上,编译器首先为我们定义了一个新的匿名变量,然后将 a
、b
、c
绑定到匿名变量中
举一下更多的例子,在 cppinsights 中:
int arr[3] = {1, 2, 3};
auto [a, b, c] = arr;
decltype(a) x = 3;
auto& [d, e, f] = arr;
decltype(d) y = x;
const auto [g, h, i] = arr;
decltype(g) z = y;
const auto& [j, k, l] = arr;
decltype(j) w = z;
和下面等价:
int arr[3] = {1, 2, 3};
int __arr6[3] = {arr[0], arr[1], arr[2]};
int& a = __arr6[0];
int& b = __arr6[1];
int& c = __arr6[2];
int x = 3;
int (&__arr9)[3] = arr;
int& d = __arr9[0];
int& e = __arr9[1];
int& f = __arr9[2];
int y = x;
const int __arr12[3] = {arr[0], arr[1], arr[2]};
const int& g = __arr12[0];
const int& h = __arr12[1];
const int& i = __arr12[2];
const int z = y;
const int (&__arr15)[3] = arr;
const int& j = __arr15[0];
const int& k = __arr15[1];
const int& l = __arr15[2];
const int w = z;
可以看到,使用结构化绑定时,a
到 l
都是 int
类型而不是 int&
类型,decltype(a)
实际上是 int
(就是所绑定的元素或成员的类型),看起来它是一个值类型,但实际上它是一个左值引用。这在 C++ 的值类别体系中是没问题的,因为它们都可以看成左值,而变量和引用之间实际上没什么差别
并且,结构化绑定时如果数组的类型有 cv 限定,那么它的元素类型也有同样的限定,也就是第三和第四个例子
这里的 auto
推导的是匿名对象的类型,满足 auto
的推导规则:
auto
是值类型推导,它进行退化。如果加上 cv 限定符变成const auto
,那么类型中就带有 cv 限定符auto&
是左值引用类型,它推导出来的就是左值引用。它会捕获 cv 限定符auto&&
是通用引用const auto&&
是const
右值引用
如果要推导出右值引用的话,可以使用 std::move
从 C++ 20 开始,也可以添加存储类说明符,如 static
、thread_local
绑定到数据成员
这里有一个最重要的限制:要求所有的非静态成员都是它自己的,或者它的某一个基类的。如:
struct A {
int x, y;
};
struct B : A {
};
struct C : B {
int z;
};
其中,A
、B
可以做结构化绑定,C
不可以。这涉及到标准内存布局的内容,可以看 一些特殊类类型
还有一些限制:
- 它必须可以访问到类的所有非静态数据成员
绑定到元组式类型的元素
C++ 提供了元组式绑定
标准库已经为 std::pair
、std::tuple
、std::array
等实现了结构化绑定
如果想要为自定义的类型实现结构化绑定,那么需要:
- 特化
std::tuple_size<T>
- 特化
std::tuple_element_t<I, T>
、在类中定义get<I>(_unnamed_)
成员函数或者在外部定义get<I>(_unnamed_)
函数(使用 ADL)
关于 get
函数中 _unnamed_
的类型:
- 如果它是左值引用,那么保持左值
- 否则视为亡值,即为右值引用
看个例子:
auto [a, b, c] = std::make_tuple(1, 2, 3);
auto tup = std::make_tuple(1, 2, 3);
auto& [d, e, f] = tup;
等价为:
std::tuple<int, int, int> __make_tuple5 = std::make_tuple(1, 2, 3);
int&& a = std::get<0UL>(static_cast<std::tuple<int, int, int>&&>(__make_tuple5)); // 这一坨 static_cast 应该是调用的 std::move
int&& b = std::get<1UL>(static_cast<std::tuple<int, int, int>&&>(__make_tuple5));
int&& c = std::get<2UL>(static_cast<std::tuple<int, int, int>&&>(__make_tuple5));
std::tuple<int, int, int> tup = std::make_tuple(1, 2, 3);
std::tuple<int, int, int>& __tup7 = tup;
int& d = std::get<0UL>(__tup7);
int& e = std::get<1UL>(__tup7);
int& f = std::get<2UL>(__tup7);
可以看到,左值引用调用左值版本的 get<I>
,其他则调用右值引用版本的 get<I>
,也就是说我们需要提供左值引用和右值引用两个版本
decltype()
取决于 std::tuple_element_t<I, T>
一个完整的自定义元组化绑定的例子:
struct Div {
int quot, rem;
};
template <>
struct std::tuple_size<Div> {
static constexpr std::size_t value = 2;
};
template <>
struct std::tuple_element<0, Div> {
using type = int;
};
template <>
struct std::tuple_element<1, Div> {
using type = int;
};
template <std::size_t I>
std::tuple_element_t<I, Div>& get(Div& div) {
if constexpr (I == 0) {
return div.quot;
} else {
return div.rem;
}
}
template <std::size_t I>
std::tuple_element_t<I, Div>&& get(Div&& div) {
if constexpr (I == 0) {
return std::move(div.quot);
} else {
return std::move(div.rem);
}
}
int main() {
auto [quot, rem] = Div{9, 4};
auto div = Div{9, 4};
auto& [quot2, rem2] = div;
auto&& [quot3, rem3] = Div{9, 4};
return 0;
}