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 comunique_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.