O objetivo de uma shared_ptr
instância distinta é garantir (na medida do possível) que, enquanto shared_ptr
estiver no escopo, o objeto para o qual aponta ainda existirá, porque sua contagem de referência será pelo menos 1.
Class::only_work_with_sp(boost::shared_ptr<foo> sp)
{
// sp points to an object that cannot be destroyed during this function
}
Portanto, ao usar uma referência a a shared_ptr
, você desativa essa garantia. Então, em seu segundo caso:
Class::only_work_with_sp(boost::shared_ptr<foo> &sp) //Again, no copy here
{
...
sp->do_something();
...
}
Como você sabe que sp->do_something()
não explodirá devido a um ponteiro nulo?
Tudo depende do que está nessas seções '...' do código. E se você chamar algo durante o primeiro '...' que tem o efeito colateral (em algum lugar em outra parte do código) de limpar um shared_ptr
para esse mesmo objeto? E se ele for o único remanescente distinto shared_ptr
daquele objeto? Tchau, objeto, exatamente onde você está prestes a tentar usá-lo.
Portanto, existem duas maneiras de responder a essa pergunta:
Examine a origem de todo o programa com muito cuidado até ter certeza de que o objeto não morrerá durante o corpo da função.
Altere o parâmetro de volta para um objeto distinto em vez de uma referência.
Um conselho geral que se aplica aqui: não se preocupe em fazer alterações arriscadas em seu código por causa do desempenho até que tenha cronometrado seu produto em uma situação realista em um criador de perfil e medido de forma conclusiva que a mudança que você deseja fazer causará um diferença significativa para o desempenho.
Atualização para comentarista JQ
Aqui está um exemplo artificial. É deliberadamente simples, então o erro será óbvio. Em exemplos reais, o erro não é tão óbvio porque está oculto em camadas de detalhes reais.
Temos uma função que enviará uma mensagem a algum lugar. Pode ser uma mensagem grande, então, em vez de usar um std::string
que provavelmente é copiado conforme é passado para vários lugares, usamos um shared_ptr
para uma string:
void send_message(std::shared_ptr<std::string> msg)
{
std::cout << (*msg.get()) << std::endl;
}
(Apenas "enviamos" para o console neste exemplo).
Agora queremos adicionar uma facilidade para lembrar a mensagem anterior. Queremos o seguinte comportamento: deve existir uma variável que contém a mensagem enviada mais recentemente, mas enquanto uma mensagem está sendo enviada, não deve haver nenhuma mensagem anterior (a variável deve ser redefinida antes do envio). Portanto, declaramos a nova variável:
std::shared_ptr<std::string> previous_message;
Em seguida, alteramos nossa função de acordo com as regras que especificamos:
void send_message(std::shared_ptr<std::string> msg)
{
previous_message = 0;
std::cout << *msg << std::endl;
previous_message = msg;
}
Portanto, antes de iniciarmos o envio, descartamos a mensagem anterior atual e, depois que o envio for concluído, podemos armazenar a nova mensagem anterior. Tudo bom. Aqui estão alguns códigos de teste:
send_message(std::shared_ptr<std::string>(new std::string("Hi")));
send_message(previous_message);
E como esperado, isso é impresso Hi!
duas vezes.
Agora vem o Sr. Maintainer, que olha o código e pensa: Ei, esse parâmetro para send_message
é um shared_ptr
:
void send_message(std::shared_ptr<std::string> msg)
Obviamente, isso pode ser alterado para:
void send_message(const std::shared_ptr<std::string> &msg)
Pense no aprimoramento de desempenho que isso trará! (Não importa que estejamos prestes a enviar uma mensagem tipicamente grande por algum canal, então a melhoria de desempenho será tão pequena a ponto de ser incomensurável).
Mas o verdadeiro problema é que agora o código de teste exibirá um comportamento indefinido (em compilações de depuração do Visual C ++ 2010, ele trava).
O Sr. Maintainer fica surpreso com isso, mas adiciona uma verificação defensiva send_message
em uma tentativa de impedir que o problema aconteça:
void send_message(const std::shared_ptr<std::string> &msg)
{
if (msg == 0)
return;
Mas é claro que ele segue em frente e trava, porque msg
nunca é nulo quando send_message
é chamado.
Como eu disse, com todo o código tão próximo em um exemplo trivial, é fácil encontrar o erro. Mas em programas reais, com relacionamentos mais complexos entre objetos mutáveis que contêm ponteiros uns para os outros, é fácil cometer o erro e difícil construir os casos de teste necessários para detectar o erro.
A solução fácil, onde você deseja que uma função possa contar com a shared_ptr
continuação de ser não nula, é que a função aloque seu próprio verdadeiro shared_ptr
, em vez de depender de uma referência a um existente shared_ptr
.
A desvantagem é que uma cópia copiada shared_ptr
não é gratuita: mesmo as implementações "livres de bloqueio" precisam usar uma operação interligada para honrar as garantias de threading. Portanto, pode haver situações em que um programa pode ser significativamente acelerado transformando de a shared_ptr
em a shared_ptr &
. Mas essa não é uma alteração que possa ser feita com segurança em todos os programas. Ele muda o significado lógico do programa.
Observe que um bug semelhante ocorreria se usássemos std::string
em vez de std::shared_ptr<std::string>
e em vez de:
previous_message = 0;
para limpar a mensagem, dissemos:
previous_message.clear();
Então, o sintoma seria o envio acidental de uma mensagem vazia, em vez de um comportamento indefinido. O custo de uma cópia extra de uma string muito grande pode ser muito mais significativo do que o custo de copiar um shared_ptr
, portanto, a compensação pode ser diferente.