C++17引入的结构化绑定

一、什么是结构化绑定

我们先来看一段python代码:

def return_values() -> tuple:
    return 1, 2

x, y = return_values()

上述函数返回的是一个元组(tuple), 值是(1, 2), 在函数返回后元组中的元素被自动地分配到了x和y上。

在C++11 标准中,同样引入了元组的概念,通过元组的形式,C++也能同时返回多个值,但书写方式上,却没有python那么优雅,例如:

#include <iostream>
#include <tuple>

std::tuple<int, int> returnValues()
{
    return std::make_tuple(1, 2);
}

int main()
{
    int x = 0, y = 0;
    std::tie(x, y) = returnValues();
    std::cout << "x:" << x << ", y:" << y << std::endl;
    return 0;
}

C++ 的代码之所以要麻烦许多,

  • 其中的一个原因就在于C++必须指定 returnValues() 函数的返回值类型
  • 另外,在调用 returnValues() 函数之前,还需要声明变量 x 和 y,并且使用函数模板 std::tie 将 x 和 y 通过引用绑定到 std::tuple<int&, int&>上。

对于指定返回值类型的问题,在C++14 中可以通过 auto 的新特性来解决:

auto returnValues()
{
    return std::make_tuple(1, 2);
}

想要解决上面第二个问题,就可以通过C++17 中引入的特性:结构化绑定 来解决。所谓结构化绑定就是指将一个或者多个名称绑定到初始化对象中的一个或多个子对象(或者元素)上,相当于给初始化对象的子对象(或元素)起了别名,但要注意的是,这里的别名不同于引用。

#include <iostream>
#include <tuple>

// C++14
auto returnValues14()
{
    return std::make_tuple(1, 2);
}

int main()
{
    // C++17
    auto [a, b] = returnValues14();
    std::cout<< "a:" << a << ", b:" << b << std::endl;
    return 0;
}

这段代码看上去,是不是跟python很接近了!

其中 auto [a, b] = returnValues14(); 就是典型的结构化绑定声明,其中 auto 是类型占位符, [a, b] 是绑定标识符列表,其中 a 和 b 是用于绑定的名称,绑定的目标是函数 returnValues14() 返回结果副本的子对象(或元素)。

需要注意的是,结构化绑定的目标不一定是一个函数的返回结果,事实上,等号的右边可以是任何一个合理的表达式,例如:

#include <iostream>
#include <string>

struct BindTest
{
    int field1 = 1;
    std::string field2 = "hello";
};

int main()
{
    BindTest bt;
    auto [f1, f2] = bt;
    std::cout << "f1:" << f1 << ", f2:" << f2 << std::endl;
    return 0;
}

可以看到,结构化绑定可以直接绑定到结构体上。基于上述运用,我们甚至还能将其使用到基于范围的for循环中:

#include <iostream>
#include <string>
#include <vector>

struct BindTest
{
    int field1 = 1;
    std::string field2 = "hello";
};

int main()
{
    std::vector<BindTest> btVec = { {1, "this is 1"}, {2, "this is 2"}, {3, "this is 3"} };
    for (const auto& [x, y] : btVec)
    {
        std::cout << "x:" << x << ", y:" << y << std::endl;
    }
    return 0;
}

上述基于范围的for循环中,x 和 y 直接绑定到了 btVec 中的结构体子对象上,省去了通过Vector元素访问成员变量field1和field2的步骤

二、深入理解结构化绑定

回过头来看以下语句:

BindTest bt;
const auto [f1, f2] = bt;

这里 f1和f2仅仅只是一个别名,它们并非是源对象的引用。

在结构化绑定中,编译器会根据限定符生成一个等号右边对象的匿名副本,而绑定的对象正是这个副本,而非源对象本身,伪代码形式如下:

BindTest bt;
const auto _anonymous = bt;         // 编译器在这里生成了一个匿名的副本
aliasname f1 = _anonymous.field1;   // 别名f1绑定到该匿名对象的成员
aliasname f2 = _anonymous.field2;

_anonymous的const属性取决于 auto 本身。这里aliasname不是真正的类型,只是想表达f1和f2并非是field1和field2的引用。f1 和 f2 的实际类型是 const int 和 const std::string, 注意不是引用。

那如果我们想通过结构化绑定来改变源对象的值呢?此时我们可以写成:

BindTest bt;
auto& [f1, f2] = bt;

将 const auto 改成 auto& 的形式,就可以达到让 f1,f2 和 bt 可以互相修改的目的了。

需要注意的是,结构化绑定不能忽略对象的子对象或元素,这一点和python类似,例如:

auto t = std::make_tuple(1, "hi")
auto [x] = t;   // 错误,tuple中有2个元素,无法只绑定1个

以上代码会编译失败,必须有2个别名分别对应tuple中的2个成员。但我们可以通过使用std::tie和std::ignore来间接地解决(没什么实际意义):

auto t = std::make_tuple(1, "hi")
int x  = 0, y = 0;
std::tie(x, std::ignore) = t;
std::tie(y, std::ignore) = t;

上述方式对于std::tie是有效的,但如果改写成结构化绑定就不行了,因为结构化绑定有一个限制:无法在同一个作用域中重复使用,例如:

auto t = std::make_tuple(1, "hi")
auto [x, ignore] = t;
auto [y, ignore] = t;  // 编译错误,ignore无法重复声明

三、结构化绑定的3种类型

结构化绑定可以作用于3中类型:

  • 原生数组
  • 结构体和类对象
  • 元组和类元组对象

3.1 绑定到原生数组

绑定到原生数组上,也就是将标识符列表中的别名逐一绑定到原生数组对应的元素上,所需满足的条件就是要求别名的数量和数组元素的个数相等。

int a[3] = {1, 2, 3};
auto [x, y, z] = a;

需要注意的是,绑定到原生数组需要小心数组的退化,因为绑定的过程中编译器必须要知道数组的元素个数,而一旦数组退化为指针,就将失去这个属性。

3.2 绑定到结构体和类对象

绑定到结构体,在上面的例子中已经有所体现,但还有一些额外的限制条件。

  • 不能绑定到类或结构体中的静态成员
  • 类或结构体中的非静态成员的个数必须和标识符列表中别名的个数相同
  • 类或结构体中的非静态成员必须是public的(这一点在C++20中有所调整,见下文描述)
  • 这些数据成员必须是在同一个类或者基类中
  • 绑定的类或结构体中不能存在匿名联合体(命名的联合体则没有限制)

3.3 绑定到元组和类元组对象

绑定到元组也在前文演示过了,那么这里的类元组又是什么意思呢?

首先来看看绑定的限制条件。 对于待绑定的元组或类元组的类型T:

  • 需要满足std::tuple_size<T>::value是一个符合语法的表达式,并且该表达式获得的整数值与标识符列表中的别名个数相同。

  • 类型T还需要保证std::tuple_element<i, T>::type也是一个符合语法的表达式,其中i是小于std::tuple_size<T>::value的整数,表达式代表了类型T中第i个元素的类型。

  • 类型T必须存在合法的成员函数模板get<i>()或者函数模板get<i>(t),其中i是小于std::tuple_size<T>::value的整数,t是类型T的实例,get<i>()和get<i>(t)返回的是实例t中第i个元素的值。

虽然上述条件比较抽象,但这些条件并没有明确规定结构化绑定的类型必须是元组,任何满足上述条件的类型都可以成为绑定的目标。另外,获取这些条件特征的代价也并不高,只需要为目标类型提供std::tuple_size, std::tuple_element以及get的特化或偏特化版本即可。

实际上,标准库中除了tuple外,std::pair和std::array也能作为结构化绑定的目标,原因就是它们是满足上述条件的类元组。

对于std::pair, 由于支持了结构化绑定,可以使得代码的书写更简单了,例如:

// C++11
void visitMapWithPair11()
{
    std::map<int, std::string> idToName{ {1, "lucy"}, {2, "lisa"} };
    for (const auto& item : idToName)
    {
        std::cout << "id:" << item.first << ", name:" << item.second << std::endl;
    }
}

// C++17 支持结构化绑定
void visitMapWithPair17()
{
    std::map<int, std::string> idToName{ {1, "lucy"}, {2, "lisa"} };
    for (const auto& [id, name] : idToName)
    {
        std::cout << "id:" << id << ", name:" << name << std::endl;
    }
}

四、绑定的访问权限问题

前面提到过,当在结构体或者类中使用结构化绑定的时候,需要有公开的访问权限,否则会导致编译失败。这条限制乍看是合理的,但是仔细想来却引入了一个相同条件下代码表现不一致的问题:

class A
{
friend void foo();
private:
    int i;
};

void foo()
{
    A a{};
    auto x = a.i;   // 编译成功
    auto [y] = a;   // 理论上C++20之前会编译失败,但实际C++17也可编译成功 
}

为了解决这类问题,C++20标准规定结构化绑定的限制不再强调必须为公开数据成员,编译器会根据当前操作的上下文来判断是否允许结构化绑定。幸运的是,虽然标准是2018年提出修改的,但无论是C++17还是C++20标准,以上代码都可以顺利地通过编译。

五、本文实验代码供参考

#include <iostream>
#include <tuple>
#include <string>
#include <vector>
#include <map>

// C++11
std::tuple<int, int> returnValues()
{
    return std::make_tuple(1, 2);
}

// C++14
auto returnValues14()
{
    return std::make_tuple(1, 2);
}

struct BindTest
{
    int field1 = 1;
    std::string field2 = "hello";
    static std::string field3;
};
std::string BindTest::field3 = "this is static";

// C++11
void visitMapWithPair11()
{
    std::map<int, std::string> idToName{ {1, "lucy"}, {2, "lisa"} };
    for (const auto& item : idToName)
    {
        std::cout << "id:" << item.first << ", name:" << item.second << std::endl;
    }
}

// C++17 支持结构化绑定
void visitMapWithPair17()
{
    std::map<int, std::string> idToName{ {1, "lucy"}, {2, "lisa"} };
    for (const auto& [id, name] : idToName)
    {
        std::cout << "id:" << id << ", name:" << name << std::endl;
    }
}

class A
{
friend void foo();
private:
    int i;
};

void foo()
{
    A a{};
    auto x = a.i;   // 编译成功
    auto [y] = a;     // 理论上C++20之前会编译失败,但实际C++17也可编译成功 
}

int main()
{
    // C++11
    int x = 0, y = 0;
    std::tie(x, y) = returnValues();
    std::cout << "x:" << x << ", y:" << y << std::endl;

    // C++17
    auto [a, b] = returnValues14();
    std::cout<< "a:" << a << ", b:" << b << std::endl;

    // 绑定到结构体
    BindTest bt;
    auto [f1, f2] = bt;
    std::cout << "f1:" << f1 << ", f2:" << f2 << std::endl;

    // 基于范围的for循环
    std::vector<BindTest> btVec = { {1, "this is 1"}, {2, "this is 2"}, {3, "this is 3"} };
    for (const auto& [x, y] : btVec)
    {
        std::cout << "x:" << x << ", y:" << y << std::endl;
    }

    // 报错,z不能绑定到静态成员
    // auto [x, y, z] = bt; 

    // std::pair 支持结构化绑定
    visitMapWithPair11();
    visitMapWithPair17();

    return 0;
}

参考文献:

  1. 《C++ Primer第五版》
  2. 《现代C++语言核心特性解析》

猜你喜欢

转载自blog.csdn.net/hubing_hust/article/details/128667994