Перемещение конструктора для возвращаемых объектов нарушает код C ++ 98? - PullRequest
8 голосов
/ 24 апреля 2019

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

Похоже, что это может ввести код UB to / break, который был действителен в 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 с этими опциями сборки, чтобы упростить пример. Можно было бы вызвать то же поведение без этих опций с помощью handmade -классы и более сложная create -функция) все в порядке для C ++ 98 (-std=c++98):

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

Результат, как и ожидалось: 21 напечатано ( здесь в прямом эфире ).

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

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

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

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

Ответы [ 3 ]

4 голосов
/ 24 апреля 2019

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

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

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

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

2 голосов
/ 25 апреля 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.

1 голос
/ 24 апреля 2019

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

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

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

...