23

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

#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: ‘&lt;anonymous>’ is used uninitialized in this function [-Wuninitialized]

Clang 6.0.1 at-O2也会给出错误的第一个结果而没有警告。

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

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

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

4

2 回答 2

13

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

auto e = std::minmax(3, 6);

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

amin和的实际类型amax是引用任何对象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));

我们现在得到一个副本,结构化绑定引用这些副本。

于 2018-07-24T16:12:16.123 回答
8

这里有两个基本问题:

  1. min, max, 并且minmax由于历史原因返回引用。所以如果你传入一个临时的,你最好按值取结果或立即使用它,否则你会得到一个悬空引用。如果在这里minmax给你 apair<int, int>而不是 a pair<int const&, int const&>,你就不会有任何问题。
  2. auto衰减顶级cv -qualifiers 并去除引用,但它不会一直删除。在这里,您正在推断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)的类型。因此,不幸的是,您还需要为引用包装器编写专业化。

于 2018-07-24T17:42:45.750 回答