Como retornar smart pointers (shared_ptr), por referência ou por valor?


94

Digamos que eu tenha uma classe com um método que retorna a shared_ptr.

Quais são as possíveis vantagens e desvantagens de devolvê-lo por referência ou por valor?

Duas pistas possíveis:

  • Destruição precoce de objetos. Se eu retornar a referência shared_ptrby (const), o contador de referência não é incrementado, então, corro o risco de ter o objeto excluído quando ele sai do escopo em outro contexto (por exemplo, outro thread). Isso está correto? E se o ambiente for de thread único, essa situação também pode acontecer?
  • Custo. A passagem por valor certamente não é gratuita. Vale a pena evitá-lo sempre que possível?

Obrigado a todos.

Respostas:


114

Retorna ponteiros inteligentes por valor.

Como você disse, se você devolvê-lo por referência, não incrementará adequadamente a contagem de referência, o que abre o risco de deletar algo no momento impróprio. Só isso já deve ser motivo suficiente para não voltar por referência. As interfaces devem ser robustas.

A preocupação com os custos é discutível hoje em dia graças à otimização do valor de retorno (RVO), então você não irá incorrer em uma seqüência incremento-incremento-decremento ou algo parecido em compiladores modernos. Portanto, a melhor maneira de retornar a shared_ptré simplesmente retornar por valor:

shared_ptr<T> Foo()
{
    return shared_ptr<T>(/* acquire something */);
};

Esta é uma oportunidade RVO óbvia para os compiladores C ++ modernos. Eu sei que os compiladores Visual C ++ implementam RVO mesmo quando todas as otimizações estão desligadas. E com a semântica de movimentação do C ++ 11, essa preocupação é ainda menos relevante. (Mas a única maneira de ter certeza é traçar um perfil e experimentar.)

Se você ainda não está convencido, Dave Abrahams tem um artigo que defende o retorno por valor. Eu reproduzo um trecho aqui; Eu recomendo fortemente que você leia o artigo inteiro:

Seja honesto: como você se sente com o código a seguir?

std::vector<std::string> get_names();
...
std::vector<std::string> const names = get_names();

Francamente, embora eu devesse saber melhor, isso me deixa nervoso. Em princípio, quando get_names() retorna, temos que copiar a vectorde strings. Então, precisamos copiá-lo novamente quando inicializamos namese precisamos destruir a primeira cópia. Se houver N strings no vetor, cada cópia pode exigir até N + 1 alocações de memória e uma grande quantidade de acessos de dados hostis ao cache> conforme o conteúdo da string é copiado.

Em vez de enfrentar esse tipo de ansiedade, muitas vezes recorri à passagem por referência para evitar cópias desnecessárias:

get_names(std::vector<std::string>& out_param );
...
std::vector<std::string> names;
get_names( names );

Infelizmente, essa abordagem está longe de ser ideal.

  • O código cresceu 150%
  • Tivemos que abandonar o const-ness porque estamos mudando nomes.
  • Como os programadores funcionais gostam de nos lembrar, a mutação torna o código mais complexo para raciocinar ao minar a transparência referencial e o raciocínio equacional.
  • Não temos mais semânticas de valor estrito para nomes.

Mas é realmente necessário bagunçar nosso código dessa forma para ganhar eficiência? Felizmente, a resposta acabou sendo não (e especialmente se você estiver usando C ++ 0x).


Eu não sei se eu diria que RVO torna a questão discutível, já que retornar por referência torna RVO decididamente impossível.
— Edward Strange

@CrazyEddie: Verdade, esse é um dos motivos pelos quais recomendo que o OP retorne por valor.
— In silico

A regra RVO, permitida pelo padrão, supera as regras sobre relacionamentos de sincronização / acontece antes, garantidos pelo padrão?
— edA-qa mort-ora-y

1
@ edA-qa mort-ora-y: RVO é explicitamente permitido, mesmo se tiver efeitos colaterais. Por exemplo, se você tiver uma cout << "Hello World!";instrução em um construtor padrão e de cópia, você não verá dois Hello World!s quando RVO estiver em vigor. No entanto, isso não deve ser um problema para ponteiros inteligentes projetados corretamente, mesmo para sincronização errada.
— In silico

23

Com relação a qualquer ponteiro inteligente (não apenas shared_ptr), não acho que seja aceitável retornar uma referência a um, e ficaria muito hesitante em passá-los por referência ou ponteiro bruto. Por quê? Porque você não pode ter certeza de que não será copiado superficialmente por meio de uma referência posterior. Seu primeiro ponto define o motivo pelo qual isso deve ser uma preocupação. Isso pode acontecer até mesmo em um ambiente de thread único. Você não precisa de acesso simultâneo a dados para colocar semântica de cópia incorreta em seus programas. Você não controla realmente o que seus usuários fazem com o ponteiro depois de passá-lo, então não incentive o uso indevido, dando aos usuários da API corda suficiente para se enforcarem.

Em segundo lugar, observe a implementação do seu ponteiro inteligente, se possível. A construção e a destruição devem ser quase insignificantes. Se essa sobrecarga não for aceitável, não use um ponteiro inteligente! Mas, além disso, você também precisará examinar a arquitetura de simultaneidade que possui, porque o acesso mutuamente exclusivo ao mecanismo que rastreia os usos do ponteiro vai deixá-lo mais lento do que a mera construção do objeto shared_ptr.

Editar, 3 anos depois: com o advento dos recursos mais modernos em C ++, eu ajustaria minha resposta para aceitar mais os casos em que você simplesmente escreveu um lambda que nunca vive fora do escopo da função de chamada, e não é copiado em outro lugar. Aqui, se você quiser economizar o mínimo de sobrecarga de copiar um ponteiro compartilhado, seria justo e seguro. Por quê? Porque você pode garantir que a referência nunca será mal utilizada.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.