Enable_shared_from_this deve ser a primeira classe base?

8

Minha classe herda de várias bases, uma das quais é std::enable_shared_from_this. Deve ser a primeira base?

Suponha o seguinte código de exemplo:

struct A { ~A(); };
struct B { ~B(); };
struct C : A, B, std::enable_shared_from_this<C> {};

std::make_shared<C>(); 

Quando ~A()e ~B()executar, posso ter certeza de que o armazenamento onde Cmorava ainda está presente?

Filipp
fonte
1
Por que você sente a ordem da destruição importa? O destruidor de std::enable_shared_from_thisnão faz muito. Seu exemplo parece bom para mim (supondo que você não esteja tentando fazer algo inteligente ~Ae ~B, como thisC*
rebaixamento
1
@ SM Este não é um problema de acesso. Eu sei que enable_shared_from_thisdeve ser uma base inequívoca acessível. No meu exemplo, é. Cé uma estrutura. Herda publicamente.
Filipp 15/03
1
Sim, mas o acesso à base é determinado pela coisa que está herdando, e não pela coisa que está sendo herdada. Eu posso mudar meu exemplo, se você quiser. O código real no qual se baseia usa classe public. Eu escolhi structo exemplo para evitar digitar.
Filipp 15/03
4
Assim falou The Standard: " [util.smartptr.weak.dest] ~weak_ptr(); Efeitos: destrói este weak_ptrobjeto, mas não tem efeito no objeto para o qual o ponteiro armazenado aponta." Ênfase minha.
Igor Tandetnik
1
@Filipp A vida útil do objeto armazenado termina quando o último shared_ptrmorre. Mesmo que isso weak_ptrevite que o bloco de controle seja desalocado, acho que não importa.
HolyBlackCat 15/03

Respostas:

1

Quando ~A()e ~B()executar, posso ter certeza de que o armazenamento onde Cmorava ainda está presente?

Claro! Seria difícil usar uma classe base que tenta liberar sua própria memória (a memória em que reside). Não tenho certeza se é formalmente legal.

As implementações não fazem isso: quando a shared_ptr<T>é destruída ou redefinida, a contagem de referência (RC) da propriedade compartilhada de Té diminuída (atomicamente); se alcançou 0 no decremento, a destruição / exclusão de Té iniciada.

Então, a contagem de proprietários fracos-ou-T-existe é diminuída (atomicamente), como Tnão existe mais: precisamos saber se somos a última entidade interessada no bloco de controle; se o decréscimo der um resultado diferente de zero, significa weak_ptrque existem alguns que compartilham (podem ser 1 compartilhamento ou 100%) de propriedade do bloco de controle e agora são responsáveis ​​pela desalocação.

De qualquer forma, em algum momento, o decremento atômico terminará com um valor zero, para o último coproprietário.

Aqui não há tópicos, não-determinismo e, obviamente, o último weak_ptr<T>foi destruído durante a destruição de C. (A suposição não escrita em sua pergunta é que nenhum outro weak_ptr<T>foi mantido.)

A destruição sempre acontece nessa ordem exata . O bloco de controle é usado para destruição, pois não se shared_ptr<T>sabe (em geral) qual destruidor (potencialmente não virtual) da classe (potencialmente diferente) mais derivada a ser chamada . (O bloco de controle também sabe não desalocar a memória na contagem compartilhada atingindo zero para make_shared.)

A única variação prática entre implementações parece ser sobre os detalhes finos de cercas de memória e evitar algumas operações atômicas em casos comuns.

curiousguy
fonte
Esta é a resposta que eu estava procurando! Obrigado! A chave é que o objeto que está vivo realmente conta como um uso fraco implícito. Ou seja, weak_counté 1 para um objeto que foi make_sharedeliminado, mesmo que não haja weak_ptrs. Liberando apenas os shared_ptrprimeiros decrementos use_count. Se ele se tornou 0, o objeto (mas não o bloco de controle) é destruído. Então weak_count é decrementado e, se 0, o bloco de controle é destruído + liberado. Um objeto que herda de enable_shared_from_thiscomeça com weak_count= 2. Uma solução brilhante por implementadores de STL, conforme o esperado.
Filipp 18/03
Apenas um pedante: STL é a Standard Template Library, que, exceto para artefatos históricos (o HP STL ou o SGI STL), é apenas definida informalmente; trata-se de tipos que seguem os requisitos de contêineres, iteradores e os "algoritmos" que trabalham neles. O STL não é estritamente limitado a modelos, pois usa algumas classes que não são modelos (por exemplo, random_access_iterator_tag). Existe um acordo informal para chamar qualquer coisa relacionada a contêineres como parte do STL. tl; dr: nem todos os modelos na lib std fazem parte do STL e nem todos os não modelos estão fora dele.
curiousguy
5

Quando ~ A () e ~ B () são executados, posso ter certeza de que o armazenamento em que C morava ainda está presente?

Não, e a ordem das classes base é irrelevante. Até o uso (ou não) de enable_shared_from_this é irrelevante.

Quando um objeto C é destruído (não importa o que aconteça), ~C()ele será chamado antes de ambos ~A()e ~B(), dessa maneira, os destruidores de base funcionam. Se você tentar "reconstruir" o objeto C no destruidor de base e acessar os campos nele, esses campos já foram destruídos, portanto, você terá um comportamento indefinido.

Chris Dodd
fonte
Não responde minha pergunta. Em nenhum lugar mencionei que a tentativa de "reconstruir" qualquer C. A resposta deve ser uma das " enable_shared_from_thispode aparecer em qualquer lugar da lista base, são necessárias implementações para liberar memória após a destruição de todo o objeto, independentemente de como ele seja herdado de enable_shared_from_this" ou " deve ser a primeira base, herdando outro lugar como UB "ou" Esse comportamento não é especificado ou a qualidade da implementação ".
Filipp 16/03
@Filipp: A resposta é uma combinação - eles podem aparecer em qualquer lugar e, independentemente, uma implementação é livre para liberar memória para parte de um objeto após a destruição dessa parte do objeto (e antes da destruição das classes base). Simplesmente não há exigência de que a memória só possa ser liberada após a destruição de todo o objeto, independentemente.
Chris Dodd
-1

Se você criar um objeto c do tipo C, com as bases A, B e um contador de referência através da herança da base enable_shared_from_this<T>, primeiro a memória será alocada para todo o objeto resultante, incluindo as bases em geral e a base enable_shared_from_this<T>. O objeto não será destruído até que o último proprietário (também conhecido como shared_ptr) renuncie à propriedade. Nesse momento ~ enable_shared ..., ~ B e ~ A serão executados após ~ C. A memória alocada completa ainda está garantida para estar lá até após a execução do último destruidor ~ A. Depois que ~ A é executado, a memória completa do objeto é liberada de uma só vez. Então, para responder sua pergunta:

Quando ~ A () e ~ B () são executados, posso ter certeza de que o armazenamento em que C morava ainda está presente?

Sim, embora você não possa acessá-lo legalmente, mas por que você precisaria saber? Que problema você está tentando evitar?

Andreas_75
fonte
O que você escreveu é verdade, mas não responde à minha pergunta. É claro que os destruidores da classe base correm atrás da classe derivada. Estou perguntando se as implementações de shared_ptr, weak_ptre enable_shared_from_thissão necessárias para manter a memória por tempo suficiente para tornar isso seguro, mesmo quando essa enable_shared_from_thisnão é a primeira base.
Filipp 16/03
Ah ok. Analisando sua pergunta original: não há um claro 'isto' [como em "faça com que isso seja salvo" no comentário acima] que você está tentando alcançar. Editarei minha resposta para refletir a pergunta como eu a entendo neste momento.
Andreas_75 16/03
Torne isso seguro = herdando de enable_shared_from_thisdepois de outra classe base.
Filipp 16/03