shared_ptr magic :)

91

O Sr. Lidström e eu tivemos uma discussão :)

A alegação do Sr. Lidström é que uma construção shared_ptr<Base> p(new Derived);não exige que a Base tenha um destruidor virtual:

Armen Tsirunyan : "Sério? O shared_ptr será limpo corretamente? Você poderia, neste caso, demonstrar como esse efeito poderia ser implementado?"

Daniel Lidström : "O shared_ptr usa seu próprio destruidor para excluir a instância Concreta. Isso é conhecido como RAII na comunidade C ++. Meu conselho é que você aprenda tudo que puder sobre RAII. Isso tornará sua codificação C ++ muito mais fácil quando você usar RAII em todas as situações. "

Armen Tsirunyan : "Eu sei sobre RAII e também sei que, eventualmente, o destruidor shared_ptr pode excluir o px armazenado quando pn atingir 0. Mas se px tiver um ponteiro de tipo estático para Basee um ponteiro de tipo dinâmico para Derived, a menos que Basetenha um destruidor virtual, isso resultará em comportamento indefinido. Corrija-me se eu estiver errado. "

Daniel Lidström : "O shared_ptr sabe que o tipo estático é concreto. Ele sabe disso desde que o passei em seu construtor! Parece um pouco mágico, mas posso garantir que é intencional e extremamente bom."

Então, nos julgue. Como é possível (se for) implementar shared_ptr sem exigir que as classes polimórficas tenham um destruidor virtual? desde já, obrigado

Armen Tsirunyan
fonte
3
Você poderia ter vinculado ao tópico original .
Darin Dimitrov,
8
Outra coisa interessante é que shared_ptr<void> p(new Derived)também irá destruir o Derivedobjeto pelo seu destruidor, independente se for virtualou não.
dalle
7
Maneira
5
Mesmo que shared_ptr permita isso, é realmente uma má ideia projetar uma classe como base sem um dtor virtual. Os comentários de Daniel sobre RAII são enganosos - não tem nada a ver com isso - mas a conversa citada parece um simples erro de comunicação (e suposição incorreta de como o shared_ptr funciona).
6
Não RAII, mas sim apaga o destruidor. Você tem que ter cuidado, porque shared_ptr<T>( (T*)new U() )onde struct U:Tnão vai fazer a coisa certa (e isso pode ser feito indiretamente facilmente, como uma função que recebe a T*e é passada a U*)
Yakk - Adam Nevraumont

Respostas:

74

Sim, é possível implementar shared_ptr dessa forma. Boost o faz e o padrão C ++ 11 também requer esse comportamento. Como uma flexibilidade adicional, shared_ptr gerencia mais do que apenas um contador de referência. Um assim chamado deleter é geralmente colocado no mesmo bloco de memória que também contém os contadores de referência. Mas a parte divertida é que o tipo desse deletador não faz parte do tipo shared_ptr. Isso é chamado de "apagamento de tipo" e é basicamente a mesma técnica usada para implementar as "funções polimórficas" boost :: function ou std :: function para ocultar o tipo do functor real. Para fazer seu exemplo funcionar, precisamos de um construtor de modelo:

template<class T>
class shared_ptr
{
public:
   ...
   template<class Y>
   explicit shared_ptr(Y* p);
   ...
};

Então, se você usar isso com suas classes Base e Derivada ...

class Base {};
class Derived : public Base {};

int main() {
   shared_ptr<Base> sp (new Derived);
}

... o construtor de modelo com Y = Derived é usado para construir o objeto shared_ptr. O construtor tem, portanto, a chance de criar o objeto deleter apropriado e contadores de referência e armazena um ponteiro para este bloco de controle como um membro de dados. Se o contador de referência chegar a zero, o deletador criado anteriormente e com reconhecimento de derivado será usado para descartar o objeto.

O padrão C ++ 11 tem o seguinte a dizer sobre este construtor (20.7.2.2.1):

Requer: p deve ser conversível para T*. Ydeve ser um tipo completo. A expressão delete pdeve ser bem formada, deve ter comportamento bem definido e não deve lançar exceções.

Efeitos: constrói um shared_ptrobjeto que possui o ponteirop .

E para o destruidor (20.7.2.2.2):

Efeitos: se *thisestiver vazio ou compartilhar a propriedade com outra shared_ptrinstância ( use_count() > 1), não haverá efeitos colaterais. Caso contrário, se *thispossui um objeto pe um deletador d, d(p)é chamado. Caso contrário, *thispossui um ponteiro pe delete pé chamado.

(ênfase em negrito é minha).

Sellibitze
fonte
the upcoming standard also requires this behaviour: (a) Qual padrão e (b) você pode fornecer uma referência (ao padrão)?
kevinarpe
Eu só quero adicionar um comentário à resposta de @sellibitze, pois não tenho pontos suficientes para add a comment. IMO, é mais do Boost does thisque the Standard requires. Não acho que o Padrão exija isso do que estou entendendo. Falando no exemplo do @sellibitze shared_ptr<Base> sp (new Derived);, Requer de constructorapenas pedir para delete Derivedser bem definido e bem formado. Para a especificação de destructor, há também um p, mas não acho que se refira a pna especificação de constructor.
Lujun Weng
28

Quando shared_ptr é criado, ele armazena um objeto deleter dentro de si. Este objeto é chamado quando o shared_ptr está prestes a liberar o recurso apontado. Como você sabe como destruir o recurso no ponto de construção, pode usar shared_ptr com tipos incompletos. Quem criou o shared_ptr armazenou um deleter correto lá.

Por exemplo, você pode criar um apagador personalizado:

void DeleteDerived(Derived* d) { delete d; } // EDIT: no conversion needed.

shared_ptr<Base> p(new Derived, DeleteDerived);

p irá chamar DeleteDerived para destruir o objeto apontado. A implementação faz isso automaticamente.

Yakov Galka
fonte
4
+1 para a observação sobre tipos incompletos, muito útil ao usar um shared_ptrcomo um atributo.
Matthieu M.
16

Simplesmente,

shared_ptr usa a função deleter especial que é criada pelo construtor que sempre usa o destruidor do objeto dado e não o destruidor do Base, isso é um pouco trabalhoso com a metaprogramação do template, mas funciona.

Algo parecido

template<typename SomeType>
shared_ptr(SomeType *p)
{
   this->destroyer = destroyer_function<SomeType>(p);
   ...
}
Artyom
fonte
1
hmm ... interessante, estou começando a acreditar nisso :)
Armen Tsirunyan,
1
@Armen Tsirunyan Você deve ter olhado a descrição do design do shared_ptr antes de iniciar a discussão. Esta 'captura do deleter' é uma das características essenciais do shared_ptr ...
Paul Michalik
6
@ paul_71: Eu concordo com você. Por outro lado, acredito que essa discussão foi útil não só para mim, mas também para outras pessoas que não sabiam desse fato sobre o shared_ptr. Então eu acho que não foi um grande pecado começar este tópico :)
Armen Tsirunyan
3
@Armen Claro que não. Em vez disso, você fez um bom trabalho ao apontar para esse recurso realmente muito importante de shared_ptr <T> que é frequentemente supervisionado até mesmo por desenvolvedores de c ++ experientes.
Paul Michalik