Pergunta

Se eu tiver uma função que precisa trabalhar com um shared_ptr, não seria mais eficiente para passar uma referência a ele (de modo a evitar copiar o objeto shared_ptr)? Quais são os possíveis efeitos colaterais ruins? Imagino dois casos possíveis:

1) dentro da função é feita uma cópia do argumento, como em

ClassA::take_copy_of_sp(boost::shared_ptr<foo> &sp)  
{  
     ...  
     m_sp_member=sp; //This will copy the object, incrementing refcount  
     ...  
}  

2) no interior da função do argumento é usado apenas, como em

Class::only_work_with_sp(boost::shared_ptr<foo> &sp) //Again, no copy here  
{    
    ...  
    sp->do_something();  
    ...  
}  

Eu não posso ver em ambos os casos uma boa razão para passar o boost::shared_ptr<foo> por valor em vez de por referência. Passando por valor seria apenas "temporariamente" incrementar a contagem de referência devido à cópia, e depois diminui-lo ao sair do escopo da função. Am I com vista para alguma coisa?

Só para esclarecer, depois de ler várias respostas: Eu perfeitamente concordar com as preocupações prematura de otimização, e eu sempre tente primeiro-profile-então-work on-the-hotspots. A minha pergunta era mais de um código de ponto-de-vista puramente técnico, se você sabe o que quero dizer.

Foi útil?

Solução

O ponto de uma instância shared_ptr distinto é a garantia (tanto quanto possível) que, enquanto esta shared_ptr está no escopo, o objeto que aponta para continuará a existir, porque a sua contagem de referência será de 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
}

Assim, usando uma referência a uma shared_ptr, você desativar essa garantia. Assim, no 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 vai explodir devido a um ponteiro nulo?

Tudo depende do que é 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 a esse mesmo objeto? E o que se passa a ser o shared_ptr distinta único remanescente a esse objeto? Bye bye objeto, exatamente onde você está prestes a tentar usá-lo.

Portanto, há duas maneiras de responder a essa pergunta:

  1. Examine a fonte de todo o seu programa com muito cuidado até ter certeza de que o objeto não vai morrer durante o corpo da função.

  2. Alterar o parâmetro volta a ser um objeto distinto, em vez de uma referência.

General pouco de conselho que se aplica aqui: não se incomodam fazer alterações arriscado para o seu código para o bem de desempenho até que você tenha programado o seu produto em uma situação real em um profiler e conclusivamente medida que a mudança que você quer fazer vai fazer uma diferença significativa para o desempenho.

Atualização para o comentarista JQ

Aqui está um exemplo inventado. É deliberadamente simples, de modo que o erro vai ser óbvio. Nos exemplos reais, o erro não é tão óbvio, porque ele está escondido em camadas de detalhe real.

Nós temos uma função que irá enviar uma mensagem em algum lugar. Pode ser uma grande mensagem para ao invés de usar um std::string que provavelmente é copiado como ele é passado em torno de vários lugares, usamos uma shared_ptr para uma string:

void send_message(std::shared_ptr<std::string> msg)
{
    std::cout << (*msg.get()) << std::endl;
}

(Nós apenas "send" para o console para este exemplo).

Agora queremos adicionar uma facilidade para lembrar a mensagem anterior. Queremos o seguinte comportamento: uma variável deve existir que contém a mensagem enviada mais recentemente, mas enquanto uma mensagem está sendo enviada, então deve haver nenhuma mensagem anterior (a variável deve ser reposto antes de enviar). Assim, declaramos a nova variável:

std::shared_ptr<std::string> previous_message;

Em seguida, alterar a nossa função de acordo com as regras que nós especificados:

void send_message(std::shared_ptr<std::string> msg)
{
    previous_message = 0;
    std::cout << *msg << std::endl;
    previous_message = msg;
}

Então, antes de começar a enviar descartamos a corrente mensagem anterior, e em seguida, após o envio está completo, podemos armazenar a nova mensagem anterior. Tudo bom. Aqui está um código de teste:

send_message(std::shared_ptr<std::string>(new std::string("Hi")));
send_message(previous_message);

E, como esperado, este impressões Hi! duas vezes.

Agora, vem ao longo Mr Mantenedor, que olha para o código e pensa: Ei, isso parâmetro para send_message é um shared_ptr:

void send_message(std::shared_ptr<std::string> msg)

Obviamente que pode ser alterado para:

void send_message(const std::shared_ptr<std::string> &msg)

Pense na melhoria de desempenho que isso trará! (Não importa que estamos prestes a enviar um tipicamente grande mensagem sobre algum canal, de modo que a melhoria de desempenho será tão pequena quanto a ser unmeasureable).

Mas o verdadeiro problema é que agora o código de teste irá apresentar um comportamento indefinido (em Visual C ++ 2010 compilações de depuração, ele trava).

Mr Mantenedor é surpreendido por isso, mas adiciona uma verificação defensiva para send_message em uma tentativa de parar o acontecimento problema:

void send_message(const std::shared_ptr<std::string> &msg)
{
    if (msg == 0)
        return;

Mas é claro que ainda vai em frente e acidentes, porque msg Nunca é nulo quando send_message é chamado.

Como eu digo, com todo o código tão juntos em um exemplo trivial, é fácil encontrar o erro. Mas em programas reais, com relações mais complexas entre objetos mutáveis ??que possuem ponteiros para o outro, é fácil make o erro, e difícil de construir o ncasos de teste ecessary para detectar o erro.

A solução fácil, onde você quer uma função para ser capaz de confiar em um shared_ptr continua a ser não-nulo todo, é para a função de atribuir a sua própria verdade shared_ptr, em vez de confiar em uma referência a um shared_ptr existente.

A desvantagem é que copiou uma shared_ptr não é livre: implementações mesmo "livre de lock-" tem que usar uma operação interligada a garantias honra de encadeamento. Então, pode haver situações em que um programa pode ser significativamente acelerados alterando uma shared_ptr em um shared_ptr &. Mas esta não é uma mudança que pode ser feita com segurança para todos os programas. Ele muda o sentido lógico do programa.

Observe que um erro semelhante ocorreria se usássemos std::string todo em vez de std::shared_ptr<std::string>, e em vez de:

previous_message = 0;

para apagar a mensagem, nós dissemos:

previous_message.clear();

Em seguida, o sintoma seria a acidental envio de uma mensagem vazia, em vez de um comportamento indefinido. O custo de uma cópia extra de um grande seqüência pode ser muito mais significativa do que o custo de copiar um shared_ptr, de modo que o trade-off pode ser diferente.

Outras dicas

Eu encontrei-me em desacordo com a resposta mais bem votado, então eu fui à procura de opinons especialistas e aqui estão. De http://channel9.msdn.com/Shows/Going+Deep/C-and-Beyond-2011-Scott-Andrei-and-Herb-Ask-Us-Anything

Herb Sutter: "quando você passa de shared_ptr, cópias são caras"

Scott Meyers: "Não há nada de especial sobre shared_ptr quando se trata de saber se você passá-lo por valor, ou passá-lo por referência Use exatamente a mesma análise que você usa para qualquer outro tipo definido pelo usuário As pessoas parecem ter essa percepção de que.. shared_ptr de alguma forma resolve todos os problemas de gestão, e isso porque é pequeno, é necessariamente barato para passar por valor. tem que ser copiado, e há um custo associado a isso ... é caro para passá-lo por valor, por isso, se eu puder fugir com ele com a semântica adequadas no meu programa, eu vou passá-lo por referência a const ou referência, em vez "

Herb Sutter: "sempre passá-los por referência a const, e, muito ocasionalmente, talvez porque você sabe o que você chamada pode modificar a coisa que você tem uma referência de, talvez, então você pode passar por valor ... se você copiá-los como parâmetros, oh meu Deus você quase nunca precisa de galo que contagem de referência porque está sendo realizada de qualquer maneira viva, e você deve estar passando-o por referência, por isso, fazer isso "

Update: Herb expandiu neste aqui: http://herbsutter.com/2013/06/05/gotw-91-solution-smart-pointer-parameters/ , embora a moral da história é que você não deve estar passando shared_ptr de em tudo "a menos você quer usar ou manipular a si mesmo, como para compartilhar ou transferir a propriedade ponteiro inteligente. "

Gostaria de aconselhar contra esta prática a menos que você e os outros programadores você trabalha com realmente, realmente sabe o que está fazendo tudo.

Em primeiro lugar, você não tem idéia de como a interface para sua classe pode evoluir e você quiser evitar que outros programadores de fazer coisas ruins. Passando um shared_ptr por referência não é algo que um programador deve esperar para ver, porque não é idiomática, e que o torna fácil de usá-lo incorretamente. Programa defensivamente: fazer a interface difícil de usar incorretamente. Passagem por referência é só ir para convidar problemas mais tarde.

Em segundo lugar, não otimizar até que você saiba esta classe particular vai ser um problema. Perfil primeiro lugar, e, em seguida, se o programa realmente precisa do impulso dado pela passagem por referência, então talvez. Caso contrário, não suar as pequenas coisas (ou seja, as instruções extras N que leva para passar por valor) em vez preocupação sobre o projeto, estruturas de dados, algoritmos e manutenção a longo prazo.

Sim, tomando uma referência está bem lá. Você não pretende dar o método de propriedade compartilhada; ele só quer trabalhar com ele. Você poderia dar uma referência para o primeiro caso também, desde que você copiá-lo de qualquer maneira. Mas para o primeiro caso, leva propriedade. Há esse truque para ainda copiá-lo apenas uma vez:

void ClassA::take_copy_of_sp(boost::shared_ptr<foo> sp) {
    m_sp_member.swap(sp);
}

Você também deve copiar quando você devolvê-lo (isto é, não retornar uma referência). Porque a sua classe não sabe o que o cliente está fazendo com ele (que poderia armazenar um ponteiro para ele e, em seguida, big bang acontece). Se depois acontece que é um gargalo (primeiro perfil!), Então você ainda pode retornar uma referência.


Editar : Claro que, como outros apontam, isto só é verdade se você sabe o seu código e saber que você não redefinir o ponteiro compartilhada passaram de alguma forma. Em caso de dúvida, basta passar por valor.

É sensato para passar shared_ptrs por const&. Ele provavelmente não vai causar problemas (exceto no caso improvável que o shared_ptr referenciado é excluído durante a chamada de função, conforme detalhado por Earwicker) e ele provavelmente será mais rápido se você passar um monte deles por aí. Lembrar; o boost::shared_ptr padrão é thread-safe, então copiá-lo inclui um incremento de thread-safe.

Tente const& uso ao invés de apenas &, porque os objetos temporários não podem ser passados ??por referência não-const. (Mesmo que uma extensão da linguagem em MSVC lhe permite fazê-lo de qualquer maneira)

No segundo caso, fazer isso é simples:

Class::only_work_with_sp(foo &sp)
{    
    ...  
    sp.do_something();  
    ...  
}

Você pode chamá-lo como

only_work_with_sp(*sp);

Gostaria de evitar uma referência "simples" a menos que a função explicitamente pode modificar o ponteiro.

A const & pode ser um micro-otimização sensível ao chamar pequenas funções - por exemplo, para permitir novas otimizações, como inlining afastado algumas condições. Além disso, o incremento / decréscimo - desde que é o segmento de seguros - é um ponto de sincronização. Eu não esperaria isso para fazer uma grande diferença na maioria dos cenários, no entanto.

Geralmente, você deve usar o estilo mais simples, a menos que você tem razão para não. Em seguida, use o const & consistentemente, ou adicionar um comentário a respeito de porque se você usá-lo apenas em poucos lugares.

eu defendo passagem ponteiro compartilhada por referência const -. Uma semântica que a função seja ultrapassado com o ponteiro não possui o ponteiro, que é uma linguagem limpa para desenvolvedores

A única dificuldade está em vários programas de rosca do objeto a ser apontado pelo ponteiro compartilhado é destruído em outro segmento. Por isso, é seguro dizer usando const referência de ponteiro compartilhada é seguro no programa de rosca única.

Passando ponteiro compartilhada por referência não-const às vezes é perigoso -. A razão é as funções de swap e redefinir a função pode invocar para dentro, de modo a destruir o objeto que ainda é considerada válida após o retorno da função

Não se trata de otimização prematura, eu acho -. É sobre como evitar o desperdício desnecessário de ciclos de CPU quando você está claro o que você quer fazer e o idioma de codificação tem sido firmemente adotada por seus desenvolvedores companheiros

Apenas meus 2 centavos: -)

Parece que todos os prós e contras aqui pode realmente ser generalizada para qualquer tipo passado por referência não apenas shared_ptr. Na minha opinião, você deve saber a semântica de passagem por referência, referência const e valor e usá-lo corretamente. Mas não há absolutamente nada de intrinsecamente errado com o passar shared_ptr por referência, a menos que você acha que todas as referências são ruins ...

Para voltar ao exemplo:

Class::only_work_with_sp( foo &sp ) //Again, no copy here  
{    
    ...  
    sp.do_something();  
    ...  
}

Como você sabe que sp.do_something() não vai explodir devido a um apontador pendente?

A verdade é que, shared_ptr ou não, const ou não, isso pode acontecer se você tiver uma falha de projeto, como direta ou indiretamente, compartilhando a propriedade de sp entre threads, missusing um objeto que fazer delete this, você tem uma propriedade circular ou erros de outra propriedade.

Uma coisa que eu ainda não vi mencionado é que quando você passar ponteiros compartilhados por referência, você perde a conversão implícita de que você ganha se você quer passar uma classe derivada ponteiro compartilhada através de uma referência a um ponteiro de classe base compartilhada .

Por exemplo, este código produzirá um erro, mas ele vai funcionar se você mudar test() para que o ponteiro compartilhada não é passado por referência.

#include <boost/shared_ptr.hpp>

class Base { };
class Derived: public Base { };

// ONLY instances of Base can be passed by reference.  If you have a shared_ptr
// to a derived type, you have to cast it manually.  If you remove the reference
// and pass the shared_ptr by value, then the cast is implicit so you don't have
// to worry about it.
void test(boost::shared_ptr<Base>& b)
{
    return;
}

int main(void)
{
    boost::shared_ptr<Derived> d(new Derived);
    test(d);

    // If you want the above call to work with references, you will have to manually cast
    // pointers like this, EVERY time you call the function.  Since you are creating a new
    // shared pointer, you lose the benefit of passing by reference.
    boost::shared_ptr<Base> b = boost::dynamic_pointer_cast<Base>(d);
    test(b);

    return 0;
}

Eu vou assumir que você está familiarizado com otimização prematura e está pedindo isso ou para fins acadêmicos ou porque você ter isolado um código pré-existente que é de baixo desempenho.

passagem por referência é aprovado

Passar por referência const é melhor, e geralmente pode ser usado, como ele não força const-ness sobre o objeto apontado.

Você está não em risco de perder o ponteiro devido ao uso de uma referência. Essa referência é evidência de que você tem uma cópia do ponteiro inteligente no início da pilha e apenas um segmento possui uma pilha de chamadas, de modo que a cópia pré-existente não está indo embora.

Usando referências é muitas vezes mais eficiente pelas razões que você menciona, , mas não garantida . Lembre-se que dereferencing um objeto pode ter trabalho também. Seu cenário de referência, o uso ideal seria se o seu estilo de codificação envolve muitas funções pequenas, onde o ponteiro que são passadas de função para função para função antes de ser utilizado.

Você deve sempre evitar o armazenamento o ponteiro inteligente como uma referência. Seus Class::take_copy_of_sp(&sp) exemplo mostra o uso correto para isso.

Assumindo que não estamos preocupados com precisão const (ou mais, você quer dizer para permitir que as funções para ser capaz de modificar ou participação acionária dos dados sendo passados ??in), passando um boost :: shared_ptr por valor é mais seguro do que passá-lo por referência como nós permitimos que o original boost :: shared_ptr para controlar a sua própria vida. Considere os resultados de código a seguir ...

void FooTakesReference( boost::shared_ptr< int > & ptr )
{
    ptr.reset(); // We reset, and so does sharedA, memory is deleted.
}

void FooTakesValue( boost::shared_ptr< int > ptr )
{
    ptr.reset(); // Our temporary is reset, however sharedB hasn't.
}

void main()
{
    boost::shared_ptr< int > sharedA( new int( 13 ) );
    boost::shared_ptr< int > sharedB( new int( 14 ) );

    FooTakesReference( sharedA );

    FooTakesValue( sharedB );
}

A partir do exemplo acima, vemos que passar sharedA por referência permite que FooTakesReference para repor o ponteiro original, o que reduz a sua contagem de uso para 0, destruindo-o de dados. FooTakesValue , no entanto, não é possível repor o ponteiro original, garantindo sharedB de dados ainda é utilizável. Quando outro desenvolvedor inevitavelmente vem e tentativas de pegar carona em frágil existência, ensues caos do sharedA. A sorte sharedB desenvolvedor, no entanto, vai para casa mais cedo tudo está bem em seu mundo.

A segurança código, neste caso, supera de longe qualquer cópia melhoria de velocidade cria. Ao mesmo tempo, o boost :: shared_ptr se destina a melhorar a segurança de código. Será muito mais fácil ir de uma cópia de uma referência, se algo requer este tipo de otimização nicho.

Sandy escreveu: "Parece que todos os prós e contras aqui pode realmente ser generalizada para qualquer tipo passado por referência não apenas shared_ptr"

É verdade até certo ponto, mas o ponto de usar shared_ptr é eliminar preocupações sobre vidas objetos e deixar a alça compilador que para você. Se você estiver indo para passar um ponteiro compartilhada por referência e permitir que os clientes de seus contados referência a objetos métodos de chamada não-const que pode libertar os dados do objeto, em seguida, usando um ponteiro compartilhado é quase inútil.

Eu escrevi "quase" em que a sentença anterior, porque o desempenho pode ser uma preocupação, e 'pode' ser justificado em casos raros, mas também gostaria de evitar esse cenário me e olhar para todas as possíveis outras soluções de otimização de mim mesmo, como a sério olhar para a adição de outro nível de engano, avaliação preguiçosa, etc ..

O código que existe passado, do autor, ou mesmo postar de memória É autor, que requer suposições implícitas sobre o comportamento, no comportamento particular sobre vidas objeto, exige clara, concisa, documentação legível, e em seguida, muitos clientes não vai lê-lo de qualquer maneira! Simplicidade supera quase sempre a eficiência, e quase sempre há outras maneiras de ser eficiente. Se você realmente precisa para passar valores de referência para evitar profunda copiando por construtores de cópia de seus contados de referência-objetos (e o operador é igual), então talvez você deve considerar maneiras de tornar os dados profundamente copiado ser referência ponteiros contados que podem ser copiado rapidamente. (Claro, isso é apenas um cenário de design que podem não se aplicar à sua situação).

Eu costumava trabalhar em um projeto que a princípio era muito forte sobre a passagem ponteiros inteligentes por valor. Quando me pediram para fazer alguma análise de desempenho -. Descobri que para incremento e decremento dos contadores de referência dos ponteiros inteligentes a aplicação gasta entre 4-6% do tempo do processador utilizado

Se você quer passar os ponteiros inteligentes em termos de valor apenas para evitar ter problemas em casos estranhos como descrito a partir de Daniel Earwicker certificar que você entendeu o preço que você pagar por isso.

Se você decidir ir com uma referência a principal razão para usar referência const é para torná-lo possível ter upcasting implícito quando você precisa passar ponteiro compartilhado para objeto de classe que herda a classe que você usar na interface.

Além do que litb disse, eu gostaria de salientar que é provavelmente passar por referência const no segundo exemplo, de que maneira você tem certeza de que não acidentalmente modificá-lo.

struct A {
  shared_ptr<Message> msg;
  shared_ptr<Message> * ptr_msg;
}
  1. passagem por valor:

    void set(shared_ptr<Message> msg) {
      this->msg = msg; /// create a new shared_ptr, reference count will be added;
    } /// out of method, new created shared_ptr will be deleted, of course, reference count also be reduced;
    
  2. passar por referência:

    void set(shared_ptr<Message>& msg) {
     this->msg = msg; /// reference count will be added, because reference is just an alias.
     }
    
  3. passar pelo ponteiro:

    void set(shared_ptr<Message>* msg) {
      this->ptr_msg = msg; /// reference count will not be added;
    }
    

Cada pedaço de código deve levar algum sentido. Se você passar um ponteiro compartilhada por valor em todos os lugares na aplicação, isto significa "Eu sou inseguro sobre o que está acontecendo em outros lugares, portanto, sou a favor da segurança raw ". Este não é o que eu chamo um sinal de boa confiança para outros programadores que poderia consultar o código.

De qualquer forma, mesmo se uma função obtém uma referência const e você é "inseguro", você ainda pode criar uma cópia do ponteiro compartilhada na cabeça da função, para adicionar uma referência forte para o ponteiro. Isso também poderia ser visto como uma dica sobre o projeto ( "o ponteiro poderia ser modificado em outro lugar").

Então, sim, IMO, o padrão deve ser " passar por referência const ".

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top