c++ - unique_ptr - using std:: make_unique




Por que shared_ptr<void> é legal, enquanto unique_ptr<void> está mal formado? (2)

A pergunta realmente se encaixa no título: estou curioso para saber qual é a razão técnica para essa diferença, mas também a lógica?

std::shared_ptr<void> sharedToVoid; // legal;
std::unique_ptr<void> uniqueToVoid; // ill-formed;

Isso ocorre porque std::shared_ptr implementa apagamento de tipo, enquanto std::unique_ptr não.

Como o std::shared_ptr implementa a eliminação de tipos, ele também suporta outra propriedade interessante, viz. ele não precisa do tipo do deleter como argumento do tipo de modelo para o modelo de classe. Veja as declarações deles:

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

que tem Deleter como parâmetro de tipo, enquanto

template<class T> 
class shared_ptr;

não tem.

Agora, a pergunta é: por que shared_ptr implementa apagamento de tipo? Bem, ele faz isso, porque precisa dar suporte à contagem de referências e, para isso, precisa alocar memória do heap e, como precisa alocar a memória de qualquer maneira, vai um passo além e implementa o apagamento de tipo - que precisa do heap alocação também. Então, basicamente, é apenas ser oportunista!

Por causa do apagamento de tipo, std::shared_ptr pode suportar duas coisas:

  • Ele pode armazenar objetos de qualquer tipo como void* , mas ainda é capaz de excluir os objetos na destruição corretamente , invocando corretamente o 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. É tudo sobre como o std::shared_ptr funciona.

Agora, a questão é: std::unique_ptr armazenar objetos como void* ? Bem, a resposta é sim - desde que você use um deleter 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 ;-)

Saída ( demonstração online ):

959 located at 0x18aec20 is being deleted

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

No meu caso, precisarei de um deleter 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 apagamento de tipo.

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

Na verdade, eu nunca tentei isso, mas talvez você possa conseguir isso usando uma std::function apropriada como o tipo deleter com unique_ptr ? Supondo que realmente funcione, então você está pronto, propriedade exclusiva e um deleter apagado por tipo.

Seguindo essa sugestão, eu implementei isso (embora ele não faça uso do std::function porque 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"));
}  

Saída ( 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.


Uma das justificativas está em um dos muitos casos de uso de um shared_ptr - ou seja, como um indicador de vida ou sentinela.

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

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_target é algo como isto:

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 
    }
};

como shared_ptr<X> é sempre conversível em shared_ptr<void> , o event_emitter agora pode ignorar o tipo de objeto para o qual está chamando de volta.

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





unique-ptr