Como "retornar um objeto" em C ++?


167

Sei que o título parece familiar, pois há muitas perguntas semelhantes, mas estou pedindo um aspecto diferente do problema (sei a diferença entre colocar as coisas na pilha e colocá-las na pilha).

Em Java, sempre posso retornar referências a objetos "locais"

public Thing calculateThing() {
    Thing thing = new Thing();
    // do calculations and modify thing
    return thing;
}

Em C ++, para fazer algo semelhante, tenho 2 opções

(1) Eu posso usar referências sempre que precisar "retornar" um objeto

void calculateThing(Thing& thing) {
    // do calculations and modify thing
}

Então use assim

Thing thing;
calculateThing(thing);

(2) Ou posso retornar um ponteiro para um objeto alocado dinamicamente

Thing* calculateThing() {
    Thing* thing(new Thing());
    // do calculations and modify thing
    return thing;
}

Então use assim

Thing* thing = calculateThing();
delete thing;

Usando a primeira abordagem, não precisarei liberar memória manualmente, mas para mim isso dificulta a leitura do código. O problema com a segunda abordagem é, vou ter que lembrar delete thing;, o que não parece muito bom. Eu não quero retornar um valor copiado porque é ineficiente (eu acho), então aqui estão as perguntas

  • Existe uma terceira solução (que não requer cópia do valor)?
  • Existe algum problema se eu continuar com a primeira solução?
  • Quando e por que devo usar a segunda solução?

32
+1 por colocar bem a pergunta.
Kangkan

1
Para ser muito pedante, é um pouco impreciso dizer que "funções retornam algo". Mais corretamente, avaliar uma chamada de função produz um valor . O valor é sempre um objeto (a menos que seja uma função nula). A distinção é se o valor é um glvalue ou um prvalue - que é determinado se o tipo de retorno declarado é uma referência ou não.
Kerrek SB

Respostas:


107

Eu não quero retornar um valor copiado porque é ineficiente

Prove.

Consulte RVO e NRVO e na semântica de movimento C ++ 0x. Na maioria dos casos, no C ++ 03, um parâmetro out é apenas uma boa maneira de tornar seu código feio, e no C ++ 0x você realmente se machucaria usando um parâmetro out.

Basta escrever um código limpo, retornar por valor. Se o desempenho for um problema, analise-o (pare de adivinhar) e descubra o que você pode fazer para corrigi-lo. Provavelmente não retornará itens das funções.


Dito isto, se você estiver decidido a escrever dessa maneira, provavelmente desejará fazer o parâmetro out. Evita alocação dinâmica de memória, que é mais segura e geralmente mais rápida. Requer que você tenha alguma maneira de construir o objeto antes de chamar a função, o que nem sempre faz sentido para todos os objetos.

Se você deseja usar a alocação dinâmica, o mínimo que pode ser feito é colocá-lo em um ponteiro inteligente. (Isso deve ser feito o tempo todo de qualquer maneira). Então você não se preocupa em excluir nada, as coisas são seguras contra exceções etc. O único problema é que provavelmente é mais lento do que retornar por valor de qualquer maneira!


10
@phunehehe: Nenhum ponto é especular, você deve analisar seu código e descobrir. (Dica: não.) Os compiladores são muito inteligentes, não perderão tempo copiando as coisas, se não precisarem. Mesmo se a cópia custar algo, você ainda deve procurar um bom código em vez do código rápido; É fácil otimizar um bom código quando a velocidade se torna um problema. Não faz sentido colocar código para algo que você não tem ideia, é um problema; especialmente se você realmente desacelerar ou não tirar nada disso. E se você estiver usando C ++ 0x, a semântica de movimento torna isso um problema.
GManNickG

1
@ GMan, re: RVO: na verdade, isso só é verdade se o chamador e o destinatário estiverem na mesma unidade de compilação, o que no mundo real não é na maioria das vezes. Portanto, você ficará desapontado se o seu código não estiver totalmente modelado (nesse caso, tudo estará em uma unidade de compilação) ou se houver alguma otimização do tempo do link em vigor (o GCC apenas o 4.5).
28810 Alex B

2
@Alex: Os compiladores estão cada vez melhores na otimização entre as unidades de tradução. (VC faz isso por vários lançamentos agora.)
SBI

9
@ Alex B: Isso é lixo completo. Muitas convenções de chamada muito comuns tornam o chamador responsável por alocar espaço para grandes valores de retorno e o responsável por sua construção. O RVO trabalha felizmente em unidades de compilação, mesmo sem otimizações de tempo de link.
CB Bailey

6
@ Charles, após a verificação, parece estar correto! Retiro minha declaração claramente desinformada.
Alex B

41

Basta criar o objeto e devolvê-lo

Thing calculateThing() {
    Thing thing;
    // do calculations and modify thing
     return thing;
}

Eu acho que você fará um favor a si mesmo se esquecer a otimização e apenas escrever código legível (você precisará executar um criador de perfil mais tarde - mas não pré-otimize).


2
Thing thing();declara uma função local e retorna a Thing.
dreamlax

2
A coisa () declara uma função retornando uma coisa. Não existe um objeto Coisa construído em seu corpo funcional.
CB Bailey

@dreamlax @Charles @GMan Um pouco tarde, mas fixo.
Amir Rachum 29/03

Como isso funciona no C ++ 98? Eu recebo erros no intérprete CINT e fiquei pensando que é devido ao C ++ 98 ou ao próprio CINT ...!
Xcorat 31/05/19

16

Basta retornar um objeto como este:

Thing calculateThing() 
{
   Thing thing();
   // do calculations and modify thing
   return thing;
}

Isso chamará o construtor de cópia no Things, portanto, você pode querer fazer sua própria implementação disso. Como isso:

Thing(const Thing& aThing) {}

Isso pode ser um pouco mais lento, mas pode não ser um problema.

Atualizar

O compilador provavelmente otimizará a chamada para o construtor de cópias, para que não haja sobrecarga extra. (Como dreamlax apontou no comentário).


9
Thing thing();declara uma função local retornando a Thing, também, o padrão permite que o compilador omita o construtor de cópia no caso apresentado; qualquer compilador moderno provavelmente fará isso.
dreamlax

1
Você traz um bom argumento para implementar o construtor de cópia, especialmente se uma cópia profunda for necessária.
precisa saber é o seguinte

+1 por declarar explicitamente sobre o construtor de cópias, embora como @dreamlax diga que o compilador provavelmente "otimizará" o código retornado para as funções, evitando uma chamada realmente não necessária ao construtor de cópias.
Jose.angel.jimenez # 29/14

Em 2018, no VS 2017, ele está tentando usar o construtor de movimentação. Se o construtor de movimentação for excluído e o construtor de cópia não, ele não será compilado.
21418 Andrew

11

Você tentou usar ponteiros inteligentes (se o objeto é realmente grande e pesado), como auto_ptr:


std::auto_ptr<Thing> calculateThing()
{
  std::auto_ptr<Thing> thing(new Thing);
  // .. some calculations
  return thing;
}


// ...
{
  std::auto_ptr<Thing> thing = calculateThing();
  // working with thing

  // auto_ptr frees thing 
}

4
auto_ptrs estão obsoletos; use shared_ptrou em unique_ptrvez disso.
precisa saber é o seguinte

Só vou adicionar isso aqui ... Eu uso c ++ há anos, embora não profissionalmente com c ++ ... Eu decidi tentar não usar mais ponteiros inteligentes, eles são apenas uma bagunça absoluta e causam tudo tipos de problemas, não ajudando muito a acelerar muito o código. Prefiro apenas copiar dados e gerenciar ponteiros, usando RAII. Portanto, aconselho que, se puder, evite indicadores inteligentes.
21718 Andrew

8

Uma maneira rápida de determinar se um construtor de cópias está sendo chamado é adicionar o log ao construtor de cópias da sua classe:

MyClass::MyClass(const MyClass &other)
{
    std::cout << "Copy constructor was called" << std::endl;
}

MyClass someFunction()
{
    MyClass dummy;
    return dummy;
}

Chamada someFunction; o número de linhas "Copiador de construção foi chamado" que você obterá variará entre 0, 1 e 2. Se você não obtiver nenhuma, seu compilador otimizou o valor de retorno (o que é permitido fazer). Se você receber não obter 0, e seu construtor de cópia é ridiculamente caro, em seguida, procurar formas alternativas para retornam instâncias de suas funções.


1

Em primeiro lugar, você tem um erro no código, você quer Thing *thing(new Thing());e somente return thing;.

  • Use shared_ptr<Thing>. Deref-lo como tho era um ponteiro. Será excluído para você quando a última referência ao Thingcontido sair do escopo.
  • A primeira solução é muito comum em bibliotecas ingênuas. Tem algum desempenho e sobrecarga sintática, evite-a se possível
  • Use a segunda solução apenas se você puder garantir que nenhuma exceção será lançada ou quando o desempenho for absolutamente crítico (você fará interface com C ou assembly antes que isso se torne relevante).

0

Tenho certeza de que um especialista em C ++ virá com uma resposta melhor, mas pessoalmente eu gosto da segunda abordagem. O uso de ponteiros inteligentes ajuda no problema de esquecer deletee, como você diz, parece mais limpo do que ter que criar um objeto antes da mão (e ainda ter que excluí-lo se desejar alocá-lo no heap).

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.