C++ 结构化绑定

在 C++ 中,“结构化绑定(structured binding)” 是 C++17 引入的一项实用语法,可以把一个复合对象里的多个成员一次性拆解出来,分别赋给多个独立的变量。

平时我们读取复合对象里的数据时,反复写 .first、.second 或 std::get<0>() 不太直观。使用结构化绑定之后,就像 “拆包裹” 一样,一行代码就能把里面的各个成员取出来并起好名字,如下图所示。

传统成员访问与结构化绑定拆包方式对比

C++ 结构化绑定的使用

在 C++ 中,结构化绑定的写法非常简洁:我们在 auto 关键字后面跟上一对方括号 [],并在方括号里依次写出各个新变量的名字即可。

语法:

auto [变量名1, 变量名2, ...] = 复合对象;

说明:

这里的方括号 [] 不是数组下标,而是用来接收各个成员的变量列表。需要注意的是,方括号前面必须写 auto 关键字,由编译器自动推导每个成员的类型,不能手动写成 int [a, b]。

在实际开发中,结构化绑定支持直接拆解以下 3 类常见对象:

  • 标准库中的 std::pair 与 std::tuple。
  • 已知长度的原生数组。
  • 非静态成员变量全部为 public 公开的结构体或类。

示例 1:拆解 pair 与 tuple

#include <iostream>
#include <string>
#include <tuple>
#include <utility>

int main() {
    std::pair<std::string, int> stu = {"Jack", 92};
    auto [name, score] = stu;
    std::cout << name << ":" << score << '\n';

    std::tuple<int, int, int> color = {255, 128, 64};
    auto [r, g, b] = color;
    std::cout << "RGB:" << r << ", " << g << ", " << b << '\n';
    return 0;
}

运行结果如下。

Jack:92
RGB:255, 128, 64

分析:

auto [name, score] = stu; 这句代码表示把 stu 中的 first 和 second 依次拆给变量 name 和 score。后面的 auto [r, g, b] = color; 则是把 color 里的 3 个整数按顺序赋给 r、g 和 b。

对于上面例子中的 stu 来说,下面 2 种写法是等价的。

// 写法 1
std::string name = stu.first;
int score = stu.second;

// 写法 2(推荐)
auto [name, score] = stu;

可以看到,写法 2 省去了多行重复的成员访问代码,可读性更好,在实际开发中我们更推荐使用写法 2。

除了 std::pair 和 std::tuple 之外,如果我们要拆解原生数组和自定义结构体,又应该怎么写呢?下面我们再来看一个例子。

示例 2:拆解数组与结构体

#include <iostream>

struct Point {
    int x;
    int y;
};

int main() {
    int arr[2] = {10, 20};
    auto [a, b] = arr;
    std::cout << "a = " << a << ", b = " << b << '\n';

    Point pt = {30, 40};
    auto [px, py] = pt;
    std::cout << "px = " << px << ", py = " << py << '\n';
    return 0;
}

运行结果如下。

a = 10, b = 20
px = 30, py = 40

分析:

在这个例子中,auto [a, b] = arr; 按下标顺序把数组的两个元素拆给了 a 和 b。auto [px, py] = pt; 则是按照结构体成员定义的先后顺序,把 pt.x 和 pt.y 依次拆给了 px 和 py。

由于 struct 的成员默认就是 public 公开的,因此我们可以直接拆解。如果换作包含 private 私有成员的 class,在类的外部就不能直接拆解了。

另外小伙伴们要注意,方括号里的变量个数必须和成员总个数一致,不能只拆解其中一部分。比如下面的数组有 3 个元素,只写两个变量接收就会在编译时报错。

int arr[3] = {10, 20, 30};
auto [x, y] = arr;    // 变量个数不匹配

按值绑定与引用绑定

拆出新变量之后,如果我们修改这些变量的值,原来的复合对象会不会跟着改变呢?这取决于我们在 auto 后面有没有加上引用符号(&)。

语法:

auto [变量名1, 变量名2, ...] = 复合对象;
auto& [变量名1, 变量名2, ...] = 复合对象;
const auto& [变量名1, 变量名2, ...] = 复合对象;

说明:

这 3 种绑定方式的区别如下:

  • auto [a, b]:按值绑定。编译器会先把原对象复制一份副本,我们修改 a 或 b 不会影响原对象。
  • auto& [a, b]:引用绑定。拆出的 a 和 b 直接成为原对象成员的引用别名,修改它们就相当于直接修改原对象。
  • const auto& [a, b]:只读引用绑定。它既省去了复制整个对象的开销,又不允许通过 a 和 b 修改原对象。

在实际开发中,如果我们只是读取数据,优先使用 const auto& 即可,遇到包含字符串等较大成员的对象时能省去复制开销。需要修改原对象成员时使用 auto&,确实需要独立副本时再使用 auto。

示例 3:对比按值与引用绑定

#include <iostream>
#include <string>

struct Student {
    std::string name;
    int score;
};

int main() {
    Student stu = {"Lucy", 90};

    auto [valName, valScore] = stu;
    valScore = 100;
    std::cout << "按值绑定修改后,原成绩:" << stu.score << '\n';

    auto& [refName, refScore] = stu;
    refScore = 100;
    std::cout << "引用绑定修改后,原成绩:" << stu.score << '\n';
    return 0;
}

运行结果如下。

按值绑定修改后,原成绩:90
引用绑定修改后,原成绩:100

分析:

当我们第 1 次使用 auto [valName, valScore] = stu; 时,采用的是按值绑定。此时把 valScore 改成 100 只是修改了复制出来的副本,原有的 stu.score 依然是 90。

当我们第 2 次使用 auto& [refName, refScore] = stu; 时,refScore 直接成了 stu.score 的引用别名。因此把 refScore 改成 100 之后,原有的 stu.score 也会跟着变成 100。

结构化绑定的常见应用

在实际项目开发中,结构化绑定最常用于 “接收函数返回的多个结果” 以及 “遍历键值对容器” 这两个场景。

1. 接收函数返回的多个值

在 C++ 中,普通函数一次只能返回一个值。比如计算两数相除时,我们希望同时拿到 “商” 和 “余数”,就可以让函数返回一个 std::pair 或自定义结构体,再在调用处通过结构化绑定拆成两个变量接收。

示例 4:接收函数多个返回值

#include <iostream>
#include <utility>

std::pair<int, int> divide(int a, int b) {
    return {a / b, a % b};
}

int main() {
    auto [quot, rem] = divide(17, 5);
    std::cout << "商:" << quot << '\n';
    std::cout << "余数:" << rem << '\n';
    return 0;
}

运行结果如下。

商:3
余数:2

分析:

在 divide() 函数中,return {a / b, a % b}; 会根据返回类型自动构造一个包含商 3 和余数 2 的 std::pair<int, int>。回到 main() 函数后,我们通过 auto [quot, rem] 直接就拿到了商和余数,不用再去写 .first 和 .second。

2. 遍历 map 容器

配合范围 for 循环使用时,结构化绑定还能大大简化键值对容器的遍历代码。因为 std::map 里的每一个元素都是一个 pair,我们可以在循环头里直接把 “键” 和 “值” 拆成两个名字清晰的变量。

示例 5:遍历并修改 map 元素

#include <iostream>
#include <map>
#include <string>

int main() {
    std::map<std::string, int> scores = {
        {"Jack", 90},
        {"Lucy", 95},
        {"Tony", 88}
    };

    // 修改每个学生的成绩
    for (auto& [name, score] : scores) {
        score += 5;
    }

    // 只读遍历并打印结果
    for (const auto& [name, score] : scores) {
        std::cout << name << ":" << score << '\n';
    }
    return 0;
}

运行结果如下。

Jack:95
Lucy:100
Tony:93

分析:

从代码可以看到,第 1 个循环使用 auto& [name, score] 引用绑定,直接给每个学生的成绩加上了 5 分。第 2 个循环只是读取并打印数据。平时不需要修改元素时,我们优先写成 const auto& [name, score] 即可,既能省去复制字符串的开销,又能防止误改数据。

注意 在遍历 std::map 时,每个元素的键(key)默认带有 const 只读限制。如果直接修改键的值会破坏内部的排序结构,因此即使写成 auto& [name, score],小伙伴们也只能修改值 score,不能修改键 name。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号