Страницы

Поиск по вопросам

четверг, 5 декабря 2019 г.

Порядок уничтожения временных объектов

#cpp #g++ #language_lawyer


Недавно столкнулся с некоторой странностью при уничтожении временных объектов. Собственно,
вопрос следующий: почему объект под номером 2 удаляется раньше объекта под номером
1? Разве не должны выполняться деструкторы аргументов при выходе из области видимости,
то есть при возврате из функции? Почему вначале удаляется временный объект из функции
main, а затем аргумент foo? Почему не наоборот?

Код:

#include 
using std::clog;

struct A {
    static size_t counter;
    size_t me = counter++;
    A() { clog << "constructor A: " << me << "\n"; }
    A(const A& o) { clog << "copy A: " << me << "\n"; }
    A(A&& o) { clog << "move A: " << me << "\n"; }
    ~A() { clog << "destructor A: " << me << "\n"; }
};
size_t A::counter = 0;

A foo(A obj) {
    clog << "into foo\n";
    return obj;
}

int main() {
    A a;
    clog << "before foo\n";
    foo(a);
    clog << "between foo\n";
    A other = foo(a);
    clog << "after foo\n";
}


Вывод:

constructor A: 0
before foo
copy A: 1
into foo
move A: 2
destructor A: 2
destructor A: 1
between foo
copy A: 3
into foo
move A: 4
destructor A: 3
after foo
destructor A: 4
destructor A: 0


Компилятор GCC 5.1.1
    


Ответы

Ответ 1



Деструкторы вызываются в порядке обратном относительно вызывов конструкторов пл принципу стека LIFO (Last Input First Output). Сначало был создан объект с номером 1 посредством вызова конструктора копирования, так как исходный объект это lvalue . В стек также был занесен его деструктор. Затем внутри функции был вызван конструктор перемешения с номером 2 благодаря RVO (return value optimization). Этот конструктор вызван после конструктора параметра, а, следовательно, его деструктор помещен в стек перед деструктором объекта с номером 1. Таким образом последним помещенным в стек, объект с номером 2 удаляется первым. Согласно стандарту C++ (12.2 Temporary objects) 5....The destruction of a temporary whose lifetime is not extended by being bound to a reference is sequenced before the destruction of every temporary which is constructed earlier in the same full-expression. Переводя на русский язык, эта цитата из стандарта говорит о следующем: "Уничтожение временного объекта, чье время жизни не продлевается за счет привязки к ссылке, происходит перед уничтожением каждого временного объекта, который был создан ранее в том же самом полном выражении" Что касается второго предложения A other = foo(a); то временный объект строится непосредственно в объекте other, а потому и удаляется, когда сам этот объект удаляется. Если вы посмотрите на сообщения, которые сосответствуют этому предложению move A: 4 destructor A: 3 after foo destructor A: 4 то вы увидите, что благодаря RVO (return value optimization) локальный объект функции (ее параметр), который является lvalue, сразу же перемещается в объект other. На это указывает сообзения after foo destructor A: 4 То есть этот временный объект был построен в other и, соответственно удален после сообщения after foo когда функция main завершила свою работу.

Ответ 2



Почему у GCC такой порядок я честно говоря не знаю, у MSVC он другой - такой какой напрашивается. Но с моей точки зрения GCC имеет право на подобное поведение, т.к. стандарт говорит следующее: [5.2.2/4]: The lifetime of a parameter ends when the function in which it is defined returns. The initialization and destruction of each parameter occurs within the context of the calling function. Таким образом, параметр будет уничтожен уже после выполненного return в вызывающей функции. Но и возвращаемое значение будет уничтожено сразу по выходу из функции, т.к. выражение на этом заканчивается и все временные объекты из оного подлежат уничтожению. В принципе, возвращаемое значение "моложе" аргумента, а значит должно быть уничтожено до него. С другой стороны, деструктор возвращенного значения можно рассматривать как функцию, которая должна быть вызвана позже удаления аргумента. Таким образом можно считать, что стандарт несколько размыт в данном случае, а можно считать, что поведение GCC более соответствует стандарту. Предлагаю добавить в примеру всего пару строк, и всё станет ещё интереснее: #include using std::clog; struct A { static size_t counter; size_t me = counter++; int a; void foo() { clog << "foo me\n"; } A() { clog << "constructor A: " << me << "\n"; } A(const A& o) { clog << "copy A: " << me << "\n"; } A(A&& o) { clog << "move A: " << me << "\n"; } ~A() { clog << "destructor A: " << me << "\n"; } }; size_t A::counter = 0; A foo(A obj) { clog << "into foo\n"; return obj; } int main() { A a; clog << "before foo\n"; foo(a).foo(); clog << "between foo\n"; A other = foo(a); clog << "after foo\n"; } Вывод MSVC2013(Release): constructor A: 0 before foo copy A: 1 into foo move A: 2 destructor A: 1 foo me destructor A: 2 between foo copy A: 3 into foo move A: 4 destructor A: 3 after foo destructor A: 4 destructor A: 0 GCC/Clang: constructor A: 0 before foo copy A: 1 into foo move A: 2 foo me destructor A: 2 destructor A: 1 between foo copy A: 3 into foo move A: 4 destructor A: 3 after foo destructor A: 4 destructor A: 0 На мой взгляд логике стало ещё меньше и лично мне, вывод MSVC более логичен. Видимо разница вызвана тем, что для MSVC аргумент функции это объект локальный для функции, тогда как для GCC/Clang это объект принадлежащий full expression. А если совместить цитату из моего ответа, с цитатой из ответа Vlad from Moscow, тогда получается, что GCC/Clang всё делают правильно, а MSVC отходит от стандарта. Создал баг-репорт на connect, посмотрим, что ответят. Баг закрыли с формулировкой "by design" и привели ссылку, где указывается, что такое поведение допустимо. Таким образом мы имеем, что MSVC не содержит проблемы, но его поведение отличается от GCC/clang.

Ответ 3



Я подозреваю, что компилятор переписал функцию так: A foo(A obj) { clog << "into foo\n"; A result = std::move(obj); return result; } Clang, во всяком случае, выдает с ней такой же вывод. Тогда если забыть об RVO, то все выглядит логично. Но с RVO все равно у меня в голове не клеется. Скорее всего компилятор очень искусно притворяется, что не делает RVO.

Комментариев нет:

Отправить комментарий