首页 > 编程笔记 > C++笔记 阅读:71

C++结构化绑定的用法(非常详细)

函数返回多个值的情况非常常见,但是 C++ 中没有一流的解决方案可以直接实现这一点。开发人员必须做出选择:
通过引用函数形参返回多个值;
定义一个包含多值的结构,或者返回 std::pair 或 std::tuple。

前两个使用命名变量,它们的优点是清楚地指明返回值的含义,缺点是必须显式地定义它们。std::pair 的成员为 first 和 second,而 std::tuple 的匿名成员只能通过函数调用访问,但可以使用 std::tie() 复制到命名变量。

不过,这些解决方案都不是很理想。

C++17 将 std::tie() 的语义扩展为一级核心语言特性,支持将元组的值解包到命名变量中。这个特性称为结构化绑定。

C++结构化绑定的用法

要支持 C++17 的编译器从函数返回多个值,应该执行以下操作:

1) 使用 std::tuple 作为返回类型:
std::tuple<int, std::string, double> find()
{
    return std::make_tuple(1, "marius", 1234.5);
}

2)使用结构化绑定将元组的值解包到命名对象中:
auto [id, name, score] = find();

3)使用分解声明将返回值绑定到 if 语句或 switch 语句中的变量:
if (auto [id, name, score] = find(); score > 1000)
{
    std::cout << name << '\n';
}

C++结构化绑定的工作原理

结构化绑定是一种语言特性,它的工作原理与 std::tie() 类似,只不过我们不必为每个需要用 std::tie() 显式解包的值定义命名变量。

在结构化绑定中,我们使用 auto 标识符在单个定义中定义所有命名变量,以便编译器可以推断每个变量的正确类型。

为了举例说明这一点,我们考虑向 std::map 中插入项的情况。insert() 方法返回一个 std::pair,其中包含已插入元素或阻止插入的元素的迭代器,以及一个指示插入是否成功的布尔值。
std::map<int, std::string> m;

auto result = m.insert({ 1, "one" });
std::cout << "inserted = " << result.second << '\n'
          << "value = " << result.first->second << '\n';
上面的代码非常明确,但是使用 second 或 first->second 使代码很难阅读,因为你需要不断地弄清楚它们的代表什么。

使用 std::tie 可以使上述代码更具可读性,它将元组解包为单个对象(并与 std::pair 一起使用,因为 std::tuple 有一个来自 std::pair 的转换赋值):
std::map<int, std::string> m;
std::map<int, std::string>::iterator it;
bool inserted;

std::tie(it, inserted) = m.insert({ 1, "one" });
std::cout << "inserted = " << inserted << '\n'
          << "value = " << it->second << '\n';

std::tie(it, inserted) = m.insert({ 1, "two" });
std::cout << "inserted = " << inserted << '\n'
          << "value = " << it->second << '\n';
代码并不一定更简单,因为它需要提前定义解包到的对象。类似地,元素越多,需要定义的对象就越多,但是使用命名对象使代码更容易阅读。

C++17 的结构化绑定将元组元素解包到命名对象中,该特性非常重要。它不需要使用 std::tie(),并且对象在声明时被初始化:
std::map<int, std::string> m;
{
    auto [it, inserted] = m.insert({ 1, "one" });
    std::cout << "inserted = " << inserted << '\n'
              << "value = " << it->second << '\n';
}
{
    auto [it, inserted] = m.insert({ 1, "two" });
    std::cout << "inserted = " << inserted << '\n'
              << "value = " << it->second << '\n';
}
在前面的例子中,使用多个块是必要的,因为变量不能在同一个块中重新声明,结构化绑定意味着使用 auto 标识符进行声明。因此,如果需要像前面的示例那样进行多个调用,并使用结构化绑定,则必须使用不同的变量名或多个块。

另一种选择是不使用结构化绑定并使用 std::tie(),因为它可以用相同的变量调用多次,所以只需声明它们一次。

在 C++17 中,也可以在 if 和 switch 语句中以 if(init; condition) 和 switch(init;condition) 这种形式声明变量。这可以与结构化绑定结合,以生成更简单的代码。让我们看一个例子:
if(auto [it, inserted] = m.insert({ 1, "two" }); inserted)
{
    std::cout << it->second << '\n';
}
在前面的代码片段中,我们尝试向 map 中插入一个新值,结果被解包到两个变量:it 和 inserted,它们在初始化部分的 if 语句的作用域中定义。我们根据 inserted 变量的值计算 if 语句的条件。

C++结构化绑定的更多用法

虽然我们关注的是将名称绑定到元组的元素,但结构化绑定可以在更广泛的范围内使用,因为它们还支持绑定到数组元素或类的数据成员。

如果想绑定到数组的元素,则必须为数组的每个元素提供一个名称,否则声明格式不正确。下面是一个绑定到数组元素的例子:
int arr[] = { 1,2 };
auto [a, b] = arr;

auto& [x, y] = arr;

arr[0] += 10;
arr[1] += 10;

std::cout << arr[0] << ' ' << arr[1] << '\n';    // 11 12
std::cout << a << ' ' << b << '\n';            // 1 2
std::cout << x << ' ' << y << '\n';            // 11 12
在本例中,arr 是一个包含两个元素的数组。我们首先将 a 和 b 绑定到它的元素,然后将 x 和 y 引用绑定到它的元素。

对数组元素所做的更改对变量 a 和 b 是不可见的,但对 x 和 y 引用是可见的,因为在执行第一次绑定时创建了数组的副本并将 a 和 b 绑定到了该副本的元素。

正如我们已经提到的,还可以绑定到类的数据成员,适用的限制如下:
标识符的绑定是按照数据成员声明的顺序进行的,其中可以包括位字段。下面是一个例子:
struct foo
{
    int id;
    std::string name;
};

foo f{ 42, "john" };

auto [i, n] = f;
auto& [ri, rn] = f;

f.id = 43;

std::cout << f.id << ' ' << f.name << '\n';    // 43 john
std::cout << i << ' ' << n << '\n';          // 42 john
std::cout << ri << ' ' << rn << '\n';        // 43 john
同样,对 foo 对象的更改对变量 i 和 n 是不可见的,但对 ri 和 rn 是可见的,这是因为结构化绑定中的每个标识符都成为引用类数据成员的左值的名称(就像对数组那样,它引用数组的一个元素)。但是,标识符的引用类型指向对应的数据成员(或数组元素)。

C++20 标准对结构化绑定进行了一系列改进,包括:
这些变化使我们能够编写以下内容:
foo f{ 42, "john" };
auto [i, n] = f;
auto l1 = [i] {std::cout << i; };
auto l2 = [=] {std::cout << i; };
auto l3 = [&i] {std::cout << i; };
auto l4 = [&] {std::cout << i; };
这些示例展示了在 C++20 的 lambda 中捕获结构化绑定的各种方法。

相关文章