std::minmax 和右值的结构化绑定

structured bindings with std::minmax and rvalues

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

#include <algorithm>
#include <cstdio>
#include <tuple>

int main()
{
    auto [amin, amax] = std::minmax(3, 6);
    printf("%d,%d\n", amin, amax); // undefined,undefined

    int bmin, bmax;
    std::tie(bmin, bmax) = std::minmax(3, 6);
    printf("%d,%d\n", 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 也会在没有警告的情况下给出错误的第一个结果。

-O0 GCC 给出了正确的结果并且没有警告。对于 clang,结果在 -O1-O0.

处似乎是正确的

第一行和第二行不应该是等价的,因为右值仍然可以被复制吗?

另外,为什么这取决于优化级别?特别令我惊讶的是 GCC 没有发出警告。

auto [amin, amax] 中需要注意的重要一点是 autoauto& 等应用到使用 return 的值 std::minmax,这是一对。本质上是这样的:

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> return。他们自己 return 对对象的引用早已消失!

当您使用 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 由于历史原因 return 引用 。所以如果你传入一个临时的,你最好按值获取结果或者立即使用它,否则你会得到一个悬空引用。如果 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)。因此,不幸的是,您还需要为参考包装器编写专门化。