A prática de retornar uma variável de referência C ++ é ruim?

341

Isso é um pouco subjetivo, eu acho; Não tenho certeza se a opinião será unânime (já vi muitos trechos de código em que as referências são retornadas).

De acordo com um comentário sobre essa pergunta que acabei de fazer, em relação à inicialização de referências , o retorno de uma referência pode ser ruim porque, como eu entendo, fica mais fácil deixar de excluí-la, o que pode levar a vazamentos de memória.

Isso me preocupa, pois tenho seguido exemplos (a menos que esteja imaginando coisas) e feito isso em poucos lugares ... Entendi mal? Isso é mau? Se sim, quão mal?

Eu sinto que, por causa do meu pacote misto de ponteiros e referências, combinado com o fato de eu ser novo em C ++, e total confusão sobre o que usar quando, meus aplicativos devem ser um inferno de vazamento de memória ...

Além disso, entendo que o uso de ponteiros inteligentes / compartilhados geralmente é aceito como a melhor maneira de evitar vazamentos de memória.

Nick Bolton
fonte
Não é ruim se você estiver escrevendo funções / métodos do tipo getter.
John Z. Li

Respostas:

411

Em geral, retornar uma referência é perfeitamente normal e acontece o tempo todo.

Se você diz:

int& getInt() {
    int i;
    return i;  // DON'T DO THIS.
}

Isso é todo tipo de mal. A pilha alocada idesaparecerá e você está se referindo a nada. Isso também é mau:

int& getInt() {
    int* i = new int;
    return *i;  // DON'T DO THIS.
}

Porque agora o cliente precisa fazer o estranho:

int& myInt = getInt(); // note the &, we cannot lose this reference!
delete &myInt;         // must delete...totally weird and  evil

int oops = getInt(); 
delete &oops; // undefined behavior, we're wrongly deleting a copy, not the original

Observe que as referências rvalue ainda são apenas referências; portanto, todos os aplicativos inválidos permanecem os mesmos.

Se você deseja alocar algo que vive além do escopo da função, use um ponteiro inteligente (ou, geralmente, um contêiner):

std::unique_ptr<int> getInt() {
    return std::make_unique<int>(0);
}

E agora o cliente armazena um ponteiro inteligente:

std::unique_ptr<int> x = getInt();

Referências também são boas para acessar itens em que você sabe que a vida está sendo mantida aberta em um nível superior, por exemplo:

struct immutableint {
    immutableint(int i) : i_(i) {}

    const int& get() const { return i_; }
private:
    int i_;
};

Aqui sabemos que não há problema em retornar uma referência a i_ porque o que quer que esteja nos chama gerencia o tempo de vida da instância da classe, e assim permanecerá i_pelo menos por tanto tempo.

E, claro, não há nada errado com apenas:

int getInt() {
   return 0;
}

Se o tempo de vida útil for deixado para o chamador, e você está apenas computando o valor.

Resumo: não há problema em retornar uma referência se a vida útil do objeto não terminar após a chamada.

GManNickG
fonte
21
Todos esses são exemplos ruins. O melhor exemplo de uso adequado é quando você está retornando uma referência a um objeto que foi passado. Ala operator <<
Arelius 10/11/11
171
Por uma questão de posteridade, e para qualquer programador mais novo que se deparar com isso, os indicadores não são ruins . Nem os indicadores da memória dinâmica são ruins. Ambos têm seus lugares legítimos em C ++. No entanto, os ponteiros inteligentes devem ser a sua opção padrão no gerenciamento dinâmico de memória, mas o ponteiro inteligente padrão deve ser unique_ptr, não shared_ptr.
Jamin Grey
12
Editar aprovadores: não aprove edições se não puder garantir sua correção. Reverti a edição incorreta.
GManNickG
7
Por uma questão de posteridade, e por quaisquer programadores mais novos que se depararem com isso, não escrevareturn new int .
Lightness Races in Orbit
3
Por uma questão de posteridade, e para qualquer programador mais novo que esteja de acordo com isso, basta retornar o T da função. A RVO cuidará de tudo.
Shoe
64

Não. Não, não, mil vezes não.

O que é mau é fazer referência a um objeto alocado dinamicamente e perder o ponteiro original. Quando você é newum objeto, assume uma obrigação de ter uma garantia delete.

Mas dê uma olhada, por exemplo, em operator<<: isso deve retornar uma referência, ou

cout << "foo" << "bar" << "bletch" << endl ;

não vai funcionar.

Charlie Martin
fonte
23
Fiz uma votação baixa porque isso não responde à pergunta (na qual o OP deixou claro que ele sabe a necessidade de exclusão) nem aborda o medo legítimo de que retornar uma referência a um objeto de armazenamento livre pode causar confusão. Suspiro.
4
A prática de retornar um objeto de referência não é ruim. Ergo no. O medo que ele expressa é um medo correto, como aponto no segundo graf.
10139 Charlie Martin
Você realmente não. Mas isso não vale o meu tempo.
2
Iraimbilanja @ Sobre o "Não" -s eu não me importo. mas este post apontou uma informação importante que estava faltando no GMan.
Kobor42
48

Você deve retornar uma referência a um objeto existente que não desaparece imediatamente e para o qual não pretende transferir a propriedade.

Nunca retorne uma referência a uma variável local ou algo parecido, porque ela não estará lá para ser referenciada.

Você pode retornar uma referência a algo independente da função, que não espera que a função de chamada assuma a responsabilidade de excluir. Este é o caso do típicooperator[] função .

Se você estiver criando algo, retorne um valor ou um ponteiro (regular ou inteligente). Você pode retornar um valor livremente, pois ele está entrando em uma variável ou expressão na função de chamada. Nunca retorne um ponteiro para uma variável local, pois ela desaparecerá.

David Thornley
fonte
11
Excelente resposta, mas para "Você pode retornar um temporário como uma referência const." O código a seguir será compilado, mas provavelmente trava porque o temporário é destruído no final da instrução de retorno: "int const & f () {return 42;} void main () {int const & r = f (); ++ r;} "
j_random_hacker 16/04/09
@j_random_hacker: C ++ possui algumas regras estranhas para referências a temporários, onde a vida útil temporária pode ser estendida. Sinto muito, mas não o entendi o suficiente para saber se ele cobre o seu caso.
Mark Ransom
3
@ Mark: Sim, ele tem algumas regras estranhas. A vida útil de um temporário só pode ser estendida inicializando uma referência const (que não é um membro da classe) com ele; ele vive até o árbitro sair do escopo. Infelizmente, retornar uma const ref não é coberto. Retornar uma temperatura por valor é seguro, no entanto.
j_random_hacker
Veja o Padrão C ++, 12.2, parágrafo 5. Veja também o Guru da Semana perdido de Herb Sutter em herbsutter.wordpress.com/2008/01/01/… .
21715 David Thornley
4
@ David: Quando o tipo de retorno da função é "T const &", o que realmente acontece é que a instrução return converte implicitamente a temp, que é do tipo T, para o tipo "T const &" conforme 6.6.3.2 (uma conversão legal, mas uma que não prolonga a vida útil) e, em seguida, o código de chamada inicializa a ref do tipo "T const &" com o resultado da função, também do tipo "T const &" - ​​novamente, um processo legal, mas que não prolonga a vida útil. Resultado final: nenhuma extensão da vida útil e muita confusão. :(
j_random_hacker
26

As respostas não são satisfatórias, então adicionarei meus dois centavos.

Vamos analisar os seguintes casos:

Uso incorreto

int& getInt()
{
    int x = 4;
    return x;
}

Isso é obviamente erro

int& x = getInt(); // will refer to garbage

Uso com variáveis ​​estáticas

int& getInt()
{
   static int x = 4;
   return x;
}

Isso está certo, porque variáveis ​​estáticas existem durante toda a vida útil de um programa.

int& x = getInt(); // valid reference, x = 4

Isso também é bastante comum ao implementar o padrão Singleton

Class Singleton
{
    public:
        static Singleton& instance()
        {
            static Singleton instance;
            return instance;
        };

        void printHello()
        {
             printf("Hello");
        };

}

Uso:

 Singleton& my_sing = Singleton::instance(); // Valid Singleton instance
 my_sing.printHello();  // "Hello"

Operadores

Os contêineres de biblioteca padrão dependem muito do uso de operadores que retornam referências, por exemplo

T & operator*();

pode ser usado no seguinte

std::vector<int> x = {1, 2, 3}; // create vector with 3 elements
std::vector<int>::iterator iter = x.begin(); // iterator points to first element (1)
*iter = 2; // modify first element, x = {2, 2, 3} now

Acesso rápido a dados internos

Há momentos em que & pode ser usado para acesso rápido a dados internos

Class Container
{
    private:
        std::vector<int> m_data;

    public:
        std::vector<int>& data()
        {
             return m_data;
        }
}

com o uso:

Container cont;
cont.data().push_back(1); // appends element to std::vector<int>
cont.data()[0] // 1

No entanto, isso pode levar a armadilhas como esta:

Container* cont = new Container;
std::vector<int>& cont_data = cont->data();
cont_data.push_back(1);
delete cont; // This is bad, because we still have a dangling reference to its internal data!
cont_data[0]; // dangling reference!
thorhunter
fonte
Voltando a referência a uma variável estática pode levar a um comportamento indesejado por exemplo, considerar um operador multiplicação que retorna uma referência a um elemento estático, em seguida, o seguinte será sempre resultar de true:If((a*b) == (c*d))
SebNag
Container::data()implementação deve lerreturn m_data;
Xeaz 21/02
Isso foi muito útil, obrigado! @Xeaz, isso não causaria problemas com a chamada anexada?
26517 Andrew Andrew
@SebTu Por que você quer fazer isso?
thorhunter
@ Andrew Não, era um shenanigan de sintaxe. Se você, por exemplo, retornasse um tipo de ponteiro, usaria o endereço de referência para criar e retornar um ponteiro.
thorhunter
14

Isso não é mau. Como muitas coisas no C ++, é bom se usado corretamente, mas há muitas armadilhas que você deve estar ciente ao usá-lo (como retornar uma referência a uma variável local).

Há coisas boas que podem ser alcançadas com ele (como map [name] = "olá mundo")

Mehrdad Afshari
fonte
11
Só estou curioso, sobre o que é bom map[name] = "hello world"?
wrongusername
5
@wrongusername A sintaxe é intuitiva. Já tentou aumentar a contagem de um valor armazenado em um HashMap<String,Integer>em Java? : P
Mehrdad Afshari
Haha, ainda não, mas olhando para os exemplos do HashMap, ele parece bastante
desonesto
Problema que tive com isso: Function retorna referência a um objeto em um contêiner, mas o código da função de chamada o atribuiu a uma variável local. Em seguida, modificou algumas propriedades do objeto. Problema: o objeto original no contêiner permaneceu intocado. O programador tão facilmente vista para o & no valor de retorno, e então você começa comportamentos realmente inesperado ...
flohack
10

"retornar uma referência é ruim porque, simplesmente [como eu entendo], é mais fácil deixar de excluí-la"

Não é verdade. Retornar uma referência não implica em semântica de propriedade. Ou seja, apenas porque você faz isso:

Value& v = thing->getTheValue();

... não significa que agora você possui a memória mencionada por v;

No entanto, este é um código horrível:

int& getTheValue()
{
   return *new int;
}

Se você estiver fazendo algo assim porque "você não precisa de um ponteiro nessa instância", então: 1) simplesmente desreferencie o ponteiro se precisar de uma referência e 2) você acabará precisando do ponteiro, porque precisa corresponder a um novo com uma exclusão e você precisa de um ponteiro para chamar a exclusão.

John Dibling
fonte
7

Existem dois casos:

  • referência const - boa idéia, às vezes, especialmente para objetos pesados ​​ou classes proxy, otimização do compilador

  • referência não-const - idéia ruim, às vezes, quebra encapsulamentos

Ambos compartilham o mesmo problema - podem apontar para um objeto destruído ...

Eu recomendaria o uso de ponteiros inteligentes para muitas situações em que você precisa retornar uma referência / ponteiro.

Além disso, observe o seguinte:

Existe uma regra formal - o Padrão C ++ (seção 13.3.3.1.4, se você estiver interessado) declara que um temporário só pode ser vinculado a uma referência const - se você tentar usar uma referência não const, o compilador deve sinalizar isso como um erro.

Sasha
fonte
11
ref não-const não necessariamente quebra o encapsulamento. considere vector :: operator []
isso é um caso muito especial ... é por isso que eu disse algumas vezes, embora eu realmente devem reivindicar maior parte do tempo :)
Então, você está dizendo que a implementação normal do operador subscrito é um mal necessário? Não discordo nem concordo com isso; como eu não sou o mais sábio.
22310 Nick Bolton
Eu não digo isso, mas pode ser mau se for mal :))) vector :: no deve ser usado sempre que possível ....
Eh? vector :: at também retorna uma referência não constante.
4

Não apenas não é mau, como às vezes é essencial. Por exemplo, seria impossível implementar o operador [] do std :: vector sem usar um valor de retorno de referência.

anon
fonte
Ah sim, claro; Eu acho que foi por isso que comecei a usá-lo; como quando implementei o operador subscrito [], percebi o uso de referências. Sou levado a acreditar que esse é o fato.
22412 Nick Bolton
Curiosamente, você pode implementar operator[]para um contêiner sem usar uma referência ... e std::vector<bool>faz. (E cria uma verdadeira bagunça no processo)
Ben Voigt
@BenVoigt mmm, por que uma bagunça? O retorno de um proxy também é um cenário válido para contêineres com armazenamento complexo que não é mapeado diretamente para os tipos externos (como ::std::vector<bool>você mencionou).
Sergey.quixoticaxis.Ivanov
11
@ Sergey.quixoticaxis.Ivanov: A bagunça é que o uso std::vector<T>no código do modelo está quebrado, se Tpossível bool, porque std::vector<bool>tem um comportamento muito diferente de outras instanciações. É útil, mas deveria ter recebido seu próprio nome e não uma especialização std::vector.
Ben Voigt
@ BenVoight Concordo com o ponto sobre a estranha decisão de tornar uma especialização "realmente especial", mas senti que seu comentário original implica que retornar um proxy é estranho em geral.
Sergey.quixoticaxis.Ivanov
2

Além da resposta aceita:

struct immutableint {
    immutableint(int i) : i_(i) {}

    const int& get() const { return i_; }
private:
    int i_;
};

Eu diria que este exemplo não está bem e deve ser evitado, se possível. Por quê? É muito fácil terminar com um referência pendente .

Para ilustrar o ponto com um exemplo:

struct Foo
{
    Foo(int i = 42) : boo_(i) {}
    immutableint boo()
    {
        return boo_;
    }  
private:
    immutableint boo_;
};

entrar na zona de perigo:

Foo foo;
const int& dangling = foo.boo().get(); // dangling reference!
AMA
fonte
1

A referência de retorno geralmente é usada na sobrecarga do operador em C ++ para Objeto grande, pois o retorno de um valor precisa de operação de cópia. (na sobrecarga do perator, geralmente não usamos ponteiro como valor de retorno)

Mas a referência de retorno pode causar problemas de alocação de memória. Como uma referência ao resultado será eliminada da função como uma referência ao valor de retorno, o valor de retorno não pode ser uma variável automática.

se você quiser usar referência de retorno, poderá usar um buffer de objeto estático. por exemplo

const max_tmp=5; 
Obj& get_tmp()
{
 static int buf=0;
 static Obj Buf[max_tmp];
  if(buf==max_tmp) buf=0;
  return Buf[buf++];
}
Obj& operator+(const Obj& o1, const Obj& o1)
{
 Obj& res=get_tmp();
 // +operation
  return res;
 }

Dessa forma, você pode usar a referência retornada com segurança.

Mas você sempre pode usar ponteiro em vez de referência para retornar valor em functiong.

MinandLucy
fonte
0

Eu acho que usar referência como valor de retorno da função é muito mais direto do que usar ponteiro como valor de retorno da função. Em segundo lugar, seria sempre seguro usar a variável estática à qual o valor de retorno se refere.

Tony
fonte
0

O melhor é criar um objeto e passá-lo como parâmetro de referência / ponteiro para uma função que aloca essa variável.

Alocar um objeto na função e retorná-lo como uma referência ou ponteiro (porém, o ponteiro é mais seguro) é uma má idéia, pois libera memória no final do bloco de funções.

Drezir
fonte
-1
    Class Set {
    int *ptr;
    int size;

    public: 
    Set(){
     size =0;
         }

     Set(int size) {
      this->size = size;
      ptr = new int [size];
     }

    int& getPtr(int i) {
     return ptr[i];  // bad practice 
     }
  };

A função getPtr pode acessar a memória dinâmica após a exclusão ou mesmo um objeto nulo. O que pode causar exceções de acesso incorreto. Em vez disso, getter e setter devem ser implementados e o tamanho verificado antes de retornar.

Amarbir
fonte
-2

A função como lvalue (também conhecido como retorno de referências não const) deve ser removida do C ++. É terrivelmente pouco intuitivo. Scott Meyers queria um minuto () com esse comportamento.

min(a,b) = 0;  // What???

o que não é realmente uma melhoria

setmin (a, b, 0);

O último ainda faz mais sentido.

Sei que a função como lvalue é importante para os fluxos de estilo C ++, mas vale ressaltar que os fluxos de estilo C ++ são terríveis. Eu não sou o único que pensa isso ... Lembro-me de que Alexandrescu tinha um grande artigo sobre como fazer melhor, e acredito que o boost também tentou criar um método de E / S seguro de tipo melhor.

Dan Olson
fonte
2
Claro que é perigoso, e deve haver uma melhor verificação de erro do compilador, mas sem ela algumas construções úteis não poderiam ser feitas, por exemplo, operator [] () no std :: map.
Jrandom_hacker 16/04/2009
2
Retornar referências não-const é realmente incrivelmente útil. vector::operator[]por exemplo. Você prefere escrever v.setAt(i, x)ou v[i] = x? Este último é MUITO superior.
Route de milhas
11
@MilesRout eu iria a v.setAt(i, x)qualquer momento. É MUITO superior.
Scravy
-2

Eu me deparei com um problema real, onde era realmente mau. Essencialmente, um desenvolvedor retornou uma referência a um objeto em um vetor. Isso foi ruim!!!

Os detalhes completos sobre os quais escrevi no Janurary: http://developer-resource.blogspot.com/2009/01/pros-and-cons-of-returing-references.html

desenvolvem recursos
fonte
2
Se você precisar modificar o valor original no código de chamada, será necessário retornar uma ref. E isso é, de fato, nem mais nem menos perigoso do que retornar um iterador para um vetor - ambos são invalidados se elementos forem adicionados ou removidos do vetor.
j_random_hacker
Esse problema específico foi causado mantendo uma referência a um elemento vetorial e modificando esse vetor de uma maneira que invalida a referência: Página 153, seção 6.2 da "Biblioteca padrão do C ++: tutorial e referência" - Josuttis, lê: "Inserindo ou remover elementos invalida referências, ponteiros e iteradores que se referem aos seguintes elementos: Se uma inserção causa realocação, ela invalida todas as referências, iteradores e ponteiros "
Trent
-15

Sobre código horrível:

int& getTheValue()
{
   return *new int;
}

Então, de fato, o ponteiro da memória perdeu após o retorno. Mas se você usar shared_ptr assim:

int& getTheValue()
{
   std::shared_ptr<int> p(new int);
   return *p->get();
}

A memória não é perdida após o retorno e será liberada após a atribuição.

Anatoly
fonte
12
É perdido porque o ponteiro compartilhado sai do escopo e libera o número inteiro.
o ponteiro não está perdido, o endereço da referência é o ponteiro.
dgsomerton