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.

Vincenzo Pii
fonte

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

Em sílico
fonte
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.

San Jacinto
fonte