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

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

Разве первая и вторая строки не должны быть эквивалентны в том смысле, что rvalue по-прежнему допустимо для копирования?

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


person Zulan    schedule 24.07.2018    source источник
comment
auto [amin, amax]? Это новый синтаксис в С++ 17?   -  person R Sahu    schedule 24.07.2018
comment
@RSahu да, это называется [структурированные привязки]   -  person Zulan    schedule 24.07.2018
comment
Вероятно, это связано с stackoverflow. com/questions/36555544/ — но все же отличается из-за комбинации со структурированными привязками.   -  person Zulan    schedule 24.07.2018


Ответы (2)


Что важно отметить в 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, вы выполняете присваивание существующим объектам (передаются по ссылке). rvalue не должны жить дольше, чем выражения присваивания, в которых они возникают.


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

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

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

person StoryTeller - Unslander Monica    schedule 24.07.2018
comment
Есть ли правильный способ использования auto в этом случае или std::tie единственный вариант? - person ptb; 24.07.2018
comment
@ptb - вы действительно не можете контролировать, как выводятся типы ваших структурированных привязок. Существует стена стандартов, которые описывают, как это должно вести себя, в зависимости от точного типа возвращаемого значения функции. Но ничто в объявлении привязок не влияет на это. Проблема здесь в том, что std::minmax по существу возвращает объект ссылочного типа (вроде string_view). И эта ссылка больше не относится к действительным вещам. Я отредактировал обходной путь, который превращает его в тип значения. - person StoryTeller - Unslander Monica; 24.07.2018
comment
возможное решение auto [amin,amax] = std::minmax( { 3, 6 } ); - person Slava; 24.07.2018
comment
@Слава - Тоже возможно. Но теперь аргументы также копируются в функцию, а не копируются только один раз на выходе. На самом деле это не так уж важно для целых чисел, но об этом следует подумать. - person StoryTeller - Unslander Monica; 24.07.2018
comment
Да, в любом случае это некрасиво, боюсь, очень скоро будет очень сложно писать строчный код без непредсказуемых побочных эффектов на C++. - person Slava; 24.07.2018
comment
Я даже не знал, что std::minmax возвращает пары ссылок. Я имею в виду константные ссылки. Это может болтаться. Кто придумал это снова? И почему перегрузка std::initializer_list помогает избежать зависаний, а не эта? - person Passer By; 24.07.2018
comment
@PasserBy - перегрузка списка инициализатора должна возвращать копии, потому что (в зависимости от того, какую версию вы запрашиваете по конкретной причине) нельзя полагаться на существование хранилища для списка. Это должно имитировать std::min и std::max. Когда Степанов придумал их, у него были только ссылки const lvalue для поддержки rvalue. Это не страшно C++98. Я не знаю, насколько далеко продвинулась стандартизация С++ 11, когда они были добавлены. - person StoryTeller - Unslander Monica; 24.07.2018
comment
Все еще звучит как довольно плохое принятие решений. Я не понимаю, как разумно людям знать, что auto p = std::minmax(3, 6); может убить их котенка. Это также легко исправить постфактум, просто добавьте перегруженную ссылку rvalue и верните значение. - person Passer By; 24.07.2018
comment
@PasserBy - Ты меня достал. Я не уверен, может ли это отрицательно сказаться на каком-либо существующем коде, но его определенно можно изменить. - person StoryTeller - Unslander Monica; 24.07.2018
comment
Спасибо @StoryTeller. Это было очень полезно. - person ptb; 24.07.2018
comment
Спасибо, это очень полезно. Есть ли у вас понимание, почему результаты на практике зависят от уровня оптимизации, особенно если учесть, что GCC вообще способен предупреждать об этом. - person Zulan; 24.07.2018
comment
@Zulan - боюсь, я не слишком знаком с внутренней работой оптимизаторов GCC и Clang, чтобы рассказать вам, почему это происходит. UB может проявляться и в поведении компилятора. - person StoryTeller - Unslander Monica; 24.07.2018

Здесь возникают две фундаментальные проблемы:

  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). Так что, к сожалению, вам нужно будет дополнительно написать специализацию для эталонных оберток.

person Barry    schedule 24.07.2018
comment
minmax(std::ref(big1), std::ref(big2)); я думаю, что функция minmax выведет свои аргументы как std::reference_wrapper<int> здесь. но я не могу найти ни operator < для reference_wrapper, ни специализацию для minmax. как это может работать? - person phön; 25.07.2018
comment
@phön reference_wrapper<T> неявно преобразуется в T&, что охватывает практически все случаи (за исключением шаблонов операторов, не являющихся членами, не являющимися друзьями, которые, к сожалению, есть у std::string). Для полноты нужна специализация. - person Barry; 25.07.2018
comment
Оно неявно преобразуется в T&, но minmax не выводит T, а выводит std::reference_wrapper<T>. А operator< вместо std::reference_wrapper<T> нет, или я ошибаюсь? (я попробовал, и он скомпилировался. но как? если я поставлю свою собственную обертку в minmax, это не сработает, потому что для моей обертки нет operator <: struct wrapper { int i; operator int&() { return i; } }; - person phön; 25.07.2018
comment
@phön Нет operator<, но есть operator T&. - person Barry; 25.07.2018
comment
ааа я забыл про константу в минмакс. Но, тем не менее, я не знал, что оператор ‹ (и, я думаю, все остальные операторы ;-P ) будут проверять все неявные преобразования из обоих задействованных типов. Спасибо за ваше понимание! - person phön; 25.07.2018