Я работал над некоторым кодом 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?
Я думаю, что общий тип двух ветвей вашего условного оператора - std::shared_ptr<int>, поэтому в первом случае создается временный экземпляр, созданный перемещением, и передается func, который крадет право собственности на ptr до вызова функции.
это имеет смысл. Вы переходите в func, а func просто уничтожает временный параметр. Последний для итерации переместит shared_ptr в функцию. и в конце func 'перемещенный', shared_ptr уничтожается
@AF_cpp Аргумент для func является ссылкой. Только время жизни ссылки enda в конце func, а не объект, на который делается ссылка (если только ссылка не продлевает время жизни материализованного временного объекта).
Кажется, что цикл добавляет ненужную сложность, чтобы продемонстрировать проблему. Вы можете просто полностью удалить эту часть godbolt.org/z/qsTncvGvT
тип условного выражения выше — std::shared_ptr<int>, поэтому временный объект создается в первом случае при выполнении условия, проверьте godbolt.org/z/44EM6YPad
@FrançoisAndrieux Не думай просто так. Я уверен, что это так. Условный оператор всегда возвращает общие типы. Этот фрагмент просто показывает предпочтение значения над ссылкой.





Поскольку второй и третий операнд условного оператора не относятся к одной и той же категории значений (т. е. 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 Это изменило бы значение существующего кода и вынудило бы реализации ввести скрытый флаг времени выполнения, который определяет, существует ли временный объект, который необходимо уничтожить, поскольку временный объект будет создан только вдоль одного ветвей условного выражения.
Если результат используется, он будет привязан к ссылке. Не нужно эвристики, просто продлите жизнь временному - вне зависимости от того, было ли оно использовано.
@Red.Wave Я не могу понять, что ты имеешь в виду. Если ваше предложение состоит в том, что результатом всего условного выражения должно быть значение x, то это означает, что когда выбирается второй операнд, он становится результатом (не временным), а когда выбирается третий операнд, материализуется временное значение. Таким образом, вы получите условное создание временного файла. В первом случае нет временного продления жизни.
2-й операнд — ссылка на rvalue, 3-й — rvalue. Результат должен быть ссылкой на rvalue. 3-й операнд продлевается до продолжительности всего, что фиксирует результат, независимо от условия. Если во время компиляции выводится условие как true, то операция может быть закорочена, а 3-й операнд немедленно уничтожен.
@ Red.Wave Вы не можете продлить срок службы третьего операнда, если третий операнд не оценивается в первую очередь. Условный оператор оценивает только второй или третий операнд, а не оба.
Да, это тупик.
@Red.Wave Итак, вы согласны с тем, что флаг времени выполнения необходим, чтобы программа знала, нужно ли ей вызывать деструктор для временного продления срока службы после окончания срока службы ссылки?
Если оба параметра не оцениваются, то потребуется флаг; не оптимальное решение. Ветвь времени выполнения не идеальна. Указатель на функцию-деструктор также может быть создан, но опять же слишком большое усложнение нехорошо.
Вызываемые функции 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>&, что вызывает копирование-конструкцию в последнем цикле. И первый вариант ок. Спасибо за советы по улучшению!
Разве UB не использует объект после того, как он был перемещен?