Как вызывается деструктор для временных объектов, возвращаемых функцией в C++?

Вот код из «Языка программирования C++» Страуструпа, который реализует finally, который я не могу понять, где вызывается деструктор.

template<typename F> struct Final_action
{
  Final_action(F f): clean{f} {}
  ~Final_action() { clean(); }
  F clean;
}

template<class F> 
Final_action<F> finally(F f)
{
  return Final_action<F>(f);
}

void test(){
  int* p=new int{7};
  auto act1 = finally( [&]{delete p;cout<<"Goodbye,cruel world\n";} );
}

У меня есть два вопроса по этому поводу:

  1. По словам автора, delete p вызывается только один раз: когда акт1 выходит за рамки. Но насколько я понимаю: сначала act1 будет инициализирован конструктором копирования, затем временный объект Final_action<F>(f) в функции finally будет уничтожен, вызывая delete p в первый раз, затем второй раз в конце функции test, когда act1 отсутствует масштаба. Где я ошибаюсь?

  2. Зачем нужна функция finally? Могу я просто определить Final_action act1([&]{delete p;cout<<"Goodbye,cruel world\n"})? Это то же самое?

Кроме того, если кто-то может придумать лучшее название, пожалуйста, измените текущее.

ОБНОВЛЕНИЕ: после некоторых размышлений я теперь убежден, что деструктор может быть вызван трижды. Дополнительный объект предназначен для временного объекта, автоматически сгенерированного в вызывающей функции void test(), который используется в качестве аргумента для конструктора копирования act1. Это можно проверить с помощью опции -fno-elide-constructors в g++. Для тех, у кого есть тот же вопрос, что и у меня, см. Копировать elision, а также Оптимизация возвращаемого значения, как указано в ответе Билла Линча.


person btshengsheng    schedule 17.09.2015    source источник
comment
Один вопрос на вопрос, пожалуйста.   -  person Lightness Races in Orbit    schedule 17.09.2015
comment
re #2 Я не понимаю, почему бы и нет. Я думаю, кто-то, одержимый auto, подумал, что это легче читать.   -  person Lightness Races in Orbit    schedule 17.09.2015
comment
@LightnessRacesinOrbit Вывод аргумента шаблона. На самом деле вы не можете написать тип Final_action<???> из-за лямбда.   -  person Quentin    schedule 17.09.2015
comment
@Quentin: Ха-ха, о да   -  person Lightness Races in Orbit    schedule 17.09.2015
comment
Прочтите главу еще раз. Конструктор копирования и operator= настроены на удаление, чтобы предотвратить копирование Final_action. Кроме того, в вашем примере кода отсутствует по крайней мере одна точка с запятой после cout   -  person Simon Kraemer    schedule 17.09.2015
comment
@Quentin Есть ли у лямбды тип? Это тот же тип, что и указатель на функцию? Может глупый вопрос, я новичок :D   -  person btshengsheng    schedule 17.09.2015
comment
Обновление: Это против Это   -  person Simon Kraemer    schedule 17.09.2015
comment
@btshengsheng лямбда-выражение создает объект функции (функтор) типа, который генерируется компилятором. Имя типа программисту неизвестно, но в остальном это обычный класс с operator ().   -  person Quentin    schedule 17.09.2015
comment
@btshengsheng re #2 Вы можете это сделать, но это будет очень чудовищно: Final_action‹std::function‹void()› › act1([&](){ delete p; cout‹‹Goodbye,cruel worldv1\n; }); а также необходимо включить ‹functional›   -  person POTEMKINDX    schedule 17.09.2015
comment
@POTEMKINDX Пожалуйста, не надо. Сравните это и это   -  person Simon Kraemer    schedule 17.09.2015
comment
@POTEMKINDX, это было бы злоупотреблением std::function.   -  person Quentin    schedule 17.09.2015


Ответы (3)


Вы правы, этот код не работает. Он работает корректно, только если применяется оптимизация возвращаемого значения. Эта строка:

auto act1 = finally([&]{delete p;cout<<"Goodbye,cruel world\n"})

Может или не может вызывать конструктор копирования. Если это так, то у вас будет два объекта типа Final_action, и вы, таким образом, дважды вызовете эту лямбду.

person Bill Lynch    schedule 17.09.2015
comment
Вероятно, кто-то хорошо говорит, что так это работает в моей системе, и не заботится о написании даже удаленно переносимого кода... или даже кода, который не работает должным образом :( - person Lightness Races in Orbit; 17.09.2015
comment
Примечание. Чтобы исправить это, сделайте Final_action типом только для перемещения. - person Quentin; 17.09.2015
comment
@Quentin: еще более простое исправление: auto&& -- продлевает срок службы временного файла до соответствия act1, предотвращает любое копирование - person Ben Voigt; 17.09.2015
comment
@BenVoigt Это сработает, но оставит мину в поле. - person Quentin; 17.09.2015
comment
@Quentin: Что ж, операции копирования и перемещения в Final_action должны быть удалены, потому что ни то, ни другое не имеет смысла (экземпляр, перемещенный из, также взорвется, если этот класс не будет полностью переписан). - person Ben Voigt; 17.09.2015
comment
Похоже, глава получила обновление: Здесь против Здесь - person Simon Kraemer; 17.09.2015
comment
Для № 2, о котором вы, возможно, захотите рассказать в своем ответе, у вас может быть auto deleter = [&] { /* bla bla */ }; Final_action<decltype(deleter)> act1{ deleter}; - person edmz; 17.09.2015
comment
@BenVoigt Если удалены операции копирования и перемещения для Final_action, как будет возвращен объект? - person btshengsheng; 18.09.2015
comment
@SimonKraemer Обновленный код также неверен, см. ответ Арне Фогеля. - person btshengsheng; 19.09.2015
comment
@bsthengsheng Я не проверял это, но спасибо, что указали на это. - person Simon Kraemer; 20.09.2015

Самое простое исправление

template<typename F> 
struct Final_action
{
  Final_action(F f): clean{std::move(f)} {}
  Final_action(const Final_action&) = delete;
  void operator=(const Final_action&) = delete;
  ~Final_action() { clean(); }
  F clean;
};

template<class F> 
Final_action<F> finally(F f)
{
  return { std::move(f) };
}

И использовать как

auto&& act1 = finally( [&]{delete p;cout<<"Goodbye,cruel world\n";} );

Использование инициализации списка копирования и ссылки на переадресацию с продлением срока службы позволяет избежать копирования/перемещения объекта Final_action. Copy-list-initialization создает временное возвращаемое значение Final_action напрямую, а временное значение, возвращаемое finally, продлевается за счет привязки к act1 — также без какого-либо копирования или перемещения.

person T.C.    schedule 19.09.2015
comment
Работает отлично! Для тех, кому интересно, фигурная скобка в операторе return return { std::move(f) } вызывает инициализацию списка копирования, см. здесь. - person btshengsheng; 19.09.2015

Код сломан. Пересмотренный код, упомянутый Саймоном Кремером, также не работает — он не компилируется (оператор return в finally недопустим, поскольку Final_action нельзя ни копировать, ни перемещать). Создание Final_action только для перемещения с помощью сгенерированного конструктора перемещения также не работает, потому что F гарантированно имеет конструктор перемещения (если его нет, то сгенерированный конструктор перемещения Final_action будет молча использовать конструктор копирования F в качестве запасного варианта), а также не гарантируется F быть бездействующим после переезда. На самом деле лямбда из примера не превратится в неоперативную.

Существует относительно простое и портативное решение:

Добавьте флаг bool valid = true; к Final_action и перезапишите команду перемещения и назначение перемещения, чтобы снять флажок в исходном объекте. Вызывать clean() только если valid. Это предотвращает создание копии и назначение копии, поэтому их не нужно явно удалять. (Дополнительные баллы: поместите флаг в многократно используемую оболочку только для перемещения, чтобы вам не пришлось реализовывать команду перемещения и назначение перемещения Final_action. В этом случае вам также не нужны явные удаления.)

В качестве альтернативы удалите аргумент шаблона Final_action и измените его, чтобы вместо него использовалось std::function<void()>. Перед вызовом убедитесь, что clean не пусто. Добавьте c'tor перемещения и назначение перемещения, которое устанавливает исходный std::function на nullptr. (Да, это необходимо для переносимости. Перемещение std::function не гарантирует, что исходный код будет пуст.) Преимущество: обычные преимущества стирания типов, такие как возможность вернуть защиту области видимости в кадр внешнего стека без раскрытия F. Недостаток: может значительно увеличить время выполнения.

В моем текущем рабочем проекте я в основном объединил два подхода с ScopeGuard<F> и AnyScopeGuard, используя объект функции стирания типа. Первый использует boost::optional<F> и может быть преобразован во второй. В качестве дополнительного преимущества, позволяющего оставлять охранники области видимости пустыми, я также могу явно dismiss() их использовать. Это позволяет использовать защиту области действия для настройки откатной части транзакции, а затем отклонить ее при фиксации (с кодом без выбрасывания).

ОБНОВЛЕНИЕ: новый пример Страуструпа даже не компилируется. Я пропустил, что явное удаление c'tor копии также отключает создание c'tor перемещения.

person Arne Vogel    schedule 17.09.2015
comment
Пересмотренный код SimonKraemer действительно не будет компилироваться (я тестировал с помощью g++), потому что, как вы сказали, конструктор перемещения не будет генерироваться автоматически, когда конструктор копирования явно объявлен (как удаленный) , см. здесь. - person btshengsheng; 19.09.2015
comment
Прочитав ваш первый абзац 6 раз, я думаю, что наконец полностью понял, что вы имеете в виду :-). Я предполагаю, что вы пропустили ничего в четвертой строке своего ответа? Кстати, довольно интересно видеть, как Страуструп снова и снова создает неверный код... Спасибо. - person btshengsheng; 19.09.2015