Skip to content

destructors not always properly run when a coroutine is suspended and then destroyed #63818

@zygoloid

Description

@zygoloid

Example:

#include <coroutine>
#include <iostream>

struct coroutine {
  struct promise_type;
  std::coroutine_handle<promise_type> handle;
  ~coroutine() { handle.destroy(); }
};

struct coroutine::promise_type {
  coroutine get_return_object() {
    return {std::coroutine_handle<promise_type>::from_promise(*this)};
  }
  std::suspend_never initial_suspend() noexcept { return {}; }
  std::suspend_never final_suspend() noexcept { return {}; }
  void return_void() {}
  void unhandled_exception() {}
};

struct Printy {
  Printy(const char *name) : name(name) { std::cout << "Printy(" << name << ")\n"; }
  Printy(const Printy&) = delete;
  ~Printy() { std::cout << "~Printy(" << name << ")\n"; }
  const char *name;
};

int main() {
  [] -> coroutine {
    Printy a("a");
    Printy arr[] = {
      Printy("b"), Printy("c"),
      (co_await std::suspend_always{}, Printy("d")),
      Printy("e")
    };
  }();
}

When the coroutine is destroyed after being suspended, a is destroyed, but arr[0] and arr[1] are not. Clang does not in general properly create cleanups for non-exceptional control flow that occurs in the middle of an expression / an initializer. At least array initialization is missing cleanups here, but it'd be worth checking through all exception-only cleanups because most of them are probably incorrect. It looks like there are 15 places where we currently push an EH-only cleanup:

CGCall.cpp:      pushFullExprCleanup<DestroyUnpassedArg>(EHCleanup, Slot.getAddress(),
CGClass.cpp:    CGF.EHStack.pushCleanup<CallBaseDtor>(EHCleanup, BaseClassDecl,
CGClass.cpp:    EHStack.pushCleanup<CallDelegatingCtorDtor>(EHCleanup,
CGCoroutine.cpp:    EHStack.pushCleanup<CallCoroEnd>(EHCleanup);
CGDecl.cpp:  pushDestroy(EHCleanup, addr, type, getDestroyer(dtorKind), true);
CGDecl.cpp:  pushFullExprCleanup<IrregularPartialArrayDestroy>(EHCleanup,
CGDecl.cpp:  pushFullExprCleanup<RegularPartialArrayDestroy>(EHCleanup,
CGException.cpp:  pushFullExprCleanup<FreeException>(EHCleanup, addr.getPointer());
CGExprAgg.cpp:        CGF.pushDestroy(EHCleanup, LV.getAddress(CGF), CurField->getType(),
CGExprAgg.cpp:        CGF.pushDestroy(EHCleanup, LV.getAddress(CGF), field->getType(),
CGExprCXX.cpp:      .pushCleanupWithExtra<DirectCleanup>(EHCleanup,
CGExprCXX.cpp:    .pushCleanupWithExtra<ConditionalCleanup>(EHCleanup,
ItaniumCXXABI.cpp:    CGF.EHStack.pushCleanup<CallGuardAbort>(EHCleanup, guard);
MicrosoftCXXABI.cpp:    CGF.EHStack.pushCleanup<ResetGuardBit>(EHCleanup, GuardAddr, GuardNum);
MicrosoftCXXABI.cpp:    CGF.EHStack.pushCleanup<CallInitThreadAbort>(EHCleanup, GuardAddr);

This bug is not new with coroutines; the same thing happens with statement expressions:

int main() {
  Printy arr[] = { Printy("a"), ({ return 0; Printy("b"); }) };
}

... never destroys arr[0]. But it seems more pressing now that it's reachable from standard C++20 code.

Metadata

Metadata

Assignees

Type

No type

Projects

Status

Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions