структурированные привязки с std :: minmax и rvalues

Я столкнулся с довольно тонкой ошибкой при использовании std::minmax со структурированными привязками. Похоже, что переданные rvalues не всегда будут скопированы, как можно было бы ожидать. Первоначально я использовал 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]

Clang 6.0.1 в -O2 также даст неправильный первый результат без предупреждения.

В -O0 GCC дает правильный результат и никаких предупреждений. Для clang результат кажется правильным в -O1 или -O0.

Должна ли первая и вторая строка быть эквивалентной в том смысле, что значение r остается действительным для копирования?

Кроме того, почему это зависит от уровня оптимизации? В частности, я был удивлен, что GCC не предупреждает.

Ответ 1

Важно отметить auto [amin, amax] что auto, auto& и т.д. std::minmax к созданному объекту 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> return для этого парного объекта. И они сами возвращают ссылки на давно исчезнувшие объекты!

Когда вы используете std::tie, вы выполняете присвоение существующим объектам (передается по ссылке). Rvalues не должны проживать дольше, чем выражения присваивания, в которых они возникают.


Как работа, вы можете использовать что-то вроде этого (а не качество продукции):

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));

Теперь мы получаем копию, и структурированные привязки относятся к этим копиям.

Ответ 2

Здесь есть два основных вопроса:

  1. min, max и minmax по историческим причинам возвращают ссылки. Поэтому, если вы переходите во временное положение, вам лучше взять результат по значению или сразу же использовать его, иначе вы получите ссылку на болтовню. Если 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). К сожалению, вам также необходимо написать специализацию для ссылочных оберток.