Странное поведение при использовании std::move shared_ptr с условным оператором

Я работал над некоторым кодом C++, используя std::move на shared_ptr, и получил очень странный результат. Я упростил свой код, как показано ниже

int func(std::shared_ptr<int>&& a) {
    return 0;
}

int main() {
    std::shared_ptr<int> ptr = std::make_shared<int>(1);

    for (int i = 0; i != 10; ++i) {
        func(i == 9 ? std::move(ptr) : std::shared_ptr<int>(ptr));
    }

    if (ptr) {
        std::cout << "ptr is not null: " << *ptr << "\n";
    } else {
        std::cout << "ptr is null\n";
    }

    return 0;
}

И я получил вывод

ptr is null

Как я и ожидал, мой ptr будет перемещен (преобразован в std::shared_ptr<int>&&) в последнем цикле, и, поскольку func никогда не крадет память в a, мой ptr снаружи будет ненулевым (что на самом деле оказывается нулевым). А если я заменю

func(i == 9 ? std::move(ptr) : std::shared_ptr<int>(ptr));

с оператором if-else

if (i == 9) func(std::move(ptr));
else func(std::shared_ptr<int>(ptr));

вывод будет

ptr is not null: 1

Я так смущен этим поведением компилятора.

Я пробовал GCC и clang с другой стандартной версией и уровнем оптимизации и получил тот же результат. Может ли кто-нибудь объяснить мне, почему и где были украдены данные под ptr?

Разве UB не использует объект после того, как он был перемещен?

Adrian Maire 09.05.2023 13:23

Я думаю, что общий тип двух ветвей вашего условного оператора - std::shared_ptr<int>, поэтому в первом случае создается временный экземпляр, созданный перемещением, и передается func, который крадет право собственности на ptr до вызова функции.

François Andrieux 09.05.2023 13:24

это имеет смысл. Вы переходите в func, а func просто уничтожает временный параметр. Последний для итерации переместит shared_ptr в функцию. и в конце func 'перемещенный', shared_ptr уничтожается

AF_cpp 09.05.2023 13:26

@AF_cpp Аргумент для func является ссылкой. Только время жизни ссылки enda в конце func, а не объект, на который делается ссылка (если только ссылка не продлевает время жизни материализованного временного объекта).

François Andrieux 09.05.2023 13:30

Кажется, что цикл добавляет ненужную сложность, чтобы продемонстрировать проблему. Вы можете просто полностью удалить эту часть godbolt.org/z/qsTncvGvT

cigien 09.05.2023 13:46

тип условного выражения выше — std::shared_ptr<int>, поэтому временный объект создается в первом случае при выполнении условия, проверьте godbolt.org/z/44EM6YPad

konchy 09.05.2023 13:58

@FrançoisAndrieux Не думай просто так. Я уверен, что это так. Условный оператор всегда возвращает общие типы. Этот фрагмент просто показывает предпочтение значения над ссылкой.

Red.Wave 09.05.2023 19:59
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
5
7
138
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Поскольку второй и третий операнд условного оператора не относятся к одной и той же категории значений (т. е. std::move(ptr) — это значение x, а std::shared_ptr<int>(ptr) — значение pr), это условное выражение попадает под действие [expr.cond]/7:

Стандартные преобразования Lvalue-to-rvalue, массива в указатель и функции в указатель выполняются для второго и третьего операндов. После этих преобразований должно выполняться одно из следующих условий:

  • Второй и третий операнды имеют одинаковый тип; результат имеет этот тип, и объект результата инициализируется с использованием выбранного операнда.
  • [...]

std::shared_ptr<int>(ptr) уже является prvalue, поэтому преобразование lvalue в rvalue (которое на самом деле является преобразованием glvalue в prvalue) ничего с ним не делает.

std::move(ptr) преобразуется в значение prvalue, и это значение prvalue используется для инициализации объекта результата. Инициализация результирующего объекта использует конструктор перемещения (потому что это конструктор, который инициализирует std::shared_ptr<int> из значения x этого типа, что и есть std::move(ptr)). Конструктор перемещения «крадет» значение из ptr. Объект результата — это временный объект, который привязан к параметру a, а затем уничтожается. Обратите внимание, что все это происходит только в том случае, когда std::move(ptr) действительно оценивается (что требует, чтобы i == 9 было истинным).

Но, я думаю, это потенциальный DR. общий тип значения и его ссылка должны быть ссылкой IMO.

Red.Wave 09.05.2023 20:02

@Red.Wave Это изменило бы значение существующего кода и вынудило бы реализации ввести скрытый флаг времени выполнения, который определяет, существует ли временный объект, который необходимо уничтожить, поскольку временный объект будет создан только вдоль одного ветвей условного выражения.

Brian Bi 09.05.2023 22:30

Если результат используется, он будет привязан к ссылке. Не нужно эвристики, просто продлите жизнь временному - вне зависимости от того, было ли оно использовано.

Red.Wave 10.05.2023 12:03

@Red.Wave Я не могу понять, что ты имеешь в виду. Если ваше предложение состоит в том, что результатом всего условного выражения должно быть значение x, то это означает, что когда выбирается второй операнд, он становится результатом (не временным), а когда выбирается третий операнд, материализуется временное значение. Таким образом, вы получите условное создание временного файла. В первом случае нет временного продления жизни.

Brian Bi 10.05.2023 14:45

2-й операнд — ссылка на rvalue, 3-й — rvalue. Результат должен быть ссылкой на rvalue. 3-й операнд продлевается до продолжительности всего, что фиксирует результат, независимо от условия. Если во время компиляции выводится условие как true, то операция может быть закорочена, а 3-й операнд немедленно уничтожен.

Red.Wave 10.05.2023 18:22

@ Red.Wave Вы не можете продлить срок службы третьего операнда, если третий операнд не оценивается в первую очередь. Условный оператор оценивает только второй или третий операнд, а не оба.

Brian Bi 10.05.2023 19:40

Да, это тупик.

Red.Wave 11.05.2023 21:07

@Red.Wave Итак, вы согласны с тем, что флаг времени выполнения необходим, чтобы программа знала, нужно ли ей вызывать деструктор для временного продления срока службы после окончания срока службы ссылки?

Brian Bi 11.05.2023 21:19

Если оба параметра не оцениваются, то потребуется флаг; не оптимальное решение. Ветвь времени выполнения не идеальна. Указатель на функцию-деструктор также может быть создан, но опять же слишком большое усложнение нехорошо.

Red.Wave 12.05.2023 09:13

Вызываемые функции C++ не обязаны перемещать значение, даже если они вызываются со ссылкой на rvalue. В частности, ваш func() даже не касается своего аргумента a, поэтому ptr остается неизменным в этом вызове: func(std::move(ptr)).

Наоборот, ваше выражение

func(i == 9 ? std::move(ptr) : std::shared_ptr<int>(ptr));

всегда создает временный тип std::shared_ptr<int> перед вызовом func() со ссылкой rvalue на этот временный объект. Что происходит под капотом:

{
    std::shared_ptr<int> temp = i == 9 ? std::move(ptr) : std::shared_ptr<int>(ptr));
    func(std::move(temp));
}

Когда i равно 9, задание temp = std::move(ptr) перемещает ptr в temp и оставляет ptr пустым. Затем, когда достигается конец блока и временное выходит за пределы области действия, оно уничтожается.

Вы можете избежать такого поведения, изменив свою строку на любой из следующих двух вариантов:

func(i == 9 ? std::move(ptr) : std::move(std::shared_ptr<int>(ptr)));

или:

func(std::move(i == 9 ? ptr : std::shared_ptr<int>(ptr)));

Оба варианта гарантируют, что созданное временное будет иметь ссылочный тип (а именно std::shared_ptr<int>&& в первом случае и std::shared_ptr<int>& во втором случае), поэтому ничего не нужно копировать.

Вы можете спросить, почему исходный код создает временный объект типа std::shared_ptr<int> (а не std::shared_ptr<int>&&). Ну, причина в хорошо известной оптимизации возвращаемого значения (RVO), которая была введена в C++ десятилетия назад: функции, которые возвращают объект, на самом деле вызываются таким образом, что вызывающая функция уже выделяет место для этого объекта и передает указатель к этому объекту к вызываемой функции. Затем вызываемая функция записывает возвращаемое значение непосредственно в это пространство. Из-за этого многие прямые назначения, такие как:

auto x = function(a, b, c);

избавьте себя от необходимости сначала создавать, а затем сразу же снова уничтожать временное значение. Но в вашем случае с тернарным оператором это скрытое временное внезапно оживает.

Я подумал (и проверил) во втором варианте, что общий тип тернара — std::shared_ptr<int>, а не std::shared_ptr<int>&, что вызывает копирование-конструкцию в последнем цикле. И первый вариант ок. Спасибо за советы по улучшению!

danry 10.05.2023 04:50

Другие вопросы по теме