1.1 更详细的阐述结构化绑定
1.1.1 绑定到匿名对象
1.1.2 使用修饰符
1.1.3 修饰符并非修饰结构化绑定
1.1.4 移动语义
1.2 结构化绑定可以在哪使用
1.2.1 结构体和类
1.2.2 原生数组
1.2.3 std::pair,std::tuple和std::array
1.3 为结构化绑定提供类似tuple的API
1.3.1 只读结构化绑定
1.3.2 结构化绑定写操作
结构化绑定:通过对象的元素或成员初始化多个实体。
一个结构体如下:
struct MyStruct {
int i = 0;
std::string s;
};
我们可以将一个MyStruct
对象绑定到两个变量上。
MyStruct ms;
auto [u, v] = ms;
在这里,u和v就是所谓的结构化绑定。将结构体的成员分解初始化了u、v变量。
结构化绑定对于返回结构或数组的函数特别有用。
例如:返回一个结构体函数
MyStruct getStruct() {
return MyStruct{42, "hello"};
};
此时我们可以直接通过结构化绑定拿到结构体的成员。
auto [id, val] = getStruct();
另一个比较有意思的使用地方在于可以增加代码可读性,例如输出map中所有的键值对。map如下:
std::map mymap = {{1, "el"}, {3, "tom"}, {4, "nic"}};
结构化绑定之前我们遍历给定的是无意义的elem。
for (const auto& elem : mymap) {
std::cout << elem.first << ": " << elem.second << std::endl;
}
有了结构体绑定之后,我们只需要[key, val]
。
for (const auto& [key, val] : mymap) {
std::cout << key << ": " << val << std::endl;
}
在这里我们可以清晰的看出结构化绑定的语义。
结构化绑定中匿名变量非常重要,结构化绑定引入的新名字的都是指代的这个匿名遍历的成员、元素。
先考虑如下绑定到匿名变量:
auto [u, v] = ms;
可以看作:
auto e = ms; // e是匿名变量
aliasname u = e.i;
aliasname v = e.s;
这里需要注意一点的是,u
与v
不是e.i
与e.s
的引用。它们只是这两个成员的别名。其中e是匿名变量,所以我们不能直接访问e.i
与e.s
,但是可以访问u
与v
,例如:
std::cout << u << ": " << v << std::endl;
这个例子输出e.i
与e.s
的值,它们是ms.i
和ms.s
的一份拷贝。
匿名变量e
的生命周期同结构化绑定的存活时间一样,当结构化绑定离开作用域时,e
会同时析构。
在这里我们还需要理解引用,通过结构化绑定,利用前面的例子:
MyStruct ms{42, "hello"};
auto [u, v] = ms;
ms.i = 100;
std::cout << u << ": " << ms.i << std::endl; // 42: 100
u = 101;
std::cout << u << ": " << ms.i << std::endl; // 101: 100
第一次输出42: 100,u是ms.i的一份拷贝,而修改了ms.i并不会影响u,因此输出是42: 100。第二次输出101:100,直接修改了u,同样u是ms.i的一份拷贝,修改了不会影响ms.i,因此输出是101: 100。现在我们换成引用:
auto& [u1, v1] = ms;
ms.i = 100;
std::cout << u1 << ": " << ms.i << std::endl;
u1 = 101;
std::cout << u1 << ": " << ms.i << std::endl;
此时第一次输出是100: 100 第二次输出是101: 101
同理,对返回值使用结构化绑定,上面规则一样成立。
auto [u, v] = getStruct();
转换为:
auto e = getStruct();
aliasname u = e.i;
aliasname v = e.s;
可以看到结构化绑定是绑定到一个新的对象,由返回值进行初始化,而不是直接绑定到返回值本身。
另外,内存地址和对齐也是存在。如果成员有对齐,结构化绑定也会存在对齐,例如:
auto [u, v] = ms;
assert(&((MyStruct*)&u)->s == &v);
结构化绑定中我们可以使用一些修饰符,如:const
和&
。
这里的修饰符是加在匿名变量上。
const auto& [u, v] = ms;
等价于
const auto& e = ms;
aliasname u = e.i;
aliasname v = e.s;
引用对结果的影响可以看前面绑定到匿名对象的例子。
修饰符修饰的是匿名变量,而不是结构化绑定。尽管在结构化绑定的时候会使用到auto,但是结构化绑定的类型不会退化(数组转指针、修饰符被忽略等)。例如:
struct S {
const char x[6];
const char y[3];
};
调用:
S s1;
auto [a, b] = s1;
我们查看a与b的类型发现,还是const char[6]与const char[3],说明并没有发生退化为指针,原因是修饰符并非修饰结构化绑定而是修饰初始化结构体绑定的对象, 这一点和使用auto初始化新对象很不一样,它会发生类型退化。
auto a2 = a;
退化为const char*
。
结构化绑定也支持移动语义,例如:
MyStruct ms = {42, "Jim"};
auto&& [u, v] = std::move(ms);
等价于:
MyStruct ms = {42, "Jim"};
auto&& e = std::move(ms);
aliasname u = e.i;
aliasname v = e.s;
结构化绑定u和v指向匿名变量中的成员,该匿名变量是ms的右值引用。ms仍然持有它的值:
std::cout << ms.i << ": " << ms.s << std::endl;
此时还可以移动赋值u与v,例如v与ms.s关联。
std::string s = std::move(v);
std::cout << ms.s << std::endl;
std::cout << v << std::endl;
std::cout << s << std::endl;
此时ms.s与v输出的是未指定的值,s输出的是Jim。移动后的对象(如前面v)的状态是有效的,只是包含了未指定的值(unspecified value)。因此,输出它的值是没有问题的,但是不能断言输出的东西一定是什么。
这一点和直接移动ms的值给匿名变量稍有不同:
MyStruct ms1 = {42, "Jim"};
auto [u1, v1] = std::move(ms1); // new entity with moved-from values from ms
std::cout << "ms1.s: " << ms1.s << std::endl; // prints unspecified value
std::cout << "v1: " << v1 << std::endl; // prints "Jim"
仍然可以移动n并赋值,或者用它赋予一个新的值,但是不会影响ms.s:
std::string s = std::move(v1); // moves v1 to s
v1 = "Lara";
std::cout << "ms.s: " << ms.s << std::endl; // prints unspecified value
std::cout << "v1: " << v1 << std::endl; // prints "Lara"
std::cout << "s: " << s << std::endl; // prints "Jim"
原则上,结构化绑定可以应用于public成员、C-style数组、类似tuple对象。具体如下:
public非静态成员 结构体或类中的非静态成员是public
原生数组 绑定到每个元素
任何类型,使用类似tuple的API
std::tuple_size
返回元素数量std::tuple_element
返回第idx个元素的类型 一个全局或成员函数get
返回idx个元素的值
使用的时候需要元素或数据成员的数量必须匹配结构化绑定的名字的个数。不能跳过任何一个元素,也不能使用同一个名字多次,但是可以使用_
跳过当前元素。
切记不要在同一命名空间使用多次_
。
auto [_, v1] = getStruct(); // OK
auto [_, v2] = getStruct(); // ERROR: name _ already used
嵌套对象分解是不支持的,诸如:
auto [a,(b,c)] = (3,(4,2));
结构化绑定不适用于继承,所有非静态数据成员必须在同一个类。
换句话说,这些数据成员要么全是基类,要么全是子类。
例如:
struct B {
int a{1};
int b{2};
};
struct D1 : public B {};
struct D2 : public B {
int c{3};
};
struct B1 {};
struct D3 : public B1 {
int c{3};
};
int main() {
auto [x, y] = D1{}; // 全部来自于基类
auto [z] = D3{}; // 全部来自于子类
auto [i, j, k] = D2{}; // 编译时错误 无法分解
return 0;
}
数组长度已知的情况下,可以结构化绑定到多个变量上。
C++允许我们返回带长度的数组引用:
auto getArr() -> int(&)[2] {
}
auto [x, y] = getArr();
同样可以对std::array进行结构化绑定。
结构化绑定可扩展,可以为任何类型添加结构化绑定,标准库中使用到的有std::pair,std::tuple,std::array
1.std::array
std::array getArray();
auto [i,j,k,l] = getArray();
同样,如果需要更改原生数组里面的值可以改为:
std::array stdarr{1, 2, 3, 4};
auto& [i, j, k, l] = stdarr;
i += 10;
2.std::tuple
std::tuple getTuple() {
std::tuple tp('1', 6.6, "hello");
return tp;
}
auto [a, b, c] = getTuple();
3.std::pair
处理关联/无序容器的insert()调用的返回值,使用结构化绑定使代码可读性更强,可以更加清晰的表达自己的一图,而不是依赖与std::pair
的first与second。
C++ 17之前写法:
std::map coll;
auto ret = coll.insert({"new",42});
if (!ret.second) {
// if insert failed, handle error using iterator pos:
}
C++17之后:
std::map coll;
auto [pos,ok] = coll.insert({"new",42});
if (!ok) {
// if insert failed, handle error using iterator pos:
}
可以看到C++17提供了一种表达力更强的带初始化的if。
4.为pair和tuple的结构化绑定赋值
声明了结构化绑定之后,通常不能一次性修改全部结构化绑定,因为结构化绑定是一次性声明所有。如果重新赋值是std::pair<>
或std::tuple<>
,可以使用std::tie()
;
std::tuple getTuple() { }
auto [a, b, c] = getTuple();
std::tie(a, b, c) = getTuple(); // next returned tuple
尤其适用于在循环中不停地赋值例如下面查找例子:
在text中查找sub
std::string text =
"Lorem ipsum dolor sit amet, consectetur adipiscing elit,"
" sed do eiusmod tempor incididunt ut labore et dolore magna aliqua";
std::string sub = "dolor";
std::boyer_moore_searcher bm{sub.begin(), sub.end()};
for (auto [beg, end] = bm(text.begin(), text.end()); beg != text.end();
std::tie(beg, end) = bm(end, text.end())) {
// find sub in text
std::cout << "find sub string in text string, start pos is "
<< beg - text.begin() << std::endl;
}
只要我们的类型实现了类似tuple的API,那么就可以针对该类型使用结构化绑定,这样便可以从std::pair<>
,std::tuple<>
,和std::array<>
推广到任意类型。
首先,定义一个类:
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; }
};
提供类似tuple的API:
template <>
struct std::tuple_size {
static constexpr int value = 3;
};
template <>
struct std::tuple_element<2, Customer> {
using type = long;
};
template
struct std::tuple_element {
using type = std::string;
};
template
auto get(const Customer& c);
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();
}
对get进行封装,使用编译时if特性:
template
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();
}
}
有了这些API,结构化绑定就可以在main函数中使用。
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';
return 0;
}
输出:
f/l/v: Tim Starr 42
f/l/v: Waters 52
c: Tim Starr 42
s: Tim
在上述Customer中对成员函数进行修改:
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; }
};
要支持读写,需要对常量引用和非常量引用准备getter重载:分别支持非常量对象、常量对象、可移动对象,为了返回引用,应该使用decltype(auto)
// define specific getters:
template
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
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
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();
}
}
std::tuple_size<>
与std::tuple_element<>
不变。
// provide a tuple-like API for class Customer for structured bindings:
template <>
struct std::tuple_size {
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
struct std::tuple_element {
using type = std::string; // the other attributes are strings
};
同样,getter方法也可以不用编译时if,如下:
// 支持非const对象
template
decltype(auto) get(Customer& c);
template <>
decltype(auto) get<0>(Customer& c) {
return c.firstname();
}
template <>
decltype(auto) get<1>(Customer& c) {
return c.lastname();
}
template <>
decltype(auto) get<2>(Customer& c) {
return c.value();
}
// 支持const对象
template
auto get(const Customer& c);
template <>
auto get<0>(const Customer& c) {
return c.firstname();
}
template <>
auto get<1>(const Customer& c) {
return c.lastname();
}
template <>
auto get<2>(const Customer& c) {
return c.value();
}
// 支持移动对象
template
decltype(auto) get(Customer&& c);
template <>
decltype(auto) get<0>(Customer&& c) {
return std::move(c.firstname());
}
template <>
decltype(auto) get<1>(Customer&& c) {
return std::move(c.lastname());
}
template <>
decltype(auto) get<2>(Customer&& c) {
return std::move(c.value());
}
调用:
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';
return 0;
}
输出:
f/l/v: Tim Starr 42
f2/l2/v2: Ringo Starr 52
c: Ringo Starr 52
s: Ti
学习自Cpp17TheCompleteGuide