|
| 1 | +## C++ 17 结构化绑定 |
| 2 | + |
| 3 | +stl 的 map 容器很多读者应该都很熟悉,map 容器提供了一个 **insert** 方法,我们用该方法向 map 中插入元素,但是应该很少有人记得 **insert** 方法的返回值是什么类型,让我们来看一下 C++98/03 提供的 **insert** 方法的签名: |
| 4 | + |
| 5 | +``` |
| 6 | +std::pair<iterator,bool> insert( const value_type& value ); |
| 7 | +``` |
| 8 | + |
| 9 | +这里我们仅关心其返回值,这个返回值是一个 **std::pair** 类型,由于 map 中的元素的 key 不允许重复,所以如果 insert 方法调用成功,T1 是被成功插入到 map 中的元素的迭代器,T2 的类型为 bool,此时其值为 true(表示插入成功);如果 insert 由于 key 重复,T1 是造成 insert 插入失败、已经存在于 map 中的元素的迭代器,此时 T2 的值为 false(表示插入失败)。 |
| 10 | + |
| 11 | +在 C++98/03 标准中我们可以使用 **std::pair** 的 **first** 和 **second** 属性来分别引用 T1 和 T2 的值。如下面的我们熟悉的代码所示: |
| 12 | + |
| 13 | +``` |
| 14 | +#include <iostream> |
| 15 | +#include <string> |
| 16 | +#include <map> |
| 17 | +
|
| 18 | +int main() |
| 19 | +{ |
| 20 | + std::map<std::string, int> cities; |
| 21 | + cities["beijing"] = 0; |
| 22 | + cities["shanghai"] = 1; |
| 23 | + cities["shenzhen"] = 2; |
| 24 | + cities["guangzhou"] = 3; |
| 25 | +
|
| 26 | + //for (const auto& [key, value] : m) |
| 27 | + //{ |
| 28 | + // std::cout << key << ": " << value << std::endl; |
| 29 | + //} |
| 30 | +
|
| 31 | + //这一行在 C++11 之前写法实在太麻烦了, |
| 32 | + //std::pair<std::map<std::string, int>::iterator, int> insertResult = cities.insert(std::pair<std::string, int>("shanghai", 2)); |
| 33 | + //C++ 11中我们写成: |
| 34 | + auto insertResult = cities.insert(std::pair<std::string, int>("shanghai", 2)); |
| 35 | +
|
| 36 | + std::cout << "Is insertion successful ? " << (insertResult.second ? "true" : "false") |
| 37 | + << ", element key: " << insertResult.first->first << ", value: " << insertResult.first->second << std::endl; |
| 38 | +
|
| 39 | + return 0; |
| 40 | +} |
| 41 | +``` |
| 42 | + |
| 43 | +代码 **19** 行实在太啰嗦了,我们使用 auto 关键字让编译器自动推导类型。 |
| 44 | + |
| 45 | +**std::pair** 一般只能表示两个元素,C++11 标准中引入了 **std::tuple** 类型,有了这个类型,我们就可以放任意个元素了,原来需要定义成结构体的 POD 对象我们可以直接使用 **std::tuple** 表示,例如下面表示用户信息的结构体: |
| 46 | + |
| 47 | +``` |
| 48 | +struct UserInfo |
| 49 | +{ |
| 50 | + std::string username; |
| 51 | + std::string password; |
| 52 | + int gender; |
| 53 | + int age; |
| 54 | + std::string address; |
| 55 | +}; |
| 56 | +
|
| 57 | +int main() |
| 58 | +{ |
| 59 | + UserInfo userInfo = { "Tom", "123456", 0, 25, "Pudong Street" }; |
| 60 | + std::string username = userInfo.username; |
| 61 | + std::string password = userInfo.password; |
| 62 | + int gender = userInfo.gender; |
| 63 | + int age = userInfo.age; |
| 64 | + std::string address = userInfo.address; |
| 65 | +
|
| 66 | + return 0; |
| 67 | +} |
| 68 | +``` |
| 69 | + |
| 70 | +我们不再需要定义 struct UserInfo 这样的对象,可以直接使用 **std::tuple** 表示: |
| 71 | + |
| 72 | +``` |
| 73 | +int main() |
| 74 | +{ |
| 75 | + std::tuple<std::string, std::string, int, int, std::string> userInfo("Tom", "123456", 0, 25, "Pudong Street"); |
| 76 | +
|
| 77 | + std::string username = std::get<0>(userInfo); |
| 78 | + std::string password = std::get<1>(userInfo); |
| 79 | + int gender = std::get<2>(userInfo); |
| 80 | + int age = std::get<3>(userInfo); |
| 81 | + std::string address = std::get<4>(userInfo); |
| 82 | +
|
| 83 | + return 0; |
| 84 | +} |
| 85 | +``` |
| 86 | + |
| 87 | +从 **std::tuple** 中获取对应位置的元素,我们使用 **std::get** ,其中 N 是元素的序号(从 0 开始)。 |
| 88 | + |
| 89 | +与定义结构体相比,通过 **std::pair** 的 **first** 和 **second** 还是 **std::tuple** 的 **std::get** 方法来获取元素子属性,这些代码都是非常难以维护的,其根本原因是 **first** 和 **second** 这样的命名不能做到见名知意。 |
| 90 | + |
| 91 | +C++17 引入的**结构化绑定**(Structured Binding )将我们从这类代码中解放出来。**结构化绑定**使用语法如下: |
| 92 | + |
| 93 | +``` |
| 94 | +auto [a, b, c, ...] = expression; |
| 95 | +auto [a, b, c, ...] { expression }; |
| 96 | +auto [a, b, c, ...] ( expression ); |
| 97 | +``` |
| 98 | + |
| 99 | +右边的 **expression** 可以是一个函数调用、花括号表达式或者支持结构化绑定的某个类型的变量。例如: |
| 100 | + |
| 101 | +``` |
| 102 | +//形式1 |
| 103 | +auto [iterator, inserted] = someMap.insert(...); |
| 104 | +//形式2 |
| 105 | +double myArray[3] = { 1.0, 2.0, 3.0 }; |
| 106 | +auto [a, b, c] = myArray; |
| 107 | +//形式3 |
| 108 | +struct Point |
| 109 | +{ |
| 110 | + double x; |
| 111 | + double y; |
| 112 | +}; |
| 113 | +Point myPoint(10.0, 20.0); |
| 114 | +auto [myX, myY] = myPoint; |
| 115 | +``` |
| 116 | + |
| 117 | +这样,我们可以给用于绑定到目标的变量名(语法中的 **a**、**b**、**c**)起一个有意义的名字。 |
| 118 | + |
| 119 | +需要注意的是,绑定名称 **a**、**b**、**c** 是绑定目标的一份拷贝,当绑定类型不是基础数据类型时,如果你的本意不是想要得到绑定目标的副本,为了避免拷贝带来的不必要开销,建议使用引用,如果不需要修改绑定目标建议使用 const 引用。示例如下: |
| 120 | + |
| 121 | +``` |
| 122 | +double myArray[3] = { 1.0, 2.0, 3.0 }; |
| 123 | +auto& [a, b, c] = myArray; |
| 124 | +//形式3 |
| 125 | +struct Point |
| 126 | +{ |
| 127 | + double x; |
| 128 | + double y; |
| 129 | +}; |
| 130 | +Point myPoint(10.0, 20.0); |
| 131 | +const auto& [myX, myY] = myPoint; |
| 132 | +``` |
| 133 | + |
| 134 | +**结构化绑定**(Structured Binding )是 C++17 引入的一个非常好用的语法特性。有了这种语法,在遍历像 map 这样的容器时,我们可以使用更简洁和清晰的代码去遍历这些容器了: |
| 135 | + |
| 136 | +``` |
| 137 | +std::map<std::string, int> cities; |
| 138 | +cities["beijing"] = 0; |
| 139 | +cities["shanghai"] = 1; |
| 140 | +cities["shenzhen"] = 2; |
| 141 | +cities["guangzhou"] = 3; |
| 142 | +
|
| 143 | +for (const auto& [cityName, cityNumber] : cities) |
| 144 | +{ |
| 145 | + std::cout << cityName << ": " << cityNumber << std::endl; |
| 146 | +} |
| 147 | +``` |
| 148 | + |
| 149 | +上述代码中 **cityName** 和 **cityNumber** 可以更好地反映出这个 map 容器的元素内容。 |
| 150 | + |
| 151 | +我们再来看一个例子,某 WebSocket 网络库(https://github.com/uNetworking/uWebSockets)中有如下代码: |
| 152 | + |
| 153 | +``` |
| 154 | +std::pair<int, bool> uncork(const char *src = nullptr, int length = 0, bool optionally = false) { |
| 155 | + LoopData *loopData = getLoopData(); |
| 156 | +
|
| 157 | + if (loopData->corkedSocket == this) { |
| 158 | + loopData->corkedSocket = nullptr; |
| 159 | +
|
| 160 | + if (loopData->corkOffset) { |
| 161 | + /* Corked data is already accounted for via its write call */ |
| 162 | + auto [written, failed] = write(loopData->corkBuffer, loopData->corkOffset, false, length); |
| 163 | + loopData->corkOffset = 0; |
| 164 | +
|
| 165 | + if (failed) { |
| 166 | + /* We do not need to care for buffering here, write does that */ |
| 167 | + return {0, true}; |
| 168 | + } |
| 169 | + } |
| 170 | +
|
| 171 | + /* We should only return with new writes, not things written to cork already */ |
| 172 | + return write(src, length, optionally, 0); |
| 173 | + } else { |
| 174 | + /* We are not even corked! */ |
| 175 | + return {0, false}; |
| 176 | + } |
| 177 | + } |
| 178 | +``` |
| 179 | + |
| 180 | +代码的第 **9** 行 **write** 函数返回类型是 **std::pair**,被绑定到 **[written, failed]** 这两个变量中去。前者在写入成功的情况下表示实际写入的字节数,后者表示是否写入成功。 |
| 181 | + |
| 182 | +``` |
| 183 | +std::pair<int, bool> write(const char *src, int length, bool optionally = false, int nextLength = 0) { |
| 184 | + //具体实现省略... |
| 185 | +} |
| 186 | +``` |
| 187 | + |
| 188 | +**结构化绑定的限制** |
| 189 | + |
| 190 | +结构化绑定不能使用 **constexpr** 修饰或被申明为 static,例如: |
| 191 | + |
| 192 | +``` |
| 193 | +//正常编译 |
| 194 | +auto [first, second] = std::pair<int, int>(1, 2); |
| 195 | +//无法编译通过 |
| 196 | +//constexpr auto [first, second] = std::pair<int, int>(1, 2); |
| 197 | +//无法编译通过 |
| 198 | +//static auto [first, second] = std::pair<int, int>(1, 2); |
| 199 | +``` |
| 200 | + |
| 201 | +注意:有些编译器也不支持在 lamda 表达式捕获列表中使用结构化绑定语法。 |
0 commit comments