Конструктор перемещения для возвращаемых объектов нарушает код С++ 98?

Стандарт не требует, чтобы компилятор выполнял оптимизацию возвращаемого значения (RVO), но тогда, начиная с C++11, результат должен быть перемещен.

Похоже, что это может ввести код UB для/разрыва, который был действителен в C++98.

Например:

#include <vector>
#include <iostream>

typedef std::vector<int> Vec;
struct Manager{
    Vec& vec;
    Manager(Vec& vec_): vec(vec_){}
    ~Manager(){
        //vec[0]=42; for UB
        vec.at(0)=42;
    }
};

Vec create(){
    Vec a(1,21);
    Manager m(a);
    return a;
}

int main(){
    std::cout<<create().at(0)<<std::endl;
}

При компиляции с gcc (или clang в этом отношении) с -O2 -fno-inline -fno-elide-constructors (я использую std::vector с этой опцией сборки, чтобы упростить пример. Можно было бы вызвать такое же поведение без этих опций с классами ручной работы и более сложным create -функция) для C++98(-std=c++98) все в порядке:

  1. return a; запускает конструктор копирования, который оставляет a нетронутым.
  2. Вызывается деструктор m (должен произойти до уничтожения a, потому что m создается после a). Доступ к a в деструкторе не проблематичен.
  3. Вызывается деструктор a.

Результат ожидаемый: печатается 21 (здесь).

Однако ситуация отличается при сборке как С++ 11 (-std=c++11):

  1. return a; запускает конструктор перемещения, который "уничтожает" a.
  2. Вызывается деструктор m, но теперь доступ к a проблематичен, потому что a был перемещен и больше не цел.
  3. vec.at(0) бросает сейчас.

Вот живая демонстрация.

Я что-то упустил, и пример проблематичен и в С++ 98?


person ead    schedule 24.04.2019    source источник
comment
Но Vec a; переживет Manager m;.   -  person Quimby    schedule 24.04.2019
comment
-fno-elide-constructors исключает оптимизацию возвращаемого значения и вместо этого использует перемещение. Почему вы используете этот флаг?   -  person Maxim Egorushkin    schedule 24.04.2019
comment
Я не очень понимаю, в чем твой вопрос? Насколько я знаю, существуют и другие способы, которыми автоматически сгенерированные конструкторы перемещения меняют поведение с C++98 на C++11 (побочные эффекты конструкторов и деструкторов — один из немногих случаев, когда оптимизация позволяет изменять наблюдаемое поведение). ), так что же особенного в сочетании его с stackoverflow.com/questions/52931095/?   -  person Max Langhof    schedule 24.04.2019
comment
@MaximEgorushkin с РВО нет УБ, т.к. a не перемещен и цел. Независимо от того, выполняется ли RVO или нет, зависит от компилятора, поэтому можно не выполнять RVO (или я мог бы придумать более сложную функцию, для которой компилятор не выполняет RVO, но я хотел, чтобы она была простой)   -  person ead    schedule 24.04.2019
comment
Я бы не назвал это неопределенным поведением, скорее, реализацией определенной. Объект a все еще находится в допустимом состоянии после перемещения.   -  person Quimby    schedule 24.04.2019
comment
@MaxLanghof Проблема в том, что код С++ 98 ломается с С++ 11. Кстати. например, в stackoverflow.com/q/52931095/5769463 результат не перемещается, а копируется также в С++ 11 .   -  person ead    schedule 24.04.2019
comment
в любом случае он работает без -fno-elide-constructors, это подсказка   -  person Marek R    schedule 24.04.2019
comment
@MaxLanghof Я мог бы уйти с кодом, который не компилируется. Другое дело отладка такого изменения поведения.   -  person ead    schedule 24.04.2019
comment
@MarekR использовал -fno-elide-constructors, чтобы упростить пример, дело в том, что компилятор не использует RVO (что действительно)   -  person ead    schedule 24.04.2019
comment
Я чувствую твою боль, но все равно не вижу, какой ответ ты здесь ищешь. Да, это правильный код C++98?   -  person Max Langhof    schedule 24.04.2019
comment
@MaxLanghof Это действительный код C ++ 98 в моей книге, но я был уверен и во многих других фрагментах, но это не так. Я просто не могу поверить, что действительный код становится недействительным, и надеюсь, что кто-нибудь покажет мне, в чем проблема.   -  person ead    schedule 24.04.2019
comment
Это действительно, когда копия Ctor не использует const?   -  person JVApen    schedule 24.04.2019
comment
Я просто не могу поверить, что действительный код становится недействительным... Почему бы и нет? Рассмотрим int decltype = 1;. Он действителен в C++03, но недействителен в C++11.   -  person Daniel Langr    schedule 24.04.2019
comment
часть стандарта, на которую вы ссылаетесь, как на подлежащую перемещению, только указывает, что она может быть перемещена   -  person 463035818_is_not_a_number    schedule 24.04.2019
comment
@DanielLangr, представляющий синтаксическую ошибку, отличается от молчаливого введения ub   -  person 463035818_is_not_a_number    schedule 24.04.2019
comment
Загвоздка в этом примере заключается в использовании целевого векторного объекта в функции main() в качестве временного объекта. Это может обмануть компилятор, заставляя его перемещать свои вещи (значения данных) подальше от вектора, когда он одновременно rvod и перемещается. Используйте более простую именованную переменную, и проблема исчезнет.   -  person jszpilewski    schedule 24.04.2019
comment
comment
@ user463035818 Это так. Однако вопрос, связанный с MaxLanghof, показывает пример, когда две разные перегрузки функции вызываются в С++ 03 и С++ 11. Может быть легко реализовать такой случай в UB только на C++11.   -  person Daniel Langr    schedule 24.04.2019
comment
@jszpilewski проблема заключается в функции «создать», и она одинакова независимо от того, является ли результат временным или нет.   -  person ead    schedule 24.04.2019
comment
@ead Проблема по-прежнему связана с обработкой временных файлов. Обратите внимание, что версия C++98 будет показывать разные результаты при компиляции с -fno-elide-constructors или без нее. В версии C++11 временное создается с семантикой move.   -  person jszpilewski    schedule 24.04.2019


Ответы (3)


Это не критическое изменение. Ваш код уже был обречен на C++98. Представьте, что вместо этого

int main(){
    Vec v;
    Manager m(v);
}

В приведенном выше примере вы получаете доступ к вектору, когда m уничтожается, и, поскольку вектор пуст, вы создаете исключение (имейте UB, если вы используете []). Это тот же самый сценарий, в который вы попадаете, когда возвращаете vec из create.

Это означает, что ваш деструктор не должен делать предположений о состоянии членов своего класса, поскольку он не знает, в каком состоянии они находятся. Чтобы сделать ваш деструктор «безопасным» для любой версии C++, вам нужно либо поместить вызов at в блоке try-catch или вам нужно проверить размер вектора, чтобы убедиться, что он равен ожидаемому или превышает его.

person NathanOliver    schedule 24.04.2019
comment
Я не очень понимаю ваш пример. Моя версия с Vec v(1); и проблем нет. Я имею в виду, что это может быть не лучший дизайн и напрашиваться на проблемы, но в исходном коде нет UB. - person ead; 24.04.2019
comment
@ead Да, в коде, который вы написали, не было UB. Однако код в моем примере работает даже на C++98. Я указываю, что ваш деструктор неверен для любой версии языка, потому что вы делаете предположение о своем векторе, истинность которого вы не можете знать. Поскольку вы берете любой вектор, включая пустые, это означает, что ваш деструктор неверен, поскольку он безоговорочно обращается к нему. - person NathanOliver; 24.04.2019

"Я просто не могу поверить, что допустимый код становится недействительным..." Да, он действительно может стать недействительным. Другой пример:

#include <iostream>
#include <string>

using namespace std;

template <typename T>
int stoi(const basic_string<T>& str)
{ 
  return 0;
}

int main()
{
  std::string s("-1");
  int i = stoi(s);
  std::cout << s[i];
}

Код действителен в C++98/03, но имеет UB в C++11.


Дело в том, что код, подобный этому или вашему, является экстремальным случаем, который на практике обычно не возникает/не должен возникать. Они (почти) всегда представляют собой очень плохую практику кодирования. Если вы следуете хорошим привычкам программирования, у вас, скорее всего, не возникнет проблем при переходе с C++98 на C++11.

person Daniel Langr    schedule 25.04.2019
comment
@ead Извините, я поменял номера. Исправлено: wandbox.org/permlink/EmDGUD8YmqmzND3l. - person Daniel Langr; 25.04.2019
comment
@DanielLangr Извините, но я не могу понять / понять, почему существует разница между C++ 98 и C++ 11 с этим кодом, несмотря на добавленный вами живой пример, показывающий поведение. Можете ли вы объяснить немного больше, что происходит? - person LoPiTaL; 25.04.2019
comment
@LoPiTaL Разница в том, что в С++ 98/03 нет std::stoi. Он был добавлен в C++11. Следовательно, в C++11 вызывается std::stoi (лучший кандидат на перегрузку), который преобразует строку "-1" в число и возвращает -1. В C++03 единственным кандидатом, который может быть вызван, является наш пользовательский stoi, который возвращает 0. Кстати, этот пример также показывает, почему using namespace std; — чистое зло ;-). - person Daniel Langr; 25.04.2019
comment
возможно, вы правы, а мои ожидания неверны. Я всегда предполагал, что сематика перемещения была введена таким образом, что она не влияет на старый код. - person ead; 25.04.2019
comment
@ead Это было, и это не так. В 99,99 процентах случаев правильно- и хорошо написанный код. - person Daniel Langr; 25.04.2019

Ваш код демонстрирует различное поведение в зависимости от того, применяется ли RVO (скомпилировано без -fno-elide-constructors) или с созданием временного объекта для возврата результата (с -fno-elide-constructors).

С RVO результат одинаков для C++98 и C++11 и равен 42. Но введение временного значения скроет окончательное присвоение 42 в C++98, и функция вернет результат 21. В C В версии ++11 дело идет еще дальше, так как временное создается с семантикой move, поэтому присваивание перемещенному (поэтому пустому) объекту приведет к исключению.

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

person jszpilewski    schedule 24.04.2019
comment
Не уверен, как это отвечает на вопрос. Весь смысл использования -fno-elide-constructors, чтобы избежать неопределенного поведения, на которое вы указываете (см. Также stackoverflow.com/q/52931095/5769463). Проблема в том, что в C++11 он становится UB, а в C++98 код может быть некрасивым, но корректным. - person ead; 24.04.2019
comment
@ead C++98 будет представлять разные результаты в зависимости от параметров компилятора и оптимизации, указанных программистом или только что выбранных компилятором. Хотя это действительно C++, это настоящая головная боль с точки зрения разработки программного обеспечения. С++ 11 ничего не ломает, просто напоминает вам, что вы должны проверить размер массива перед доступом к нему. При этом вы получите результаты, подобные C++98. - person jszpilewski; 24.04.2019