01 Structured Bindings
结构化绑定可以用一个对象的成员或元素初始化多个变量。比如
某种程度上说,分解了某个对象来初始化其他变量。下面两种初始化语法都是支持的。
当函数返回结构体或数组的时候,结构化绑定非常有用。MyStruct getStruct()
{
return MyStruct{42, "hello"};
}
// id and val name i and s of returned struct
auto [id, val] = getStruct();
// using
if (id > 30)
{
std::cout << val;
}
id
val
是返回的结构体中的 i
s
。
使用结构化绑定,可以提高可读性。
for (const auto &elem : mymap)
{
std::cout << elem.first << ": " << elem.second << '\n';
}
for (const auto &[key, val] : mymap)
{
std::cout << key << ": " << val << '\n';
}
in Detail
Binding to an Anonymous Entity
结构化绑定的背后有一个匿名的对象,变量指向这个匿名对象的元素或成员。
// initialization
auto [u, v] = ms;
auto e = ms;
aliasname u = e.i;
aliasname v = e.s;
// for return values
auto [u, v] = getStruct();
auto e = getStruct();
aliasname u = e.i;
aliasname v = e.s;
u
v
是 ms
对应成员变量的拷贝,不是引用类型,decltype(u)
decltype(v)
分别是 i
s
的类型。
匿名对象 e
的生命周期与结构化绑定生命周期一致,结构化绑定离开作用域,临时对象被销毁。
除非使用引用,否则修改结构化绑定变量的值不影响结构体的成员,反之亦然。
MyStruct ms{42, "hello"};
auto [u, v] = ms;
ms.i = 77;
std::cout << u; // prints 42
u = 99;
std::cout << ms.i; // prints 77
Using Qualifiers
使用 const
和引用修饰作用于匿名对象 e
这个整体。
const auto &[u, v] = ms; // a reference, so that u/v refer to ms.i/ms.s
ms.i = 77; // affects the value of u
std::cout << u; // prints 77
const
的话,可以双向修改。
MyStruct ms{42, "hello"};
auto &[u, v] = ms; // the initialized entity is a reference to ms
ms.i = 77; // affects the value of u
std::cout << u; // prints 77
u = 99; // modifies ms.i
std::cout << ms.i; // prints 99
Qualifiers Do Not Necessarily Apply to the Structured Bindings
下面的 u
和 v
并不是引用类型,只有匿名对象 e
是引用。不过是 const
的,无法修改。因此,u
v
的类型分别是 const int
和 const std::string
。
u
按照 16 字节对齐了,但是 v
并没有。
即使使用 auto
进行结构化绑定,没有类型退化。因为 auto
作用于匿名对象整体,而整体不会类型退化。这与 auto
初始化不同。
struct S
{
const char x[6];
const char y[3];
};
S s1{};
auto [a, b] = s1; // a and b get the exact member types: const char[6]
auto a2 = a; // a2 gets decayed type of a
Move Semantics
移动语义也符合上述规则。
u
v
指向匿名对象,它是 ms
的右值引用。此时 ms
还持有原来的值。
MyStruct ms = {42, "Jim"};
auto &&[v, n] = std::move(ms); // entity is rvalue reference to ms
std::cout << "ms.s: " << ms.s << '\n'; // prints "Jim"
n
进行移动赋值,此时会将 ms.s
移走,这样,被移动的数据处于未定义的状态。
std::string s = std::move(n); // moves ms.s to s
std::cout << "ms.s: " << ms.s << '\n'; // prints unspecified value
std::cout << "n: " << n << '\n'; // prints unspecified value
std::cout << "s: " << s << '\n'; // prints "Jim"
ms
移动的值来初始化,ms
已经不再持有原来的值了。
MyStruct ms = {42, "Jim"};
auto [v, n] = std::move(ms); // new entity with moved-from values from ms
std::cout << "ms.s: " << ms.s << '\n'; // prints unspecified value
std::cout << "n: " << n << '\n'; // prints "Jim"
n
赋予新值,或者移动赋值给其他变量,但是这都与 ms.s
无关了。
std::string s = std::move(n); // moves n to s
n = "Lara";
std::cout << "ms.s: " << ms.s << '\n'; // prints unspecified value
std::cout << "n: " << n << '\n'; // prints "Lara"
std::cout << "s: " << s << '\n'; // prints "Jim"
Where Can Be Used
数据绑定可以用于都是 public
的结构体、C 风格数组和类似元组(tuple-like)的对象。
- 非
static
的成员都是public
的结构体:每个字段可以给一个名字。 - 原生数组:每一个元素可以给给一个名字。
- 类元组:需要提供以下三个 API。任意类型可以通过提供 API 实现数据绑定。
std::tuple_size<type>::value
返回元素的个数std::tuple_element<idx,type>::type
返回第idx
个元素的类型get<idx>()
返回第idx
个元素的值
std::pair<>
, std::tuple<>
, std::array<>
都提供了这些 API,所以可以进行数据绑定。
必须绑定制定个数的元素,不能跳过某个元素,名字也不能重复。下面使用两次 _
这个名字是非法的。
当前还不支持嵌套的结构化绑定。
Structures and Classes
继承的使用会导致无法结构化绑定。所有的数据必须来自同一个类。
struct B
{
int a = 1;
int b = 2;
};
struct D1 : B
{
};
auto [x, y] = D1{}; // OK
struct D2 : B
{
int c = 3;
};
auto [i, j, k] = D2{}; // compile-time ERROR
B
中的 a
b
交换了顺序,那么 x
y
的值也会发生变化。
union
不支持结构化绑定。
原生数组
非常直观,不过有两点需要注意,首先,这里少数使用原始数组会发生按值拷贝的场景。第二,入参由于退化成了指针,无法结构化绑定,不过返回类型可以通过引用指定大小信息,所以可以结构化绑定。
int arr[] = {47, 11};
auto [x, y] = arr; // x and y are the int elems of a copy of arr
auto [z] = arr; // ERROR: number of elements doesn't fit
auto getArr() -> int (&)[2]; // getArr() returns reference to raw int array
auto [x, y] = getArr(); // x and y are the int elems of a copy of the returned array
std::pair
, std::tuple
, and std::array
对 std::array
的结构化绑定亦相当直观。
std::array<int, 4> getArray();
auto [a, b, c, d] = getArray(); // a,b,c,d name the 4 elements of the copied return value
std::array<int, 4> stdarr{1, 2, 3, 4};
auto &[a, b, c, d] = stdarr;
a += 10; // OK: modifies stdarr[0]
const auto &[e, f, g, h] = stdarr;
e += 10; // ERROR: reference to constant object
auto &&[i, j, k, l] = stdarr;
i += 10; // OK: modifies stdarr[0]
auto [m, n, o, p] = stdarr;
m += 10; // OK: but modifies copy of stdarr[0]
// cannot initialize a non-const reference from a temporary object
auto &[a, b, c, d] = getArray(); // ERROR
下面是 std::tuple
的结构化绑定例子。
std::tuple<char, float, std::string> getTuple();
// a,b,c have types and values of returned tuple
// a: char, b: float, c: std::string
auto [a, b, c] = getTuple();
std::pair
可以参考本章开头的例子即可。
由于结构化绑定的例子通常是一起声明的,但是不能一起用,所以无法对这些变量做修改。不过如果被赋的值可以赋值给 std::pair
或 std::tuple
,那么可以使用 std::tie()
一次性更新所有的值。这在处理有多个返回值的循环的时候非常有用。
std::tuple<char, float, std::string> getTuple();
auto [a, b, c] = getTuple(); // a,b,c have types and values of returned tuple
std::tie(a, b, c) = getTuple(); // a,b,c get values of next returned tuple
std::boyer_moore_searcher bmsearch{sub.begin(), sub.end()};
for (auto [beg, end] = bmsearch(text.begin(), text.end());
beg != text.end();
std::tie(beg, end) = bmsearch(end, text.end()))
{
//
}
Providing a Tuple-Like API for Structured Bindings
与 std::pair
std::tuple
std::array
类似,我们可以为自定义的类型提供类元组 API,使得可以进行结构化绑定的操作。
首先看下只读的实现。假定需要给下面的类提供类元组 API。
#include <string>
#include <utility> // for std::move()
class Customer
{
private:
std::string first;
std::string last;
long val;
public:
Customer(std::string f, std::string l, long v)
: first{std::move(f)}, last{std::move(l)}, val{v}
{
}
std::string getFirst() const
{
return first;
}
std::string getLast() const
{
return last;
}
long getValue() const
{
return val;
}
};
那么类元组 API 实现如下。
#include "customer1.hpp"
#include <utility> // for tuple-like API
// provide a tuple-like API for class Customer for structured bindings:
// std::tuple_size
template <>
struct std::tuple_size<Customer>
{
static constexpr int value = 3; // we have 3 attributes
};
// std::tuple_element
// a full specialization
template <>
struct std::tuple_element<2, Customer>
{
using type = long; // last attribute is a long
};
// a partial specialization
template <std::size_t Idx>
struct std::tuple_element<Idx, Customer>
{
using type = std::string; // the other attributes are strings
};
// define specific getters:
// a primary function template declaration
template <std::size_t>
auto get(const Customer &c);
// full specializations
template <>
auto get<0>(const Customer &c) { return c.getFirst(); }
template <>
auto get<1>(const Customer &c) { return c.getLast(); }
template <>
auto get<2>(const Customer &c) { return c.getValue(); }
完全特化的函数签名需要与声明一致,包括返回类型,所以如下代码无法通过编译。
template <std::size_t>
auto get(const Customer &c);
template <>
std::string get<0>(const Customer &c) { return c.getFirst(); }
template <>
std::string get<1>(const Customer &c) { return c.getLast(); }
template <>
long get<2>(const Customer &c) { return c.getValue(); }
if constexpr
上述模板可以改写为
template <std::size_t I>
auto get(const Customer &c)
{
static_assert(I < 3);
if constexpr (I == 0)
{
return c.getFirst();
}
else if constexpr (I == 1)
{
return c.getLast();
}
else
{ // I == 2
return c.getValue();
}
}
#include "structbind1.hpp"
#include <iostream>
int main()
{
Customer c{"Tim", "Starr", 42};
auto [f, l, v] = c;
std::cout << "f/l/v: " << f << ' ' << l << ' ' << v << '\n';
// modify structured bindings:
std::string s{std::move(f)};
l = "Waters";
v += 10;
std::cout << "f/l/v: " << f << ' ' << l << ' ' << v << '\n';
std::cout << "c: " << c.getFirst() << ' '
<< c.getLast() << ' ' << c.getValue() << '\n';
std::cout << "s: " << s << '\n';
}
get<>
函数初始化 f
l
v
。当输出的时候,不会再次调用 get<>
函数了。
同时,由于返回的是值,拷贝了一次,所以新的变量与 c
完全无关,因此秀爱 c
并不会影响 f
l
v
,反之亦然。
整个代码输出如下
我们也可以对 std::vector<Customer>
进行迭代。
std::vector<Customer> coll;
for (const auto &[first, last, val] : coll)
{
std::cout << first << ' ' << last << ": " << val << '\n';
}
const auto&
,所以不会拷贝 Customer
,但是由于 first
last
按值返回,所以会拷贝字符串。当每一次迭代结束,拷贝的字符串会销毁。
使用 decltype
推导是变量自身的类型,与匿名对象修饰无关,因此 decltype(first)
decltype(last)
是 const std::string
而不是引用。
现在分析如何实现提供可写的类元组 API。首先,自定义类需要提供可以修改成员变量的 API,即提供返回 const
和非 const
引用的 API。
#include <string>
#include <utility> // for std::move()
class Customer
{
private:
std::string first;
std::string last;
long val;
public:
Customer(std::string f, std::string l, long v)
: first{std::move(f)}, last{std::move(l)}, val{v}
{
}
const std::string &firstname() const
{
return first;
}
std::string &firstname()
{
return first;
}
const std::string &lastname() const
{
return last;
}
std::string &lastname()
{
return last;
}
long value() const
{
return val;
}
long &value()
{
return val;
}
};
get<>
有三个重载,分别用于处理 const
对象、非 const
对象和可移动对象。另外,为了让接口返回引用,返回类型需要写成 decltype(auto)
,否则会丢掉引用修饰。
#include "customer2.hpp"
#include <utility> // for tuple-like API
// provide a tuple-like API for class Customer for structured bindings:
template <>
struct std::tuple_size<Customer>
{
static constexpr int value = 3; // we have 3 attributes
};
template <>
struct std::tuple_element<2, Customer>
{
using type = long; // last attribute is a long
};
template <std::size_t Idx>
struct std::tuple_element<Idx, Customer>
{
using type = std::string; // the other attributes are strings
};
// define specific getters:
template <std::size_t I>
decltype(auto) get(Customer &c)
{
static_assert(I < 3);
if constexpr (I == 0)
{
return c.firstname();
}
else if constexpr (I == 1)
{
return c.lastname();
}
else
{ // I == 2
return c.value();
}
}
template <std::size_t I>
decltype(auto) get(const Customer &c)
{
static_assert(I < 3);
if constexpr (I == 0)
{
return c.firstname();
}
else if constexpr (I == 1)
{
return c.lastname();
}
else
{ // I == 2
return c.value();
}
}
template <std::size_t I>
decltype(auto) get(Customer &&c)
{
static_assert(I < 3);
if constexpr (I == 0)
{
return std::move(c.firstname());
}
else if constexpr (I == 1)
{
return std::move(c.lastname());
}
else
{ // I == 2
return c.value();
}
}
下面是对使用结构化绑定的代码,现在可以真的修改成员变量的值了。
#include "structbind2.hpp"
#include <iostream>
int main()
{
Customer c{"Tim", "Starr", 42};
auto [f, l, v] = c;
std::cout << "f/l/v: " << f << ' ' << l << ' ' << v << '\n';
// modify structured bindings via references:
auto &&[f2, l2, v2] = c;
std::string s{std::move(f2)};
f2 = "Ringo";
v2 += 10;
std::cout << "f2/l2/v2: " << f2 << ' ' << l2 << ' ' << v2 << '\n';
std::cout << "c: " << c.firstname() << ' '
<< c.lastname() << ' ' << c.value() << '\n';
std::cout << "s: " << s << '\n';
}