结构化绑定取代std::tie滥用

Structured binding to replace std::tie abuse

本文关键字:tie 滥用 std 绑定 取代 结构化      更新时间:2023-10-16

在阅读c++17最终特性的总结时,我对结构化绑定部分(强调我的)感到有点惊讶:

结构化绑定

到目前为止,有一个已知的技巧是滥用std::tie直接将元组或对赋值给不同的变量,而不必手动处理结果类型。这是一个hack,并且变量必须存在,现在您可以在一行中声明变量并初始化它们:

auto [a, b, c] = getvalues();

需要大括号,getvalues返回一个元组。建议中没有提到std::pair,所以不清楚这是否适用于pair,它是由STL在某些插入方法中返回的。

我假设他们指的是std::tie的这种用法

int a,b,c;
std::tie(a,b,c) = std::make_tuple(1,2,3);

我认为这是一种推荐的做法。

有人能解释一下为什么他们把上面的例子称为黑客吗?

我可以这么简单地说:

函数只能返回一个变量的语言

int a,b,c;
std::tie(a,b,c) = function_returning_multiple_values();

是对

的改进:
auto [a, b, c] = function_returning_multiple_values();

就像假设c++只允许函数有一个形参

int p1, p2, p3;
p1 = ...;
p2 = ...;
p3 = ...;
function_taking_multiple_params(std::tie_params(p1, p2, p3));

可以替换为:

function_taking_multiple_params(p1, p2, p3)

你已经习惯了c++中一个函数最多只能返回一个对象的限制,但实际上这只是一种人为的语言限制,就像最多只能接受一个参数的限制是人为的语言限制一样。

std::tie是为缺少的语言特性编写的库。它也有一些缺点:

  • 变量需要事先声明
  • 变量类型必须显式声明
  • 效率低下或不能用于非默认可构造类型

结构化绑定是它们本来可以做到的一切吗?不,但在大多数情况下,它们是我们所需要的一切。

缺少什么?

  • 某些元素的显式类型:例如:
auto [a, std::string b, c] = foo();

其中ac已推导出类型,b显式为"std::string"

  • 嵌套。例如:
auto [a, [b1, b2], c] = foo();

,从foo返回的第二个对象是一个类似tuple的对象。

  • 返回站点的语言功能(一起绕过std::tuple):
auto foo() -> [int, int]
不是

auto foo() -> std::tuple<int, int>
  • 指定返回对象
auto foo() -> [int& key, int& value]

…嗯…那不是很好吗

  • 并将其与…-准备好一个很酷的新名字-广义返回初始化:
auto minmax_element(It begin, It end) -> [It min_it, It max_it];
auto [min = *min_it, max = *max_it] = minmax_element(...);

一个非常明显的区别是std::ignore。看一下示例

std::tuple<string, string> data {"Lord", "Buddha"};
auto [a, b] = data; //valid
auto [ , b] = data; //not valid as the identifier is strongly required
string y;
std::tie( std::ignore, y ) = data; //voila

std::tie本身还有另一个功能。

用来创建一个引用变量

的元组

创建一个由左值引用到其实参或std::ignore实例组成的元组。

这对于创建动态元组很有用,而不必复制变量,因为它们是引用。我只是用cppreference中的例子作为一个用例。

bool operator<(const S& rhs) const
{
    // compares n to rhs.n,
    // then s to rhs.s,
    // then d to rhs.d
    return std::tie(n, s, d) < std::tie(rhs.n, rhs.s, rhs.d);
}

这里元组被创建,但它们不复制变量,而是有引用。

现在,因为它们保存引用,你可以"黑"它来做这样的事情

int a,b,c;
std::tie(a,b,c) = std::make_tuple(1,2,3);

它将返回的元组的值赋给本身有引用的元组。

这甚至是在cpprevention上刚刚提到的"注释"

std::tie可用于解包std::pair,因为std::tuple具有对

的转换赋值。

在c++17中,他们引入了"结构化绑定"来处理一次性分配多个变量的场景。因此,无论这是有意为之还是黑客所为,从c++17开始,tie的这种用法应该不再必要了。

无论std::tie是打算这样使用还是"黑客"可能是个人的意见,我想引入std::tie的人对此最了解。但是考虑到结构化绑定在这种情况下是如何取代std::tie的,他们想出了一个他们认为更好的解决方案。

我希望没有人介意我提出我的观点,因为我认为它仍然是有效的。我同意上面所说的很多内容,但我不认为结构化绑定会取代std::tie。对于std::tie有一个特定的用例,您根本无法使用结构化绑定,因为变量是在现场声明的。不要误解我的意思,我是结构化绑定的粉丝,但我最近遇到了一个案例,它们就是不合适。我有这样的结构…

std::vector<std::tuple<std::string, uint32_t, uint64_t>> values;
typedef struct
{
    std::string s;
    uint32_t o;
    uint64_t v;
} A;
std::vector<A> my_objs;

好了,我有一个元组的向量和一个对象的向量我要做的就是从元组中取值并将这些值赋给向量中每个现有的对象,如下所示:

// (This is a very contrived example and you should assume that the
// objects in my_obj are much more complex, already exist and you just want
// to set some values in them)
for (size_t i = 0; i < my_obj.size(); i++)
    std::tie(my_objs.at(i).s, my_objs.at(i).o, my_objs.at(i).v) = values.at(i);
// Sure, you could create a method for my_obj that takes the values, but that's a very
// heavy handed approach and missing the point.

如果变量不存在,那么结构化绑定是您最好的朋友,但是如果存在,它们就没有帮助。此外,正如有人提到的,结构化绑定中还有许多其他的遗漏,对我来说,这意味着它们缺乏。首先是嵌套它们的能力,例如其中一个变量本身可能是一个元组/对等。其次,虽然这有点不同,但无法在lambda表达式声明符中使用结构化绑定,如:

std::unordered_map<std::string, Item*> items;
std::for_each(items.begin(), items.end(), [](const auto&[s, item]) { delete item; });   // Not allowed, you have to do...
std::for_each(items.begin(), items.end(), [](const auto& item_pair) { delete item_pair.second; });   // Acceptable

我认为对结构化绑定有用的是,声明的变量可以是对现有对象的引用。因此,尽管我觉得有些人认为std::tie将被结构化绑定所取代,但现实情况是,std::tie仍然具有结构化绑定可以提供的非常有用的目的,但只是没有。