一、Python中的相似功能
熟悉python的应该对下面的代码很熟悉
def return_multiple_values():
return 11, 7
x, y = return_multiple_values()
函数返回一个元组,元组自动分配给了x和y。
二、C++11中的元组
c++11中就存在类似python元组的概念了:
std::tuple<int, int> return_multiple_values() {
return std::make_tuple(11, 7);
}
int main() {
int x = 0, y = 0;
std::tie(x, y) = return_multiple_values();
std::cout << "x=" << x << " y=" << y << std::endl;
}
这段代码和Python完成了同样的工作,但代码却要麻烦许多。一个原因是C++11必须指定return_multiple_values
函数的返回值类型,另外,在调用return_multiple_values
函数前还需要声明变量x
和y
,并且使用函数模板std::tie
将x
和y
通过引用绑定到std::tuple<int&, int&>
上
三、结构化绑定
对于第一个问题,可以使用C++14中auto
的新特性来简化返回类型的声明
auto return_multiple_values() {
return std::make_tuple(11, 7);
}
对于第二个问题,就必须使用C++17的新特性,结构化绑定。所谓结构化绑定是指将一个或者多个名称绑定到初始化对象中的一个或者多个子对象(或者元素)上,相当于给初始化对象的子对象(或者元素)起了别名,请注意别名不同于引用。
auto return_multiple_values() {
return std::make_tuple(11, 7);
}
int main() {
auto[x, y] = return_multiple_values();
std::cout << "x=" << x << " y=" << y << std::endl;
}
其中auto
是类型占位符,[x, y]
是绑定标识符列表,其中x
和y
是用于绑定的名称,绑定的目标是函数return_multiple_values()
返回结果副本的子对象或者元素。
结构化绑定的目标不必是一个函数的返回结果,实际上等号的右边可以是任意一个合理的表达式,比如:
struct BindTest {
int a = 42;
std::string b = "hello structured binding";
};
int main() {
BindTest bt;
auto[x, y] = bt;
std::cout << "x=" << x << " y=" << y << std::endl;
}
可以看到结构化绑定能够直接绑定到结构体上。将其运用到基于范围的for
循环中会有更好的效果:
struct BindTest {
int a = 42;
std::string b = "hello structured binding";
};
int main() {
std::vector<BindTest> bt{ {11, "hello"}, {7, "c++"}, {42, "world"} };
for (const auto& [x, y] : bt) {
std::cout << "x=" << x << " y=" << y << std::endl;
}
}
四、深入理解结构化绑定
在结构化绑定时,编译器会生成一个等号右边对象的匿名副本,绑定的对象是这个匿名副本,不是右边对象,或者右边对象的引用。
BindTest bt;
const auto [x, y] = bt;
// 经过编译器处理
BindTest bt;
const auto _anonymous = bt;
aliasname x = _anonymous.a
aliasname y = _anonymous.b
_anonymous
是编译器生成的匿名对象,const auto [x, y] = bt
中auto
的限定符会直接应用到匿名对象_anonymous
上。也就是说,_anonymous
是const
还是volatile
完全依赖auto
的限定符。
4.1、示例一
看下面的代码:
int main() {
BindTest bt;
const auto[x, y] = bt;
std::cout << "&bt.a=" << &bt.a << " &x=" << &x << std::endl;
std::cout << "&bt.b=" << &bt.b << " &y=" << &y << std::endl;
std::cout << "std::is_same_v<const int, decltype(x)>="
<< std::is_same_v<const int, decltype(x)> << std::endl;
std::cout << "std::is_same_v<const std::string, decltype(y)>="
<< std::is_same_v<const std::string, decltype(y)> << std::endl;
}
结果如下
可以看到别名x
并不是bt.a
,因为它们的内存地址不同。另外,x
和y
的类型分别与const int
和const std::string
相同也证明了它们是别名而不是引用的事实。可见,如果在上面这段代码中试图使用x
和y
去修改bt
的数据成员是无法成功的,因为一方面x
和y
都是常量类型;另一方面即使x
和y
是非常量类型,改变的x
和y
只会影响匿名对象而非bt
本身。
4.2、示例二
看下面的代码
int main() {
BindTest bt;
auto&[x, y] = bt;
std::cout << "&bt.a=" << &bt.a << " &x=" << &x << std::endl;
std::cout << "&bt.b=" << &bt.b << " &y=" << &y << std::endl;
x = 11;
std::cout << "bt.a=" << bt.a << std::endl;
bt.b = "hi structured binding";
std::cout << "y=" << y << std::endl;
}
结果如下
虽然只是将const auto
修改为auto&
,但是已经能达到让bt
数据成员和x
、y
相互修改的目的了。别名真的是单纯的别名,别名的类型和绑定目标对象的子对象类型相同,而引用类型本身就是一种和非引用类型不同的类型
4.3、示例三
auto t = std::make_tuple(42, "hello world");
auto [x] = t;
以上代码是无法通过编译的,必须有两个别名分别对应bt
的成员变量a
和b
,使用结构化绑定无法忽略对象的子对象或者元素。
可以仿照C++11中std::tie
使用std::ignore
的方案:
auto t = std::make_tuple(42, "hello world");
int x = 0, y = 0;
std::tie(x, std::ignore) = t;
std::tie(y, std::ignore) = t;
虽然这个方案对于std::tie
是有效的,但是结构化绑定的别名还有一个限制:无法在同一个作用域中重复使用。这一点和变量声明是一样的,比如:
auto t = std::make_tuple(42, "hello world");
auto[x, ignore] = t;
auto[y, ignore] = t; // 编译错误,ignore无法重复声明
五、结构化绑定的三种类型
5.1、绑定到原生数组
绑定到原生数组即将标识符列表中的别名一一绑定到原生数组对应的元素上。所需条件仅仅是要求别名的数量与数组元素的个数一致,比如:
int a[3]{ 1, 3, 5 };
auto[x, y, z] = a;
std::cout << "[x, y, z]=[" << x << ", " << y << ", " << z << "]" << std::endl;
5.2、绑定到结构体和类对象
一些限制:
1、类或者结构体中的非静态数据成员个数必须和标识符列表中的别名的个数相同;
2、这些数据成员必须是公有的
3、这些数据成员必须是在同一个类或者基类中
4、绑定的类和结构体中不能存在匿名联合体
示例一:
class BindTest {
int a = 42; // 私有成员变量
public:
double b = 11.7;
};
int main() {
BindTest bt;
auto[x, y] = bt; // 编译失败,有私有成员变量
auto[x] = bt; // 编译失败,有私有成员变量
}
示例二
class BindBase1 {
public:
int a = 42;
double b = 11.7;
};
class BindBase2 {};
class BindBase3 {
public:
int a = 42;
};
class BindTest1 : public BindBase1 {};
class BindTest2 : public BindBase2 {
public:
int a = 42;
double b = 11.7;
};
class BindTest3 : public BindBase3 {
public:
double b = 11.7;
};
int main() {
BindTest1 bt1;
BindTest2 bt2;
BindTest3 bt3;
auto[x1, y1] = bt1; // 编译成功
auto[x2, y2] = bt2; // 编译成功
auto[x3, y3] = bt3; // 编译错误,不在一个类或者基类内
}
auto[x1, y1] = bt1
和auto[x2, y2] = bt2
可以顺利地编译,因为类BindTest1
和BindTest2
的非静态数据成员要么全部在派生类中定义,要么全部在基类中定义。BindTest3
却不同,其中成员变量a
的定义在基类,成员变量b
的定义在派生类,这一点违反了绑定结构体的限制条件,所以auto[x3, y3] = bt3
会导致编译错误。
5.3、绑定到元组和类元组的对象
绑定到元组就是将标识符列表中的别名分别绑定到元组对象的各个元素。绑定到类元组要从绑定的限制条件讲起。绑定元组和类元组有一系列抽象的条件:对于元组或者类元组类型T
。
1.需要满足
std::tuple_size<T>::value
是一个符合语法的表达式,并且该表达式获得的整数值与标识符列表中的别名个数相同。2.类型
T
还需要保证std::tuple_element<i, T>::type
也是一个符合语法的表达式,其中i
是小于std::tuple_size<T>::value
的整数,表达式代表了类型T
中第i
个元素的类型。3.类型
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
的特化或者偏特化版本即可。实际上,标准库中除了元组本身毫无疑问地能够作为绑定目标以外,std::pair
和std::array
也能作为结构化绑定的目标,其原因就是它们是满足上述条件的类元组。
可以利用这个特性简化代码:
int main() {
std::map<int, std::string> id2str{ {1, "hello"}, {3, "Structured"}, {5, "bindings"} };
for (const auto& elem : id2str) {
std::cout << "id=" << elem.first << ", str=" << elem.second << std::endl;
}
}
// ==> 简化为
for (const auto&[id, str]:id2str) {
std::cout << "id=" << id << ", str=" << str << std::endl;
}