C++ 的结构化绑定

https://zh.cppreference.com/w/cpp/language/structured_binding

https://www.bilibili.com/video/BV1NH4y1X7q6

https://cppinsights.io

结构化绑定是 C++ 17 新增的特性

简单的语法如下:

auto [a, b, c] = x;
auto& [d, e, f] = x;
const auto& [g, h, i] = x;

cppreference 中的定义是这样的:

属性(可选) 声明说明符序列 引用限定符(可选) [ 绑定标识符列表 ] 初始化器;

和普通的初始化一样,也可以拷贝初始化,也可以直接初始化,这一点和正常的类一样

在进行结构化绑定时,编译器会这样来判断是哪一种绑定:

  1. 检查结构化绑定的初始化表达式的类型,如果后面的类型是数组类型,那么进行数组绑定
  2. 如果不是数组类型,那么查看 std::tuple_size<T> 对该类型是不是合法,有没有 size 常量,如果有,那么进行元组式绑定
  3. 如果上面都不是,那么就绑定到数据成员(优先级最低)

绑定到数组

基本的语法如下:

int arr[3] = {1, 2, 3};
auto [a, b, c] = arr;

实际上,编译器首先为我们定义了一个新的匿名变量,然后将 abc 绑定到匿名变量中

举一下更多的例子,在 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;

可以看到,使用结构化绑定时,al 都是 int 类型而不是 int& 类型,decltype(a) 实际上是 int(就是所绑定的元素或成员的类型),看起来它是一个值类型,但实际上它是一个左值引用。这在 C++ 的值类别体系中是没问题的,因为它们都可以看成左值,而变量和引用之间实际上没什么差别

并且,结构化绑定时如果数组的类型有 cv 限定,那么它的元素类型也有同样的限定,也就是第三和第四个例子

这里的 auto 推导的是匿名对象的类型,满足 auto 的推导规则:

  • auto 是值类型推导,它进行退化。如果加上 cv 限定符变成 const auto,那么类型中就带有 cv 限定符
  • auto& 是左值引用类型,它推导出来的就是左值引用。它会捕获 cv 限定符
  • auto&& 是通用引用
  • const auto&&const 右值引用

如果要推导出右值引用的话,可以使用 std::move

从 C++ 20 开始,也可以添加存储类说明符,如 staticthread_local

绑定到数据成员

这里有一个最重要的限制:要求所有的非静态成员都是它自己的,或者它的某一个基类的。如:

struct A {
    int x, y;
};

struct B : A {

};

struct C : B {
    int z;
};

其中,AB 可以做结构化绑定,C 不可以。这涉及到标准内存布局的内容,可以看 一些特殊类类型

还有一些限制:

  • 它必须可以访问到类的所有非静态数据成员

绑定到元组式类型的元素

C++ 提供了元组式绑定

标准库已经为 std::pairstd::tuplestd::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;
}