Неожиданный вызов деструктора происходит при сопряжении с конструктором перемещения

Следующий код был скомпилирован и запущен в Visual Studio 2012 Express для Windows Desktop в качестве учебного упражнения.

#include <cstdio>

class X
{
public:
X()  { printf("default constructed\n"); }
~X() { printf("destructed\n");}
X(const X&) { printf("copy constructed\n"); }
X(X&&) { printf("move constructed\n"); }
X & operator= (const X &) { printf("copy assignment operator\n"); }
};

X A() {
X x;
return x;
}

int main() {
{
A();
}
std::getchar();
}

При компиляции с отключенной оптимизацией компилятора (/ Od) результирующий вывод указывает, что деструктор вызывается дважды. Это проблема, учитывая, что построен только один объект. Почему деструктор вызывается дважды? Не будет ли это проблемой, если класс будет управлять своими ресурсами?

default constructed
move constructed
destructed
destructed   <<< Unexpected call

Я попробовал пару экспериментов, чтобы попытаться объяснить результат, но в конечном итоге это не привело к каким-либо полезным объяснениям.

Эксперимент 1. Когда тот же код компилируется с включенной оптимизацией (/ O1 или / O2), в результате получается:

default constructed
destructed

Это указывает на то, что оптимизация именованных возвращаемых значений исключила вызов конструктора перемещения и скрыла основную проблему.

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

default constructed
copy constructed
destructed
destructed

1

Решение

Имейте в виду, что когда объект является источником операции перемещения, он все равно будет уничтожен. Таким образом, источник перемещения должен поставить себя в такое состояние, чтобы при разрушении он не высвобождал ресурсы, которыми он больше не владеет (поскольку они были перемещены в другой объект). Например, любые необработанные указатели (которые теперь будут принадлежать сконструированному объекту перемещения) в исходном объекте должны быть установлены в NULL.

7

Другие решения

X в A уничтожается, когда выходит из области видимости.

A возвращает временный объект (созданный из X конструктором перемещения), который является отдельным экземпляром. Это уничтожено в области действия вызывающего абонента. Это приведет к повторному вызову деструктора (временно).

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

Когда вы передаете rvalue по значению или возвращаете что-либо по значению из функции, компилятор сначала получает возможность удалить копию. Если копия не удалена, но у рассматриваемого типа есть конструктор перемещения, компилятор должен использовать конструктор перемещения.

http://cpp-next.com/archive/2009/09/move-it-with-rvalue-references/

При выходе из области, в которой был создан временный объект, он уничтожается. Если ссылка связана с временным объектом, временный объект уничтожается, когда ссылка выходит из области видимости, если только она не была уничтожена ранее из-за разрыва в потоке управления.

http://publib.boulder.ibm.com/infocenter/comphelp/v8v101/index.jsp?topic=%2Fcom.ibm.xlcpp8a.doc%2Flanguage%2Fref%2Fcplr382.htm

RVO может производить другое поведение от неоптимизированной версии:

Оптимизация возвращаемого значения, или просто RVO, является техникой оптимизации компилятора, которая включает в себя удаление временного объекта, созданного для хранения возвращаемого значения функции. [1] В C ++ он особенно примечателен тем, что ему разрешено изменять наблюдаемое поведение результирующей программы. [2]

http://en.wikipedia.org/wiki/Return_value_optimization

3

Хотя ответы Майкла и jspcal точны, они не ответили на суть моего вопроса, поэтому было сделано два вызова деструкторов. Я ожидал только одного.

Ответ в том, что функция A () возвращает временный объект. Всегда. Именно так работают возвращаемые значения функции, и семантика перемещения не имеет никакого отношения к этому факту. Я предполагаю, что Майкл и jspcal предположили, что я не пропустил такой основной факт. Я отождествил термин «переехал» с понятием «своп». При обмене объекты не создаются и не разрушаются. Следовательно, я ожидал только один вызов деструктора.

Поскольку возвращаемый объект должен быть построен и уничтожен, был сделан второй вызов деструктора (и второй вызов конструктора).

Теперь фактический конструктор, выбранный для выполнения, зависит от того, что предоставлено в определении класса. Если доступен Move-конструктор, он будет вызван. В противном случае будет вызван конструктор копирования.

2
По вопросам рекламы [email protected]