带有 std::minmax 和 rvalues 的结构化绑定

structured bindings with std::minmax and rvalues

本文关键字:结构化 rvalues 绑定 std minmax 带有      更新时间:2023-10-16

我在将std::minmax与结构化绑定一起使用时遇到了一个相当微妙的错误。似乎传递的右值并不总是像人们预期的那样被复制。最初我在自定义容器上使用T operator[]() const,但它似乎与文字整数相同。

#include <algorithm>
#include <cstdio>
#include <tuple>
int main()
{
auto [amin, amax] = std::minmax(3, 6);
printf("%d,%dn", amin, amax); // undefined,undefined
int bmin, bmax;
std::tie(bmin, bmax) = std::minmax(3, 6);
printf("%d,%dn", bmin, bmax); // 3,6
}

将 GCC 8.1.1 与-O1 -Wuninitialized一起使用将导致0,0打印为第一行,并且:

warning: ‘<anonymous>’ is used uninitialized in this function [-Wuninitialized]

-O2的 Clang 6.0.1 也会在没有警告的情况下给出错误的第一个结果。

-O0GCC给出正确的结果并且没有警告。对于 clang,结果在-O1-O0处似乎是正确的。

第一行和第二行难道不应该等效,因为右值仍然有效,可以复制吗?

另外,为什么这取决于优化级别?我特别惊讶的是,海湾合作委员会没有发出警告。

auto [amin, amax]中需要注意的重要一点是,autoauto&等应用于用返回值std::minmax初始化的组成对象e,这是一个对。本质上是这样的:

auto e = std::minmax(3, 6);
auto&& amin = std::get<0>(e);
auto&& amax = std::get<1>(e);

aminamax的实际类型是引用,这些引用引用了该对对象的任何std::get<0>std::get<1>返回。它们自己返回对早已消失的对象的引用!

使用std::tie时,您正在对现有对象进行赋值(通过引用传递)。右值的生存时间不需要比它们产生的赋值表达式长。


作为解决方法,您可以使用类似以下内容(不是生产质量)函数:

template<typename T1, typename T2>
auto as_value(std::pair<T1, T2> in) {
using U1 = std::decay_t<T1>;
using U2 = std::decay_t<T2>;
return std::pair<U1, U2>(in);
}

它确保该对保存值类型。当像这样使用时:

auto [amin, amax] = as_value(std::minmax(3, 6));

我们现在制作了一个副本,结构化绑定引用了这些副本。

这里有两个基本问题:

  1. minmaxminmax由于历史原因返回引用。因此,如果您传入临时值,则最好按值获取结果或立即使用它,否则您将获得悬而未决的引用。如果minmax在这里给你一个pair<int, int>而不是一个pair<int const&, int const&>,你就不会有任何问题。
  2. auto衰减顶级cv限定符并剥离引用,但它不会一直向下删除。在这里,你推论的是pair<int const&, int const&>,但如果我们推论pair<int, int>,我们就不会有任何问题了。

(1)是一个比(2)更容易解决的问题:编写自己的函数以按值获取所有内容:

template <typename T>
std::pair<T, T> minmax(T a, T b) {
return (b < a) ? std::pair(b, a) : std::pair(a, b);
}
auto [amin, amax] = minmax(3, 6); // no problems

按值获取所有内容的好处是,您永远不必担心隐藏的悬空引用,因为没有任何引用。无论如何,这些函数的绝大多数用法都使用整型,因此引用没有任何好处。

当您确实需要参考时,当您比较昂贵的复制对象时......好吧,采用一个接受值的函数并强制它使用引用比采用一个使用引用的函数并尝试修复它更容易:

auto [lo, hi] = minmax(std::ref(big1), std::ref(big2)); 

此外,在调用站点上,我们使用引用非常明显,因此如果我们搞砸了,它会更加明显。


虽然由于reference_wrapper<T>隐式转换为T&,上述方法适用于许多类型,但它不适用于那些具有非成员,非友元,运算符模板(如std::string)的类型。因此,不幸的是,您还需要为引用包装器编写专门的内容。