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

Здесь приведен код из Stroustrup "Язык программирования С++", который реализует 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";} );
}

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

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

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

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

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

Ответы

Ответ 1

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

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

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

Ответ 2

Код сломан. Измененный код, упомянутый SimonKraemer, также нарушен - он не компилируется (оператор return в finally является незаконным, поскольку Final_action не является ни копируемым, ни перемещаемым). Создание Final_action move-only с сгенерированным конструктором перемещения также не работает, потому что F гарантированно имеет конструктор перемещения (если он не имеет значения, то конструктор перемещения Final_action без использования шрифта будет использовать конструктор F copy как резерв), равно как F гарантированно будет no-op после переезда. Фактически, lambda из примера не превратится в no-op.

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

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

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

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

UPDATE: новый пример Stroustrup даже не компилируется. Я пропустил, что явное удаление копии c'tor также отключает генерацию перемещения c'tor.

Ответ 3

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

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 инициализирует временное возвращаемое значение Final_action, а временное значение, возвращаемое finally, продлевается продолжительностью при привязке к act1 - также без какого-либо копирования или перемещения.

Ответ 4

Он вызывается, как только переменная act1 больше не находится в области видимости. Это пример RAII, общая идиома в С++.