É UB retomar uma rotina de função de membro de um objeto cuja vida útil terminou?

9

Esta pergunta é decorrente deste comentário: Explicação da vida útil do Lambda para corotinas C ++ 20

em relação a este exemplo:

auto foo() -> folly::coro::Task<int> {
    auto task = []() -> folly::coro::Task<int> {
        co_return 1;
    }();
    return task;
}

Portanto, a questão é se a execução da rotina retornada por fooresultaria em UB.

"Chamar" uma função de membro (após o término da vida útil do objeto) é UB: http://eel.is/c++draft/basic.life#6.2

... qualquer ponteiro que represente o endereço do local de armazenamento onde o objeto estará ou foi localizado pode ser usado, mas apenas de maneiras limitadas. [...] O programa tem comportamento indefinido se:

[...]

- o ponteiro é usado para acessar um membro de dados não estáticos ou chamar uma função de membro não estático do objeto , ou

No entanto, neste exemplo:

  • o ()operador do lambda é chamado enquanto a vida útil do lambda ainda é válida
  • É então suspenso,
  • então a lambda é destruída,
  • e, em seguida, a função de membro (operador ()) é retomada em algum momento.

Essa retomada é considerada um comportamento indefinido?

Mike Lui
fonte
2
Talvez a resposta a seguir seja relevante stackoverflow.com/a/60495359/12345656 Parece bem diferente, mas também trata de uma função de membro durante a qual a execução do thisponteiro é invalidada. Considere também a discussão nos comentários.
n314159 11/03

Respostas:

2

[dcl.fct.def.coroutine] p3 :

O tipo de promessa de uma corotina é std::coroutine_traits<R, P1, ..., Pn>::promise_type, onde Ré o tipo de retorno da função, e P1 ... Pné a sequência de tipos dos parâmetros da função, precedida pelo tipo do parâmetro implícito do objeto (12.4.1) se a corotina for não estática função membro.

O parâmetro implícito do objeto é, no seu exemplo, uma referência const e, portanto, essa referência ficará pendente quando a execução for retomada após a destruição do objeto de fechamento.

No entanto, na nota de objetos sendo destruídos durante a execução de uma função membro, isso é realmente bom por si só, e nada além do próprio padrão implica isso em [basic] :

Antes do início da vida útil de um objeto, mas após a alocação do armazenamento que o objeto ocupará ou, após o término da vida útil do objeto e antes da reutilização ou liberação do armazenamento ocupado pelo objeto, qualquer ponteiro que represente o endereço de o local de armazenamento onde o objeto será ou foi localizado pode ser usado, mas apenas de maneiras limitadas. [...]

void B::mutate() {
  new (this) D2;    // reuses storage --- ends the lifetime of *this
  f();              // undefined behavior
  ... = this;       // OK, this points to valid memory
}

(NB: o UB acima é porque o implícito thisnão é lavado e ainda se refere ao parâmetro implícito do objeto.)

Portanto, seu exemplo parece estar bem definido, dependendo da idéia de que a retomada da execução não se enquadre nas mesmas regras que uma invocação original. Observe que a referência ao objeto de fechamento pode estar pendente, mas não é acessada de forma alguma entre a suspensão e a retomada.

Columbo
fonte
Você quer dizer "retomada e conclusão" no final?
Davis Herring
@DavisHerring Não, eu quis dizer especificamente dentro desse período "externo", onde não está claro se a referência pode ser atribuída a uma nova referência etc., o que exigiria um objeto real. O fato de a referência não ser acessada de forma oculta é importante para que isso não seja UB
Columbo
Mas não é suficiente deixar a referência pendente em paz até a retomada; você deve deixá-lo em paz ( por exemplo , no corpo lambda) para sempre - pelo resto de sua vida, que é até a conclusão. Então talvez deva ser "suspensão e conclusão".
Davis Herring
@DavisHerring Mencionei especificamente esse intervalo, porque no nosso exemplo sabemos que o outro é seguro.
Columbo
Certo; Eu apenas acho o texto confuso. Talvez ninguém mais faça.
Davis Herring