Encontrei algum código usando std :: shared_ptr para executar uma limpeza arbitrária no desligamento. No começo, achei que esse código não funcionaria, mas tentei o seguinte:
#include <memory>
#include <iostream>
#include <vector>
class test {
public:
test() {
std::cout << "Test created" << std::endl;
}
~test() {
std::cout << "Test destroyed" << std::endl;
}
};
int main() {
std::cout << "At begin of main.\ncreating std::vector<std::shared_ptr<void>>"
<< std::endl;
std::vector<std::shared_ptr<void>> v;
{
std::cout << "Creating test" << std::endl;
v.push_back( std::shared_ptr<test>( new test() ) );
std::cout << "Leaving scope" << std::endl;
}
std::cout << "Leaving main" << std::endl;
return 0;
}
Este programa fornece a saída:
At begin of main.
creating std::vector<std::shared_ptr<void>>
Creating test
Test created
Leaving scope
Leaving main
Test destroyed
Eu tenho algumas idéias sobre por que isso pode funcionar, que tem a ver com os internos de std :: shared_ptrs, conforme implementado para o G ++. Como esses objetos envolvem o ponteiro interno junto com o contador, a conversão de std::shared_ptr<test>
para std::shared_ptr<void>
provavelmente não está atrapalhando a chamada do destruidor. Esta suposição está correta?
E, é claro, a pergunta muito mais importante: isso garante o funcionamento do padrão ou pode mudar ainda mais os internos do std :: shared_ptr, outras implementações realmente quebram esse código?
fonte
Respostas:
O truque é que
std::shared_ptr
executa o apagamento do tipo. Basicamente, quando um novoshared_ptr
é criado, ele armazena internamente umadeleter
função (que pode ser fornecida como argumento ao construtor, mas, se não houver, o padrão é chamardelete
). Quando oshared_ptr
é destruído, chama a função armazenada e chama odeleter
.Um esboço simples do apagamento de tipo simplificado com a função std :: e evitando toda a contagem de referências e outros problemas podem ser vistos aqui:
Quando um
shared_ptr
é copiado (ou construído por padrão) de outro, o deleter é repassado, de modo que, quando você constrói umshared_ptr<T>
de,shared_ptr<U>
as informações sobre o destruidor a chamar também são repassadas nodeleter
.fonte
my_shared
. Eu consertaria isso, mas não tenho privilégio de editar ainda.std::shared_ptr<void>
evito declarar uma classe de invólucro inútil apenas para que eu possa herdá-la de uma determinada classe base.my_unique_ptr
. Quandomain
o modelo é instanciado,double
o deleter correto é escolhido, mas isso não faz parte do tipomy_unique_ptr
e não pode ser recuperado do objeto. O tipo do deletador é apagado do objeto, quando uma função recebe umamy_unique_ptr
(digamos, por referência de valor-r), essa função não sabe nem precisa saber o que é o deletador.shared_ptr<T>
logicamente [*] possui (pelo menos) dois membros de dados relevantes:A função deleter da sua
shared_ptr<Test>
, dada a maneira como você a construiu, é a função normal paraTest
, que converte o ponteiro emTest*
edelete
s.Quando você insere seu
shared_ptr<Test>
vetorshared_ptr<void>
, ambos são copiados, embora o primeiro seja convertido emvoid*
.Portanto, quando o elemento vetorial é destruído levando a última referência, ele passa o ponteiro para um deleter que o destrói corretamente.
Na verdade, é um pouco mais complicado do que isso, porque
shared_ptr
pode levar um functor deleter em vez de apenas uma função; portanto, pode até haver dados por objeto a serem armazenados, em vez de apenas um ponteiro de função. Mas, nesse caso, não existem dados extras, seria suficiente armazenar um ponteiro na instanciação de uma função de modelo, com um parâmetro de modelo que captura o tipo pelo qual o ponteiro deve ser excluído.[*] logicamente no sentido de ter acesso a eles - eles podem não ser membros do shared_ptr em si, mas em vez de algum nó de gerenciamento para o qual ele aponta.
fonte
shared_ptr
diretamente com o tipo apropriado ou se você usarmake_shared
. Mas, ainda assim, é uma boa idéia como o tipo do ponteiro pode mudar de construção até que seja armazenada noshared_ptr
:base *p = new derived; shared_ptr<base> sp(p);
, na medida em queshared_ptr
está em causa o objeto ébase
nãoderived
, então você precisa de um destrutor virtual. Esse padrão pode ser comum aos padrões de fábrica, por exemplo.Funciona porque usa apagamento de tipo.
Basicamente, quando você cria um
shared_ptr
, ele passa um argumento extra (que você pode realmente fornecer, se desejar), que é o função deleter.Esse functor padrão aceita como argumento um ponteiro para o tipo que você usa no
shared_ptr
, portanto,void
aqui, lança-o adequadamente no tipo estático que você usoutest
aqui e chama o destruidor neste objeto.Qualquer ciência suficientemente avançada parece mágica, não é?
fonte
O construtor
shared_ptr<T>(Y *p)
realmente parece estar chamandoshared_ptr<T>(Y *p, D d)
whered
é um deleter gerado automaticamente para o objeto.Quando isso acontece, o tipo do objeto
Y
é conhecido; portanto, o deletador desseshared_ptr
objeto sabe qual destruidor chamar e essas informações não são perdidas quando o ponteiro é armazenado em um vetor deshared_ptr<void>
.De fato, as especificações exigem que, para que um
shared_ptr<T>
objeto recebedor aceite umshared_ptr<U>
objeto, deve ser verdade queU*
deve ser implicitamente convertível em aeT*
este é certamente o caso,T=void
pois qualquer ponteiro pode ser convertido em umvoid*
implicitamente. Nada é dito sobre o deleter que será inválido; portanto, as especificações exigem que isso funcione corretamente.Tecnicamente, o IIRC a
shared_ptr<T>
mantém um ponteiro para um objeto oculto que contém o contador de referência e um ponteiro para o objeto real; armazenando o deleter nessa estrutura oculta, é possível fazer com que esse recurso aparentemente mágico funcione, mantendoshared_ptr<T>
o tamanho de um ponteiro comum (no entanto, desmarcando o ponteiro requer uma dupla indireçãofonte
Test*
é implicitamente conversível emvoid*
, portanto,shared_ptr<Test>
implicitamente conversível emshared_ptr<void>
, da memória. Isso funciona porqueshared_ptr
é projetado para controlar a destruição no tempo de execução, e não no tempo de compilação, eles usarão internamente a herança para chamar o destruidor apropriado como era no tempo de alocação.fonte
Vou responder a essa pergunta (2 anos depois) usando uma implementação muito simplista do shared_ptr que o usuário entenderá.
Em primeiro lugar, vou a algumas classes secundárias, shared_ptr_base, sp_counted_base sp_counted_impl, e verifiquei_determine o último dos quais é um modelo.
Agora vou criar duas funções "gratuitas", chamadas make_sp_counted_impl, que retornarão um ponteiro para um recém-criado.
Ok, essas duas funções são essenciais para o que acontecerá a seguir quando você criar um shared_ptr por meio de uma função de modelo.
Observe o que acontece acima se T for nulo e U for sua classe "test". Ele chamará make_sp_counted_impl () com um ponteiro para U, não um ponteiro para T. O gerenciamento da destruição é feito aqui. A classe shared_ptr_base gerencia a contagem de referência em relação à cópia e atribuição, etc. A própria classe shared_ptr gerencia o uso seguro de sobrecargas do operador (->, * etc).
Portanto, embora você tenha um shared_ptr para anular, abaixo você está gerenciando um ponteiro do tipo que passou para novo. Observe que, se você converter seu ponteiro em um vácuo * antes de colocá-lo no shared_ptr, ele falhará na compilação no selected_delete, para que você também esteja seguro lá.
fonte