8.2 结构化绑定
结构化绑定始于 Herb Sutter、Bjarne Stroustrup 和 Gabriel Dos Reis 的一个简单的提案 [Sutter et al. 2015],旨在简化写法和消除剩余的几个变量未初始化的来源。例如:
template<typename T, typename U>
void print(vector<pair<T,U>>& v)
{
for (auto [x,y] : v)
cout << '{' << x << ' ' << y << "}\n";
}
名称 x
和 y
被分别绑定于 pair
的第一个和第二个元素。这可算作是写法上的重大便利。
C++14 给我们提供了返回多个值的方便方式。例如:
tuple<T1,T2,T3> f(/*...*/) // 优美的声明语法
{
// ...
return {a,b,c}; // 优美的返回语法
}
我认为在当前的 C++ 中,tuple
有点被过度使用了,当多个值并不互相独立的时候,我倾向于使用明确定义的类型,但从写法上讲,这没有什么区别。然而,C++14 并没有提供像创建多返回值那样方便的方式去解包它们。这导致了繁琐的变通解决方案、变量未初始化或运行期开销。例如:
tuple<T1,T2,T3> res = f();
T1& alpha = get<0>(res); // 通过 alpha 来间接访问
T2& val = get<1>(res);
T3 err_code = get<2>(res); // 拷贝
很多专家更喜欢用标准库函数 tie()
去解包 tuple
:
T1 x;
T2 y;
T3 z;
// ...
tie(x,y,z) = f(); // 使用现有变量的优美调用方式
向 tie()
函数赋值的时候,会向 tie()
函数的参数赋值。然而,使用 tie
,你必须分别定义变量,并且写出它们的类型以匹配 f()
返回的对象的成员(在这个例子中就是 T1
、T2
、和 T3
)。不幸的是,这会导致局部变量设置前使用
的错误,及初始化后赋值
的开销。并且,大多数程序员并不知道 tie()
的存在,或者认为在真实代码中使用它太奇怪了。
Herb Sutter 建议了一种跟正常返回语法类似的方案:
auto {x,y,z} = f(); // 优美的调用语法,会引入别名
这对任何有三个成员的 struct
都有效,而不仅仅只对 tuple
。消除核心指南(§10.6)中未初始化变量的倒数第二个来源是我的主要动机。是的,我喜欢这种写法,但更重要的是它使得 C++ 更接近于其理想。
不是每个人都喜欢这个想法,而且我们几乎没能在 C++17 中及时讨论它。提出结构化绑定的论文 [Sutter et al. 2015] 比较晚,而正当 2015 年 11 月底在科纳 Ville Voutilainen 刚要结束 EWG 会议时,我注意到我们离午饭还有 45 分钟,我觉得小组应该会想要看到这个提案。2015 年科纳的会议是我们冻结 C++17 的功能集的时间点,所以这 45 分钟很关键。我们甚至没时间去另一个小组找到 Herb,我就直接讲了这个提案。EWG 喜欢这个提案,会议纪要说鼓掌以资鼓励;EWG 想要这样的东西。
现在,真正的工作开始了。
在这个及以后的会议中,几个人——尤其是 Chandler Carruth——指出要达到 C++ 的理想,我们需要扩展将一个对象分解为多个值的能力,以应对不是 tuple
或普通 struct
的类型。例如:
complex<double> z = 2+3i;
auto {re,im} = sqrt(z); // sqrt() 返回复数值
标准库类型 complex
并没有暴露其内部表示。
在 C++17 中我们通过允许用户定义一系列 get
函数解决了这个问题,如 get<0>
和 get<1>
,实际上是把计算结果当成 tuple
。这能工作,但需要用户提供一些不优雅的重复样板代码。关于潜在改进的讨论仍在继续,但没有明显的简化被纳入 C++20。
有人要求让这种方式也能适用于返回数组的函数和返回带位域的 struct
的函数。我们加入了对那些情况的支持,所以最终设计至少比原始提案复杂了一倍。
有一个冗长的争论(跨多次会议),是关于是否可能(或必须)显式地指定被引入的局部变量类型。例如:
auto {int x, const double* y, string& z} = f(); // 非 C++
关于这种做法的理由——其中最雄辩的当属 Ville Voutilainen——如果没有显式类型,写法的可读性将会降低,从而损害可维护性,还可能导致错误。这跟常见的反对 auto
的理由很相似,而显式类型也会有它们自己的问题。如果类型跟返回值不匹配怎么办?有人说这应该属于错误。有些人说,转换到指定的类型将是非常有用的(例如,char[20]
返回到 string
中)。我指出结构化绑定应该引入零开销别名,而任何意味着表示变化的类型转换将导致显著的开销。并且,结构化绑定的一个目的是优化写法,而要求显式类型会导致代码比现有的方式更加冗长。
最初的提案使用花括号({}
)来组合引入的名字:
auto {x,y,z} = f(); // 优美的调用语法,引入别名
然而一些成员,如 Chandler Carruth 和 David Vandevoorde,怕语法上会有歧义,而坚持认为这样会令人困惑,因为
{}意味着作用域
。所以我们有了 []
语法:
auto [x,y,z] = f(); // 调用语法,引入别名
这是个小改动,但我认为是个错误。这个最后一刻的改动,导致了属性写法的小小复杂化(比如 [[fallthrough]]
)(§4.2.10)。我对关于美学或作用域的论据并不买账,并且在 2014 年我就展示了关于为 C++ 添加函数式编程风格的模式匹配的想法,以 { … }
表示用模式将值分解出来(§8.3)。结构化绑定的设计就是为了适应这一总体方案。
这些并不是唯一的后期修改提案。每个提案都增加了或将增加复杂性。
对语言每次升级仅孤立地增加一项功能是危险的。除非符合更大的规划,最后一刻的改变也是危险的,容易导致在要求完整性
的过程中膨胀
。在这个结构化绑定的例子中,我不相信允许结构化绑定指定位域能提供充分的效用,值得为之提高复杂性。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论