Зомби-объекты после std::move

Меня смущает состояние объекта после его перемещения с использованием семантики перемещения C++0x. Насколько я понимаю, после перемещения объекта он по-прежнему является действительным объектом, но его внутреннее состояние было изменено, так что при вызове его деструктора никакие ресурсы не освобождаются.

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

Но этого не происходит, когда я выполняю простой тест:

struct Foo
{
    Foo()  
    {
        s = new char[100]; 
        cout << "Constructor called!" << endl;  
    }

    Foo(Foo&& f) 
    {
        s = f.s;
        f.s = 0;
    }

    ~Foo() 
    { 
        cout << "Destructor called!" << endl;   
        delete[] s; // okay if s is NULL
    }

    void dosomething() { cout << "Doing something..." << endl; }

    char* s;
};

void work(Foo&& f2)
{
    f2.dosomething();
}

int main()
{
    Foo f1;
    work(std::move(f1));
}

Этот вывод:

Constructor called!
Doing something...
Destructor called!

Обратите внимание, что деструктор вызывается только один раз. Это показывает, что мое понимание здесь выключено. Почему деструктор не был вызван дважды? Вот моя интерпретация того, что должно произойти:

  1. Foo f1 построен.
  2. Foo f1 передается в work, который принимает значение f2.
  3. Вызывается конструктор перемещения Foo, который перемещает все ресурсы из f1 в f2.
  4. Теперь вызывается деструктор f2, освобождающий все ресурсы.
  5. Теперь вызывается деструктор f1, который фактически ничего не делает, так как все ресурсы были переданы f2. Тем не менее, деструктор вызывается.

Но поскольку вызывается только один деструктор, ни шаг 4, ни шаг 5 не выполняются. Я сделал обратную трассировку от деструктора, чтобы увидеть, откуда он вызывается, и он вызывается с шага 5. Так почему же не вызывается также деструктор f2?

РЕДАКТИРОВАТЬ: Хорошо, я изменил это, чтобы оно фактически управляло ресурсом. (Внутренний буфер памяти.) Тем не менее, я получаю такое же поведение, когда деструктор вызывается только один раз.


person Channel72    schedule 02.11.2010    source источник
comment
на каком компиляторе вы тестируете?   -  person jalf    schedule 03.11.2010
comment
gcc 4.3 .........................   -  person Channel72    schedule 03.11.2010
comment
В этом случае, возможно, стоит отметить, что он написан для гораздо более старой версии черновика C++0x. И семантика движений с тех пор сильно изменилась. Например, я считаю, что более новый компилятор полностью отклонил бы ваш код, прежде чем вы добавили бы std::move. Он не сможет вызвать work, потому что аргумент фактически является ссылкой lvalue, потому что он был назван.   -  person jalf    schedule 03.11.2010
comment
@jalf, хорошо. Но согласны ли вы со мной, что в текущей версии C++0x деструктор должен вызываться здесь дважды?   -  person Channel72    schedule 03.11.2010
comment
@Channel72: при условии, что ход не оптимизирован, да.   -  person jalf    schedule 03.11.2010
comment
Связано: Как можно использовать перемещенные объекты?   -  person    schedule 03.11.2010
comment
@jalf Обновленный код компилируется правильно и не вызывает конструктор перемещения с GCC 4.9.   -  person Kyle Strand    schedule 26.05.2015
comment
Я сделал несколько простых модификаций этого кода, которые привели к segfault и еще большей путанице с моей стороны:   -  person Kyle Strand    schedule 26.05.2015


Ответы (2)


Изменить (Новый и правильный ответ)
Извините, если присмотреться к коду, кажется, что ответ намного проще: вы никогда не вызываете конструктор перемещения. На самом деле вы никогда не перемещаете объект. Вы просто передаете ссылку rvalue в функцию work, которая вызывает функцию-член для этой ссылки, которая по-прежнему указывает на исходный объект.

Исходный ответ, сохраненный для потомков

Чтобы на самом деле выполнить перемещение, у вас должно быть что-то вроде Foo f3(std::move(f2)); внутри work. Затем вы можете вызвать свою функцию-член на f3, которая является новым объектом, созданным путем перехода от f

Насколько я вижу, вы вообще не понимаете семантику перемещения. Вы просто видите простую старую копию.

чтобы перемещение произошло, вы должны использовать std::move (или, в частности, аргумент, передаваемый конструктору, должен быть безымянным/временным) ссылкой rvalue, такой как та, которая возвращается из std::move). В противном случае он обрабатывается как простая старомодная ссылка lvalue, а затем должно происходить копирование, но, как обычно, компилятору разрешается оптимизировать его, оставляя вам один создаваемый объект и один уничтожаемый объект.

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

Также стоит отметить, что вы используете относительно старый компилятор, и в более ранних версиях спецификации было очень неясно, что должно происходить с этими «зомби-объектами». Так что возможно, что GCC 4.3 просто не вызывает деструктор. Я считаю, что только последняя ревизия или, может быть, предыдущая, явно требует вызова деструктора.

person jalf    schedule 02.11.2010
comment
Хорошо, я добавил явный вызов std::move, просто чтобы быть уверенным, что я действительно передаю rvalue. Но это не меняет поведения. - person Channel72; 03.11.2010
comment
Я бы этого не ожидал. Насколько мне известно, компилятору также разрешено выполнять копирование (или перемещение исключения) для ссылок rvalue: в вашем простом тестовом примере он может просто построить объект на месте, а не создавать его, а затем перемещать Это. - person jalf; 03.11.2010
comment
Хм... хорошо, я отредактировал код так, что теперь Foo фактически управляет ресурсом. Он выделяет буфер и передает право собственности на буфер в конструкторе перемещения. Однако деструктор по-прежнему вызывается только один раз. Я делаю что-то не так здесь? - person Channel72; 03.11.2010
comment
@Chanel72: насколько я понимаю, компилятору по-прежнему ничто не мешает просто оптимизировать перемещение. Зачем вообще нужен исходный объект? Вы просто сразу уходите от него. - person jalf; 03.11.2010
comment
@Chanel72: попробуйте скомпилировать с помощью -S, вы увидите, что на самом деле делается (на ассемблере). - person Vlad; 03.11.2010
comment
Если бы я был вами, я бы определил каждый тип конструктора (включая ctor копирования, который atm генерируется компилятором) для записи некоторого вывода, чтобы вы могли точно видеть, сколько раз вызывается каждый конструктор. - person jalf; 03.11.2010
comment
О, также проверьте мое редактирование. Похоже, ответ намного проще - person jalf; 03.11.2010
comment
Вы действительно должны переместить редактирование наверх. Это проблема, которая решает вопрос. - person SingleNegationElimination; 03.11.2010

Обратите внимание, что компилятор может оптимизировать ненужное построение/разрушение, фактически создавая только один объект. Это особенно верно для ссылок rvalue (которые были придуманы именно для этой цели).

Я думаю, что вы ошибаетесь в своей интерпретации того, что происходит. Конструктор перемещения не вызывается: если значение не является временным, ссылка rvalue ведет себя как обычная ссылка.

Возможно, эта статья содержит больше информации о семантике ссылок rvalue.

person Vlad    schedule 02.11.2010
comment
Даже если я не компилирую с какими-либо флагами оптимизации? - person Channel72; 03.11.2010
comment
@Channel72: стандарт C++ не заботится о флагах оптимизации. Оптимизация либо допустима (она соответствует семантике C++), либо нет. И пока это допустимо, компилятор может применять его, когда захочет, насколько это касается стандарта C++. Конечно, если это противозаконно, его не следует никогда применять. Компилятор обычно выполняет несколько небольших оптимизаций (например, RVO, а на некоторых компиляторах и NRVO) даже при отключенных оптимизациях. Вероятно, он также выполняет исключение копирования, что объясняет ваши результаты. - person jalf; 03.11.2010
comment
Я думаю, что никакого копирования нет, так как в вашем коде фактически нет временных объектов. - person Vlad; 03.11.2010
comment
да... Интересно, почему я не видел этого до сих пор. На самом деле вы никогда не просите о переезде, вы просто передаете рекомендации. Тот факт, что они являются ссылками rvalue вместо lvalue, не меняет того факта, что они по-прежнему являются ссылками, а не новыми объектами. - person jalf; 03.11.2010