Как деструктор называется временными объектами, возвращаемыми из функции в С++?
Здесь приведен код из 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, общая идиома в С++.