Devemos passar um shared_ptr por referência ou por valor?

270

Quando uma função recebe um shared_ptr(de boost ou C ++ 11 STL), você a está passando:

  • por referência const: void foo(const shared_ptr<T>& p)

  • ou por valor void foo(shared_ptr<T> p):?

Eu preferiria o primeiro método porque suspeito que seria mais rápido. Mas isso realmente vale a pena ou existem outros problemas?

Poderia, por favor, fornecer os motivos de sua escolha ou, se for o caso, por que você acha que isso não importa.

Danvil
fonte
14
O problema é que esses não são equivalentes. A versão de referência grita "Vou usar um pseudônimo shared_ptr, e posso alterá-lo, se quiser.", Enquanto a versão de valor diz "Vou copiar o seu shared_ptr, portanto, enquanto posso alterá-lo, você nunca saberá. ) Um parâmetro const-reference é a solução real, que diz "eu vou usar um pseudônimo shared_ptre prometo não alterá-lo". (O que é extremamente semelhante à semântica de valor!)
GManNickG
2
Ei, eu estaria interessado na opinião de vocês sobre o retorno de um shared_ptraluno. Você faz isso por const-refs?
Johannes Schaub - litb
Terceira possibilidade é usar std :: move () com C ++ 0x, este swaps tanto shared_ptr
Tomaka17
@ Johnnes: eu retornaria por referência const apenas para evitar qualquer cópia / ref-counting. Então, novamente, eu retorno todos os membros por referência constante, a menos que sejam primitivos.
GManNickG
possível repetição de C ++ - ponteiro passando questão
kennytm

Respostas:

229

Esta pergunta foi discutida e respondida por Scott, Andrei e Herb durante a sessão Ask Us Anything em C ++ e Beyond 2011 . Assista a partir de 4:34 sobre shared_ptrdesempenho e correção .

Logo, não há razão para passar por valor, a menos que o objetivo seja compartilhar a propriedade de um objeto (por exemplo, entre diferentes estruturas de dados ou entre diferentes threads).

A menos que você possa otimizá-lo conforme explicado por Scott Meyers no vídeo de discussão vinculado acima, mas isso está relacionado à versão real do C ++ que você pode usar.

Uma grande atualização desta discussão aconteceu durante o Painel Interativo da conferência GoingNative 2012 : Ask Us Anything! que vale a pena assistir, especialmente a partir das 22:50 .

mloskot
fonte
5
mas, como mostrado aqui, é mais barato passar por valor: stackoverflow.com/a/12002668/128384 não deve ser levado em consideração também (pelo menos para argumentos de construtores etc. em que um shared_ptr se tornará membro de a classe)?
stijn
2
@stijn Sim e não. As perguntas e respostas apontadas estão incompletas, a menos que esclareça a versão do padrão C ++ a que se refere. É muito fácil espalhar regras gerais de nunca / sempre que são simplesmente enganosas. A menos que os leitores se familiarizem com o artigo e as referências de David Abrahams, ou levem em consideração a data de postagem versus o padrão C ++ atual. Portanto, as duas respostas, a minha e a que você apontou, estão corretas, considerando o horário da postagem.
mloskot
1
"a menos que haja multiencadeamento " não, o MT não é de forma alguma especial.
precisa
3
Estou muito atrasado para a festa, mas minha razão para querer passar shared_ptr por valor é que isso torna o código mais curto e mais bonito. Seriamente. Value*é curto e legível, mas é ruim, então agora meu código está cheio const shared_ptr<Value>&e é significativamente menos legível e apenas ... menos arrumado. O que costumava ser void Function(Value* v1, Value* v2, Value* v3)agora void Function(const shared_ptr<Value>& v1, const shared_ptr<Value>& v2, const shared_ptr<Value>& v3), e as pessoas estão bem com isso?
Alex
7
@Alex A prática comum é criar aliases (typedefs) logo após a aula. Para o seu exemplo: class Value {...}; using ValuePtr = std::shared_ptr<Value>;Então, sua função se torna mais simples: void Function(const ValuePtr& v1, const ValuePtr& v2, const ValuePtr& v3)e você obtém o desempenho máximo. É por isso que você usa C ++, não é? :)
4LegsDrivenCat
92

Aqui está o exemplo de Herb Sutter

Diretriz: Não passe um ponteiro inteligente como parâmetro de função, a menos que queira usar ou manipular o próprio ponteiro inteligente, como compartilhar ou transferir propriedade.

Diretriz: Expresse que uma função armazenará e compartilhará a propriedade de um objeto de heap usando um parâmetro shared_ptr por valor.

Diretriz: Use um parâmetro non-const shared_ptr & apenas para modificar o shared_ptr. Use uma const shared_ptr & como parâmetro apenas se você não tiver certeza se deseja tirar uma cópia e compartilhar a propriedade; caso contrário, use o widget * (ou, se não for nulo, um widget &).

acel
fonte
3
Obrigado pelo link para Sutter. É um excelente artigo. Não concordo com ele no widget *, preferindo <widget &> opcional se o C ++ 14 estiver disponível. widget * é muito ambíguo do código antigo.
Eponymous
3
+1 para incluir widget * e widget e como possibilidades. Apenas para elaborar, passar o widget * ou widget & provavelmente é a melhor opção quando a função não está examinando / modificando o próprio objeto ponteiro. A interface é mais geral, pois não requer um tipo de ponteiro específico, e o problema de desempenho da contagem de referência shared_ptr é esquivado.
tgnottingham
4
Penso que esta deve ser a resposta aceita hoje, devido à segunda orientação. Invalida claramente a resposta aceita atualmente, que diz: não há razão para passar por valor.
Mbrt
62

Pessoalmente, eu usaria uma constreferência. Não há necessidade de incrementar a contagem de referência apenas para diminuí-la novamente por causa de uma chamada de função.

Evan Teran
fonte
1
Não votei sua resposta como negativo, mas antes que isso seja uma questão de preferência, há prós e contras em cada uma das duas possibilidades a serem consideradas. E seria bom conhecer e discutir essas vantagens e desvantagens. Depois, todos podem tomar uma decisão por si mesmos.
Danvil 28/07/10
@ Danvil: levando em consideração como shared_ptrfunciona, a única desvantagem possível de não passar por referência é uma pequena perda de desempenho. Existem duas causas aqui. a) o recurso de alias do ponteiro significa que os ponteiros valem dados mais um contador (talvez 2 para referências fracas) é copiado, por isso é um pouco mais caro copiar a rodada de dados. b) a contagem de referência atômica é um pouco mais lenta que o código antigo simples de incremento / decremento, mas é necessária para garantir a segurança do encadeamento. Além disso, os dois métodos são os mesmos para a maioria das intenções e propósitos.
Evan Teran
37

Passe por constreferência, é mais rápido. Se você precisar armazená-lo, diga em algum recipiente, a ref. A contagem será incrementada automaticamente pela operação de cópia.

Nikolai Fetissov
fonte
4
O voto negativo se tornou sua opinião sem números para apoiá-lo.
Kwesolowski
22

Executei o código abaixo, uma vez com fooo shared_ptrby const&e novamente com fooo shared_ptrby.

void foo(const std::shared_ptr<int>& p)
{
    static int x = 0;
    *p = ++x;
}

int main()
{
    auto p = std::make_shared<int>();
    auto start = clock();
    for (int i = 0; i < 10000000; ++i)
    {
        foo(p);
    }    
    std::cout << "Took " << clock() - start << " ms" << std::endl;
}

Usando o VS2015, versão x86 build, no meu processador intel core 2 quad (2.4GHz)

const shared_ptr&     - 10ms  
shared_ptr            - 281ms 

A cópia por versão de valor era uma ordem de magnitude mais lenta.
Se você estiver chamando uma função de forma síncrona a partir do encadeamento atual, prefira a const&versão.

tcb
fonte
1
Você pode dizer quais configurações de compilador, plataforma e otimização você usou?
Carlton
Eu usei a compilação de depuração do vs2015, atualizei a resposta para usar a compilação de versão agora.
TCB
1
Estou curioso para saber se quando a otimização estiver ligada, você obtém os mesmos resultados com ambos
Elliot madeiras
2
Otimização não ajuda muito. o problema é a contenção de bloqueio na contagem de referência na cópia.
27418 Alex
1
Essa não é a questão. Essa foo()função nem deveria aceitar um ponteiro compartilhado em primeiro lugar, porque não está usando esse objeto: ela deve aceitar um int&e fazer p = ++x;, chamando foo(*p);de main(). Uma função aceita um objeto ponteiro inteligente quando precisa fazer algo com ele e, na maioria das vezes, o que você precisa fazer é movê-lo ( std::move()) para outro lugar, para que um parâmetro por valor não tenha custo.
eepp
15

Desde o C ++ 11, você deve valorizá- lo em const e mais frequentemente do que você imagina.

Se você estiver usando o std :: shared_ptr (em vez do tipo subjacente T), estará fazendo isso porque deseja fazer algo com ele.

Se você deseja copiá-lo em algum lugar, faz mais sentido pegá-lo por cópia e std :: movê-lo internamente, em vez de pegá-lo com const & e depois copiá-lo. Isso ocorre porque você permite ao chamador a opção std :: move o shared_ptr ao chamar sua função, economizando um conjunto de operações de incremento e decremento. Ou não. Ou seja, o chamador da função pode decidir se precisa ou não do std :: shared_ptr depois de chamar a função e dependendo de se mover ou não. Isso não é possível se você passar por const & e, portanto, é preferencialmente aceitá-lo por valor.

Obviamente, se o chamador precisa do seu shared_ptr por mais tempo (portanto, não pode std :: move-lo) e você não deseja criar uma cópia simples na função (digamos que você queira um ponteiro fraco, ou às vezes deseja apenas para copiá-lo, dependendo de alguma condição), então uma const & ainda pode ser preferível.

Por exemplo, você deve fazer

void enqueue(std::shared<T> t) m_internal_queue.enqueue(std::move(t));

sobre

void enqueue(std::shared<T> const& t) m_internal_queue.enqueue(t);

Porque nesse caso, você sempre cria uma cópia internamente

Biscoito
fonte
1

Sem saber o custo de tempo da operação de cópia shared_copy em que está o incremento e o decréscimo atômico, sofri um problema de uso da CPU muito maior. Eu nunca esperei que o incremento e o decréscimo atômicos pudessem custar muito.

Após o resultado do teste, o incremento e decremento at32 int32 levam 2 ou 40 vezes ao incremento e decremento não atômico. Comprei no Core i7 3GHz com o Windows 8.1. O primeiro resultado sai quando não ocorre contenção, o último quando ocorre alta possibilidade de contenção. Tenho em mente que as operações atômicas são, finalmente, o bloqueio baseado em hardware. Bloqueio é bloqueio. Mau para o desempenho quando ocorre contenção.

Experimentando isso, eu sempre uso byref (const shared_ptr &) do que byval (shared_ptr).

Hyunjik Bae
fonte
1

Houve uma postagem recente no blog: https://medium.com/@vgasparyan1995/pass-by-value-vs-pass-by-reference-to-const-c-f8944171e3ce

Portanto, a resposta para isso é: nunca (quase) nunca passe const shared_ptr<T>&.
Simplesmente passe a classe subjacente.

Basicamente, os únicos tipos de parâmetros razoáveis ​​são:

  • shared_ptr<T> - Modifique e tome posse
  • shared_ptr<const T> - Não modifique, tome posse
  • T& - Modificar, sem propriedade
  • const T& - Não modifique, sem propriedade
  • T - Não modifique, sem propriedade, Barato para copiar

Como @accel apontou em https://stackoverflow.com/a/26197326/1930508, o conselho de Herb Sutter é:

Use uma const shared_ptr & como parâmetro apenas se você não tiver certeza se deseja ou não tirar uma cópia e compartilhar a propriedade

Mas em quantos casos você não tem certeza? Então essa é uma situação rara

Fogo Flamejante
fonte
0

Sabe-se que a transmissão de shared_ptr por valor tem um custo e deve ser evitada, se possível.

O custo da passagem por shared_ptr

Na maioria das vezes, passaria shared_ptr por referência e, melhor ainda, por referência const.

A diretriz principal do cpp possui uma regra específica para transmitir shared_ptr

R.34: Pegue um parâmetro shared_ptr para expressar que uma função é o proprietário da parte

void share(shared_ptr<widget>);            // share -- "will" retain refcount

Um exemplo de quando é realmente necessário transmitir shared_ptr por valor é quando o chamador passa um objeto compartilhado para um receptor assíncrono - ou seja, o chamador fica fora do escopo antes que o receptor conclua seu trabalho. O chamado deve "estender" a vida útil do objeto compartilhado, assumindo um share_ptr por valor. Nesse caso, passar uma referência para shared_ptr não será suficiente.

O mesmo vale para passar um objeto compartilhado para um thread de trabalho.

artm
fonte
-4

shared_ptr não é grande o suficiente, nem seu construtor \ destrutor trabalha o suficiente para que haja uma sobrecarga suficiente da cópia para se preocupar com o desempenho de passagem por referência versus desempenho de passagem por cópia.

pedregoso
fonte
15
Você mediu isso?
curiousguy
2
@stonemetal: E as instruções atômicas durante a criação do novo shared_ptr?
Quarra 22/10
É um tipo não POD, portanto, na maioria das ABIs, mesmo passando "por valor" passa um ponteiro. Não é a cópia real de bytes que é o problema. Como você pode ver na saída asm, a transmissão de um shared_ptr<int>valor by leva mais de 100 instruções x86 (incluindo lockinstruções ed caras para aumentar / diminuir atomicamente a contagem de ref). Passar por ref constante é o mesmo que passar um ponteiro para qualquer coisa (e, neste exemplo, no explorador do compilador Godbolt, a otimização de chamada de cauda transforma isso em um jmp simples em vez de em uma chamada: godbolt.org/g/TazMBU ).
Peter Cordes
TL: DR: Este é o C ++, onde os construtores de cópias podem fazer muito mais trabalho do que apenas copiar os bytes. Esta resposta é lixo total.
Peter Cordes
2
stackoverflow.com/questions/3628081/shared-ptr-horrible-speed Como um exemplo de ponteiros compartilhados passados ​​por valor vs passados ​​por referência, ele vê uma diferença no tempo de execução de aproximadamente 33%. Se você estiver trabalhando em código crítico de desempenho, os ponteiros nus obterão um aumento maior no desempenho. Então, com certeza, passe por const ref se você se lembrar, mas não é grande coisa se não se lembrar. É muito mais importante não usar o shared_ptr se você não precisar.
Stonemetal #