C++ 结构化绑定过程
起因
同学给出如下代码,指出 std::forward_as_tuple()
的返回值不能被 auto
结构化绑定:
#include <tuple>
int main() {
auto t1 = std::tuple{0};
auto [a0] = t1; // ok
auto t2 = std::forward_as_tuple(0);
auto [a1] = t2; // error
}
修改代码尝试了几次发现,t2
只是不能被 auto
结构化绑定,可以被 auto &
和 auto &&
(万能引用)结构化绑定。查阅资料得知 std::forward_as_tuple()
返回的 std::tuple
的参数类型都是完美转发后的类型,t2
的类型是 std::tuple<int &&>
而不是 std::tuple<int>
。
将代码修改如下,果然 t1
也无法被结构化绑定了。
#include <tuple>
int main() {
- auto t1 = std::tuple{0};
- auto [a0] = t1; // ok
+ auto t1 = std::tuple<int &&>{0}; // don't use CTAD
+ auto [a0] = t1; // error
auto t2 = std::forward_as_tuple(0);
auto [a1] = t2; // error
}
所以说,std::forward_as_tuple()
不应该背这个锅。
std::tuple<T&&>
的复制构造函数被删除了
从上面代码编译时的错误来看,结构化绑定在尝试调用 std::tuple
的复制构造函数,但是当有一个模板参数为右值引用类型时,std::tuple
的复制构造函数就会被删除,导致编译失败。
做以下测试:
struct A {
int &&x;
};
struct B {
int &x;
};
static_assert(std::is_move_constructible_v<A>);
static_assert(!std::is_copy_constructible_v<A>);
static_assert(std::is_move_constructible_v<B>);
static_assert(std::is_copy_constructible_v<B>);
这说明右值引用成员的存在会导致类的移动构造函数被删除。std::forward_as_tuple()
在转发纯右值的时候,将纯右值转换成了右值引用,从而返回的 std::tuple
中包含右值引用成员,因而不能用 auto
结构化绑定(auto &
可以)。
另一个问题是:为什么上述代码的结构化绑定会调用 std::tuple
的复制构造函数呢?
结构化绑定过程
https://en.cppreference.com/w/cpp/language/structured_binding
<attr> auto <C++20:static/thread_local> <cv> <ref> [<id-list>] = <initializer>
<attr> auto <C++20:static/thread_local> <cv> <ref> [<id-list>] { <initializer> }
<attr> auto <C++20:static/thread_local> <cv> <ref> [<id-list>] ( <initializer> )
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 这一块在 cppreference 写的是 cv-auto
C++20 起,可以增加 static
或者 thread_local
修饰符,同时 volatile
修饰符在结构化绑定中的使用被标记为过时。
步骤 1: 创建临时变量 e
结构化绑定时会创建一个临时变量 e
用来存储初始化器(initializer)的值。初始化的方式如下:
- 如果表达式是数组类型
A
,而且没有引用限定符,那么 e 的类型是<cv> A
,其中<cv>
表示结构化绑定声明时的 cv 限定符。e
的每个元素会被复制构造或者直接构造。(对于基本类型数组来说,就是拷贝了一遍数组。) - 否则,
e
按照取代[<id-list>]
的方式定义。
举个例子来说明第 2 点:
- auto const &[x, y] = std::pair{3, 4};
+ auto const &e = std::pair{3, 4};
在初始化器不为数组类型的情况下,就好像把变量名替换成临时变量 e
一样。这个时候 e
的类型为 std::pair<int, int> const &
。
Clangd 的类型提示
如果用 Clangd 作为 LSP,把鼠标指到 [
的位置上去,会看到:
variable (anonymous)
Type: const std::pair<int, int> & (aka const pair<int, int> &)
这个 anonymous
变量应该就是指的临时变量 e
。
一开始结构化绑定测试代码中的 auto [a1] = t2
会尝试调用 std::tuple
的复制构造函数也是因为临时变量 e
是用 auto
定义的。
步骤 2:决定绑定方式
记 e
的无引用类型 E
为 std::remove_reference_t<decltype((e))>
。
- 如果
E
是数组类型,使用数组绑定方式。 - 如果
E
是非 union 的类类型,且std::tuple_size<E>
有定义且有一个成员名为value
,使用元组绑定方式。 - 如果
E
是非 union 的类类型,且std::tuple_size<E>
无定义,使用数据成员绑定方式。
步骤 3:绑定
数组绑定
int bind_array() {
int a[2] = {1, 2};
auto [x, y] = a; // creates e[2], copies a into e,
// then x refers to e[0], y refers to e[1]
auto &[xr, yr] = a; // xr refers to a[0], yr refers to a[1]
static_assert(std::is_same_v<decltype(x), int>);
static_assert(std::is_same_v<decltype(y), int>);
static_assert(std::is_same_v<decltype(xr), int>);
static_assert(std::is_same_v<decltype(yr), int>);
}
可以看到 auto &
只是作用于临时值 e
上面的,从绑定中获得的元素并不是引用类型的。
元组绑定
元组绑定规则:对于 tuple-like 类型,std::tuple_size<E>::value
必须是整数常量表达式,结构化绑定中每个变量的类型按顺序则由 std::tuple_element<i, E>::type
决定。决定了每个变量的类型之后,就要用 e.get<i>()
(优先考虑)或者 get<i>(e)
(只考虑 ADL,不考虑非 ADL,比如说 std::tuple
就考虑 std::get
)来逐一初始化它们。
这说明 auto &[...] = ...
的 &
(ref-qualifier)只会影响临时匿名变量的类型,不会影响被绑定标识符的类型。如果想要在结构化绑定时建立引用关系,则需要 std::tuple
的参数类型(或者 std::tuple_element<i, E>::type
)为引用类型。
void bind_with_ref() {
float x{};
char y{};
int z{};
std::tuple<float &, char &&, int> tpl(x, std::move(y), z);
{
const auto &[a, b, c] = tpl;
static_assert(std::is_same_v<decltype(a), float &>);
static_assert(std::is_same_v<decltype(b), char &&>);
static_assert(std::is_same_v<decltype(c), int const>);
}
{
// c 没有引用 z,而是复制了一份。
auto &[a, b, c] = tpl;
static_assert(std::is_same_v<decltype(a), float &>);
static_assert(std::is_same_v<decltype(b), char &&>);
static_assert(std::is_same_v<decltype(c), int>);
c = 5;
std::cout << "z: " << z << "\n"; // prints "z: 0"
}
}
虽然 ref-qualifier 没有作用在绑定后的变量上,但是 cv-qualifier 作用了在绑定后的变量上。第一个例子中 c
的类型为 int const
就是证明。
数据成员绑定
- 所有数据成员都要参与绑定,少了一个都不行。
- 必须有所有数据成员的访问权限。
void foo() {
struct C {
int x;
private:
int y;
};
auto [x, y] = C{}; // error: Cannot decompose private member 'y' of 'C'
// error: Type 'C' decomposes into 2 elements, but only 1 name was provided
auto [x1] = C{};
}
结构化绑定的顺序
以下由 C++ 语言规范保证:
- 临时变量的创建在所有标识符绑定之前。
- 位次小的变量绑定总是在位次大的变量绑定之前。
其他注意点
Cppreference 上写了不少注意点,这里只抄了一部分我觉得容易忽略的。(网页上最后的 Notes 是需要非常关注的,里面讲了不少额外信息。)
🚩 The portion of the declaration preceding [
applies to the hidden variable e, not to the introduced identifiers:
int a = 1, b = 2;
const auto& [x, y] = std::tie(a, b); // x and y are of type int&
auto [z, w] = std::tie(a, b); // z and w are still of type int&
assert(&z == &a); // passes
🚩 结构化绑定创建的变量在 C++20 之前不能被 lambda 捕获。