Por que shared_ptr <void> é legal, enquanto unique_ptr <void> é malformado?

100

A questão cabe mesmo no título: tenho curiosidade de saber qual é a razão técnica para esta diferença, mas também a lógica?

std::shared_ptr<void> sharedToVoid; // legal;
std::unique_ptr<void> uniqueToVoid; // ill-formed;
Anúncio N
fonte

Respostas:

118

É porque std::shared_ptrimplementa o apagamento de tipo, enquanto std::unique_ptrnão.


Uma vez que std::shared_ptrimplementa o apagamento de tipo, ele também suporta outra propriedade interessante, viz. ele não precisa do tipo de deletador como argumento de tipo de modelo para o modelo de classe. Veja suas declarações:

template<class T,class Deleter = std::default_delete<T> > 
class unique_ptr;

que tem Deletercomo parâmetro de tipo, enquanto

template<class T> 
class shared_ptr;

não tem.

Agora a questão é: por que shared_ptrimplementar a eliminação de tipo? Bem, ele faz isso, porque tem que suportar a contagem de referência, e para suportar isso, ele tem que alocar memória do heap e, uma vez que tem que alocar memória de qualquer maneira, ele vai um passo adiante e implementa o apagamento de tipo - que precisa do heap alocação também. Então, basicamente, é apenas ser oportunista!

Devido ao apagamento de tipo, std::shared_ptré capaz de suportar duas coisas:

  • Ele pode armazenar objetos de qualquer tipo void*, mas ainda assim é capaz de excluir os objetos em destruição apropriadamente invocando corretamente seu destruidor .
  • O tipo de deleter não é passado como argumento de tipo para o modelo de classe, o que significa um pouco de liberdade sem comprometer a segurança de tipo .

Tudo bem. Isso é tudo sobre como std::shared_ptrfunciona.

Agora a questão é: pode std::unique_ptrarmazenar objetos como void* ? Bem, a resposta é sim - desde que você passe um deletor adequado como argumento. Aqui está uma dessas demonstrações:

int main()
{
    auto deleter = [](void const * data ) {
        int const * p = static_cast<int const*>(data);
        std::cout << *p << " located at " << p <<  " is being deleted";
        delete p;
    };

    std::unique_ptr<void, decltype(deleter)> p(new int(959), deleter);

} //p will be deleted here, both p ;-)

Resultado ( demonstração online ):

959 located at 0x18aec20 is being deleted

Você fez uma pergunta muito interessante no comentário:

No meu caso, vou precisar de um apagador de apagamento de tipo, mas também parece possível (ao custo de alguma alocação de heap). Basicamente, isso significa que existe realmente um nicho para um terceiro tipo de ponteiro inteligente: um ponteiro inteligente de propriedade exclusiva com eliminação de tipo.

para o qual @Steve Jessop sugeriu a seguinte solução,

Na verdade, eu nunca tentei isso, mas talvez você pudesse conseguir isso usando um apropriado std::functioncomo o tipo de exclusão com unique_ptr? Supondo que isso realmente funcione, então você está pronto, propriedade exclusiva e um apagador apagado.

Seguindo essa sugestão, implementei (embora não faça uso std::function, pois não parece necessário):

using unique_void_ptr = std::unique_ptr<void, void(*)(void const*)>;

template<typename T>
auto unique_void(T * ptr) -> unique_void_ptr
{
    return unique_void_ptr(ptr, [](void const * data) {
         T const * p = static_cast<T const*>(data);
         std::cout << "{" << *p << "} located at [" << p <<  "] is being deleted.\n";
         delete p;
    });
}

int main()
{
    auto p1 = unique_void(new int(959));
    auto p2 = unique_void(new double(595.5));
    auto p3 = unique_void(new std::string("Hello World"));
}  

Resultado ( demonstração online ):

{Hello World} located at [0x2364c60] is being deleted.
{595.5} located at [0x2364c40] is being deleted.
{959} located at [0x2364c20] is being deleted.

Espero que ajude.

Nawaz
fonte
13
Boa resposta, +1. Mas você pode torná-lo ainda melhor mencionando explicitamente que std::unique_ptr<void, D>ainda é possível fornecendo um adequado D.
Angew não está mais orgulhoso de SO
1
@Angrew: Boa, você encontrou a verdadeira questão subjacente que não estava escrita na minha pergunta;)
Anúncio N
@Nawaz: Obrigado. No meu caso, vou precisar de um apagador de apagamento de tipo, mas também parece possível (ao custo de alguma alocação de heap). Basicamente, isso significa que há realmente um nicho para um terceiro tipo de ponteiro inteligente: um ponteiro inteligente de propriedade exclusiva com eliminação de tipo?
Anúncio N de
8
@AdN: Na verdade, nunca tentei isso, mas talvez você pudesse conseguir isso usando um apropriado std::functioncomo o tipo de exclusão com unique_ptr? Supondo que isso realmente funcione, então você está pronto, propriedade exclusiva e um apagador apagado.
Steve Jessop
Grammar nit: "por que X verbos Y?" deve ser "por que X verbo Y?" em inglês.
zwol
7

Uma das razões está em um dos muitos casos de uso de a shared_ptr- a saber, como um indicador vitalício ou sentinela.

Isso foi mencionado na documentação original do boost:

auto register_callback(std::function<void()> closure, std::shared_ptr<void> pv)
{
    auto closure_target = { closure, std::weak_ptr<void>(pv) };
    ...
    // store the target somewhere, and later....
}

void call_closure(closure_target target)
{
    // test whether target of the closure still exists
    auto lock = target.sentinel.lock();
    if (lock) {
        // if so, call the closure
        target.closure();
    }
}

Onde closure_targetestá algo assim:

struct closure_target {
    std::function<void()> closure;
    std::weak_ptr<void> sentinel;
};

O chamador registraria um retorno de chamada mais ou menos assim:

struct active_object : std::enable_shared_from_this<active_object>
{
    void start() {
      event_emitter_.register_callback([this] { this->on_callback(); }, 
                                       shared_from_this());
    }

    void on_callback()
    {
        // this is only ever called if we still exist 
    }
};

porque shared_ptr<X>é sempre conversível shared_ptr<void>, o event_emitter pode agora felizmente não estar ciente do tipo de objeto para o qual está chamando de volta.

Esse arranjo libera os assinantes para o emissor do evento da obrigação de lidar com casos cruzados (e se o retorno de chamada estiver em uma fila, esperando para ser acionado enquanto o objeto_ativo vai embora?), E também significa que não há necessidade de sincronizar o cancelamento da assinatura. weak_ptr<void>::locké uma operação sincronizada.

Richard Hodges
fonte