C++17 结构化绑定
学习目标
完成本节学习后,应能够:
- 理解结构化绑定的作用和基本语法;
- 使用结构化绑定拆分数组、结构体、
pair和tuple; - 正确区分值绑定、引用绑定和常量引用绑定;
- 在范围
for循环和 STL 容器中使用结构化绑定; - 识别结构化绑定中的常见错误。
什么是结构化绑定
结构化绑定(Structured Bindings)是 C++17 引入的一种语法。
它允许程序员将一个由多个元素组成的对象拆分为若干个变量,从而减少使用 first、second、get<0>() 等写法。
传统写法:
std::pair<int, int> point = {10, 20};
int x = point.first;
int y = point.second;
使用结构化绑定:
std::pair<int, int> point = {10, 20};
auto [x, y] = point;
此时:
x的值为 10;y的值为 20。
基本语法
结构化绑定的一般形式如下:
auto [变量1, 变量2, ...] = 表达式;
也可以使用引用或常量引用:
auto [a, b] = object; // 值绑定
auto& [a, b] = object; // 引用绑定
const auto& [a, b] = object; // 常量引用绑定
变量数量必须匹配
结构化绑定左侧变量的数量必须与对象中可拆分元素的数量一致。
正确示例:
std::pair<int, int> p = {1, 2};
auto [x, y] = p;
错误示例:
std::pair<int, int> p = {1, 2};
auto [x] = p; // 错误:pair 中有两个元素
绑定数组
结构化绑定可以拆分普通数组。
#include <iostream>
using namespace std;
int main() {
int arr[3] = {10, 20, 30};
auto [a, b, c] = arr;
cout << a << ' ' << b << ' ' << c << '\n';
return 0;
}
输出结果:
10 20 30
这里的 a、b、c 是数组元素的副本。
修改这些变量不会改变原数组。
int arr[3] = {10, 20, 30};
auto [a, b, c] = arr;
a = 100;
cout << arr[0] << '\n'; // 输出 10
如果希望通过绑定变量修改原数组,应使用引用绑定:
int arr[3] = {10, 20, 30};
auto& [a, b, c] = arr;
a = 100;
cout << arr[0] << '\n'; // 输出 100
绑定结构体
结构化绑定可以拆分成员均为公开成员的简单结构体。
#include <iostream>
#include <string>
using namespace std;
struct Student {
string name;
int age;
double score;
};
int main() {
Student stu{"Alice", 20, 95.5};
auto [name, age, score] = stu;
cout << name << '\n';
cout << age << '\n';
cout << score << '\n';
return 0;
}
结构体成员的绑定顺序与成员的声明顺序一致:
struct Student {
string name; // 第一个
int age; // 第二个
double score; // 第三个
};
因此:
auto [name, age, score] = stu;
分别对应 stu.name、stu.age 和 stu.score。
例如:
auto [a, b, c] = stu;
其中:
a对应name;b对应age;c对应score。
绑定 std::pair
std::pair 中包含两个元素,分别是 first 和 second。
传统写法:
std::pair<std::string, int> student = {"Tom", 18};
std::string name = student.first;
int age = student.second;
结构化绑定写法:
std::pair<std::string, int> student = {"Tom", 18};
auto [name, age] = student;
完整示例:
#include <iostream>
#include <string>
#include <utility>
using namespace std;
int main() {
pair<string, int> student = {"Tom", 18};
auto [name, age] = student;
cout << "姓名:" << name << '\n';
cout << "年龄:" << age << '\n';
return 0;
}
绑定 std::tuple
std::tuple 可以存储任意数量、任意类型的元素。
传统写法:
std::tuple<std::string, int, double> stu{"Bob", 19, 88.5};
std::string name = std::get<0>(stu);
int age = std::get<1>(stu);
double score = std::get<2>(stu);
结构化绑定写法:
auto [name, age, score] = stu;
完整示例:
#include <iostream>
#include <string>
#include <tuple>
using namespace std;
int main() {
tuple<string, int, double> stu{"Bob", 19, 88.5};
auto [name, age, score] = stu;
cout << name << ' ' << age << ' ' << score << '\n';
return 0;
}
值绑定与引用绑定
结构化绑定中最重要的内容之一,是区分值绑定和引用绑定。
值绑定
语法:
auto [a, b] = object;
此时会根据原对象创建一份新的绑定对象。
通过绑定变量进行修改,通常不会影响原对象。
std::pair<int, int> p = {10, 20};
auto [x, y] = p;
x = 100;
cout << p.first << '\n'; // 输出 10
引用绑定
语法:
auto& [a, b] = object;
绑定变量直接引用原对象中的元素。
std::pair<int, int> p = {10, 20};
auto& [x, y] = p;
x = 100;
cout << p.first << '\n'; // 输出 100
常量引用绑定
语法:
const auto& [a, b] = object;
这种方式不会复制对象,同时禁止通过绑定变量修改元素。
std::pair<int, int> p = {10, 20};
const auto& [x, y] = p;
cout << x << ' ' << y << '\n';
// x = 100; // 错误:x 是只读的
三种方式对比
| 写法 | 是否复制 | 能否修改原对象 |
auto [a,b] | 是 | 否 |
auto& [a,b] | 否 | 是 |
const auto& [a,b] | 否 | 否 |
在函数返回值中使用
结构化绑定非常适合接收包含多个结果的函数返回值。
返回 pair
#include <iostream>
#include <utility>
using namespace std;
pair<int, int> divide(int a, int b) {
return {a / b, a % b};
}
int main() {
auto [quotient, remainder] = divide(17, 5);
cout << "商:" << quotient << '\n';
cout << "余数:" << remainder << '\n';
return 0;
}
输出:
商:3
余数:2
返回 tuple
#include <iostream>
#include <tuple>
using namespace std;
tuple<int, int, double> analyze(int a, int b) {
int sum = a + b;
int product = a * b;
double average = (a + b) / 2.0;
return {sum, product, average};
}
int main() {
auto [sum, product, average] = analyze(4, 6);
cout << sum << '\n';
cout << product << '\n';
cout << average << '\n';
return 0;
}
这种写法比使用多个输出参数更加直观。
在范围 for 循环中使用
结构化绑定经常与范围 for 循环结合使用。
遍历 map
传统写法:
for (const auto& item : mp) {
cout << item.first << ' ' << item.second << '\n';
}
结构化绑定写法:
for (const auto& [key, value] : mp) {
cout << key << ' ' << value << '\n';
}
完整示例:
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main() {
map<string, int> scores{
{"Alice", 95},
{"Bob", 88},
{"Tom", 91}
};
for (const auto& [name, score] : scores) {
cout << name << ": " << score << '\n';
}
return 0;
}
修改 map 中的值
如果需要修改容器中的值,应使用引用:
for (auto& [name, score] : scores) {
score += 5;
}
此时每个学生的分数都会增加 5。
与集合插入操作结合
许多 STL 操作会返回 pair,结构化绑定可以使代码更清晰。
例如,set::insert 会返回:
pair<iterator, bool>
其中:
- 第一个元素是指向对应元素的迭代器;
- 第二个元素表示是否成功插入。
示例:
#include <iostream>
#include <set>
using namespace std;
int main() {
set<int> numbers;
auto [it1, success1] = numbers.insert(10);
auto [it2, success2] = numbers.insert(10);
cout << boolalpha;
cout << success1 << '\n'; // true
cout << success2 << '\n'; // false
return 0;
}
这种写法比下面的传统形式更容易理解:
auto result = numbers.insert(10);
auto it = result.first;
bool success = result.second;
结构化绑定的作用域
结构化绑定变量与普通局部变量一样,受到作用域限制。
int main() {
{
std::pair<int, int> p = {1, 2};
auto [x, y] = p;
std::cout << x << ' ' << y << '\n';
}
// std::cout << x; // 错误:x 已离开作用域
}
结构化绑定变量也不能与当前作用域内已有变量重名。
int x = 10;
std::pair<int, int> p = {1, 2};
auto [x, y] = p; // 错误:x 重复定义
常见错误
忘记启用 C++17
结构化绑定属于 C++17 特性。
使用 GCC 或 Clang 编译时,应指定:
g++ main.cpp -std=c++17 -o main
如果未启用 C++17,可能出现类似错误:
structured bindings only available with -std=c++17
绑定变量数量不匹配
std::tuple<int, int, int> t{1, 2, 3};
auto [a, b] = t; // 错误:tuple 中有三个元素
应改为:
auto [a, b, c] = t;
误以为值绑定会修改原对象
std::pair<int, int> p{1, 2};
auto [x, y] = p;
x = 100;
cout << p.first; // 仍然是 1
需要修改原对象时应使用:
auto& [x, y] = p;
在循环中不必要地复制
下面的代码会在每次循环时复制元素:
for (auto [key, value] : mp) {
cout << key << ' ' << value << '\n';
}
只读遍历时更推荐:
for (const auto& [key, value] : mp) {
cout << key << ' ' << value << '\n';
}
尝试绑定私有成员
结构化绑定不能直接拆分具有不可访问成员的普通类对象。
class Student {
private:
string name;
int age;
};
直接写:
Student stu;
auto [name, age] = stu;
通常会因为成员不可访问而编译失败。
在基础教学中,可以先将结构化绑定用于简单结构体、数组、pair 和 tuple。
综合示例:学生成绩统计
下面的程序使用结构体、vector、结构化绑定和函数返回值,完成学生成绩统计。
#include <iostream>
#include <string>
#include <tuple>
#include <vector>
using namespace std;
struct Student {
string name;
int score;
};
tuple<int, int, double> analyze(const vector<Student>& students) {
int maximum = students.front().score;
int minimum = students.front().score;
int sum = 0;
for (const auto& [name, score] : students) {
if (score > maximum) {
maximum = score;
}
if (score < minimum) {
minimum = score;
}
sum += score;
}
double average =
static_cast<double>(sum) / students.size();
return {maximum, minimum, average};
}
int main() {
vector<Student> students{
{"Alice", 95},
{"Bob", 82},
{"Tom", 91}
};
for (const auto& [name, score] : students) {
cout << name << ": " << score << '\n';
}
auto [maximum, minimum, average] = analyze(students);
cout << "最高分:" << maximum << '\n';
cout << "最低分:" << minimum << '\n';
cout << "平均分:" << average << '\n';
return 0;
}
知识总结
板书式速记
// 1. 基本语法
auto [a, b] = object;
// 2. 修改原对象
auto& [a, b] = object;
// 3. 只读且避免复制
const auto& [a, b] = object;
// 4. 遍历 map
for (const auto& [key, value] : mp) {
// ...
}
// 5. 接收函数的多个返回值
auto [x, y, z] = function();