Software Design

Design de software em C, C++, Java, etc…

C++: O que é melhor – retornar cópia ou passar uma referência?

A maioria dos programadores C++ não gostam de retornar objetos por valor porque isso é considerado má idéia em termos de performance. Uma chamada como estas:

std::vector<Cliente> getClientes() { ... };
...
std::vector<Cliente> clientes = getClientes();

Isso dá arrepios só de ler o código, porque quando retornamos um objeto por valor estamos inicializando ele localmente e no return o compilador fará uma cópia deste objeto e uma nova cópia ao incializar a variável clientes, então o compilador destruirá o objeto temporário criado para o return. Se o nosso vector tiver 100 clientes isso pode levar a várias alocações e desalocações desnecessárias, além do consumo de memória e processamento extras.

Por isso somos tentados a codificar desta maneira:

void getClientes(std::vector<Cliente> &result) { ... };
...
std::vector<Cliente> clientes;
getClientes(clientes);

Desta forma estamos passando uma referência para o método getClientes, que irá apenas adicionar os itens num vector já inicializado. Porém essa solução nem sempre é boa. Primeiro porque o código fica menos elegante. Além disso perdemos a vantagem de usar o retorno da função como passagem de parâmetro para outra função ( algo do tipo: imprimeClientes(getClientes()) ).

Felizmente, os compiladores modernos nos permitem utilizar a passagem-por-valor sem o overhead de várias cópias dos objetos. Creio que todos os compiladores que tem suporte completo ao C++0x já trazem essas otimizações. O compilador que eu uso é o g++ e à partir da versão 4.6 (talvez até anteriores) já é possível tirar proveito desses recursos

As otimizações: Copy Elision / RVO ( Return Value Optimization )

Os compiladores podem fazer algumas otimizações de acordo com o seu código fonte e como seus métodos são chamados. Copy elision é o termo utilizado quando o compilador consegue suprimir uma cópia.
Por exemplo, com o RVO a função que faz a chamada aloca o espaço para o valor de retorno na pilha e passa o endereço de memória para a função chamada. A função chamada pode então trabalhar diretamente neste espaço de memória, o que elimina a cópia no retorno da função. Então no código:

std::vector<Cliente> clientes = getClientes();

Nenhuma cópia é necessária.

Além disso, apesar de normalmente o compilador precisar fazer uma cópia do objeto quando ele é passado por valor para que as modificações locais não afetem o objeto original, o compilador ainda consegue otimizar isso e evitar uma cópia se o objeto passado for um rvalue ( ou seja, um objeto temporário – para mais detalhes sobre rvalues veja este artigo ). Nestes casos o compilador pode omitir a cópia e passar por referência, já que o objeto será descartado ao fim da chamada. Então por exemplo:

std::vector<Cliente> ordenar(std::vector<Cliente> clientes) {
  std::sort(clientes);
  return clientes;
}
std::vector<Cliente> clientes = ordenar(getClientes());

Nenhuma cópia é necessária. Passo à passo o compilador faz o seguinte:

  1. Aloca a memória na pilha para a variável clientes
  2. Chama o método getClientes() passando o endereço de memória desta variável
  3. Dentro do método getClientes() a memória para os objetos é alocada
  4. Quando o método retorna, a cópia não é necessária pois ele está retornando o mesmo objeto previamente alocado.
  5. O retorno do getClientes() é um rvalue portanto o compilador sabe que não precisa copiar o vector já que ele será destruído no retorno do método

Quando o método ordenar retorna, todo o trabalho foi feito através utilizando uma única alocação de memória e nenhuma cópia, apesar de todas as passagens por valor.

O que não fazer:

Considere esse código parecido com o ordenar anterior:

// clientes é passador por referência e retornado por valor
std::vector<Cliente> ordenar(std::vector<Cliente> const &clientes) {
  std::vector<Cliente> cli(clientes);
  std::sort(cli);
  return cli;
}
Apesar de serem bastante similares, esta segunda versão não permite copy elision. Isso porque apesar de o argumento ser um rvalue, a origem da cópia não é e por isso a cópia não será otimizada. Então basicamente a ideia é a seguinte: Não copie os seus parâmetros de função – deixe que o compilador o faça. Na pior das hipóteses, se o seu compilador não fizer copy elision a performance vai ser a mesma.
Resumindo, as otimizações feitas pelos compiladores mais novos nos ajudam a evitar cópias desnecessárias de objetos mas devemos estar sempre atentos à nossa forma de codificar. Não existe uma solução que resolva todos os problema e o compilador não faz milagres, o programador precisa estar ciente do que está escrevendo e de como o compilador vai interpretar aquilo.
Em breve vou escrever um artigo sobre move semantics que é um recurso novo disponível à partir do C++11.

Referências:

Deixe uma resposta

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s

Informação

Publicado às julho 18, 2012 por em C++, Unix e marcado , , , , .
%d blogueiros gostam disto: