¿Es mala la práctica de devolver una variable de referencia de C ++?


341

Esto es un poco subjetivo, creo; No estoy seguro de si la opinión será unánime (he visto muchos fragmentos de código donde se devuelven referencias).

De acuerdo con un comentario sobre esta pregunta que acabo de hacer, con respecto a la inicialización de referencias , devolver una referencia puede ser malo porque, [según tengo entendido], hace que sea más fácil no borrarlo, lo que puede provocar pérdidas de memoria.

Esto me preocupa, ya que he seguido ejemplos (a menos que esté imaginando cosas) y haya hecho esto en unos pocos lugares ... ¿He entendido mal? ¿Es malo? Si es así, ¿qué tan malvado?

Siento que debido a mi mezcla de punteros y referencias, combinado con el hecho de que soy nuevo en C ++, y la confusión total sobre qué usar cuando, mis aplicaciones deben ser una pérdida de memoria ...

Además, entiendo que el uso de punteros inteligentes / compartidos es generalmente aceptado como la mejor manera de evitar pérdidas de memoria.


No es malo si estás escribiendo funciones / métodos tipo getter.
John Z. Li

Respuestas:


411

En general, devolver una referencia es perfectamente normal y ocurre todo el tiempo.

Si te refieres a:

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

Eso es todo tipo de maldad. La pila asignada idesaparecerá y no te estás refiriendo a nada. Esto también es malo:

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

Porque ahora el cliente finalmente tiene que hacer lo extraño:

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

Tenga en cuenta que las referencias de valor siguen siendo solo referencias, por lo que todas las aplicaciones malvadas siguen siendo las mismas.

Si desea asignar algo que vive más allá del alcance de la función, use un puntero inteligente (o, en general, un contenedor):

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

Y ahora el cliente almacena un puntero inteligente:

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

Las referencias también están bien para acceder a cosas donde sabe que la vida útil se mantiene abierta en un nivel superior, por ejemplo:

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

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

Aquí sabemos que está bien devolver una referencia i_porque lo que sea que nos llame gestiona la vida útil de la instancia de clase, por i_lo que vivirá al menos ese tiempo.

Y, por supuesto, no hay nada malo con solo:

int getInt() {
   return 0;
}

Si el tiempo de vida debe dejarse en manos de la persona que llama, y ​​solo está calculando el valor.

Resumen: está bien devolver una referencia si la vida útil del objeto no terminará después de la llamada.


21
Todos estos son malos ejemplos. El mejor ejemplo de uso adecuado es cuando va a devolver una referencia a un objeto que fue aprobada en el operador << Ala.
Arelius

171
Por el bien de la posteridad, y para cualquier programador más nuevo que se meta en esto, los indicadores no son malos . Tampoco son malos los punteros a la memoria dinámica. Ambos tienen sus lugares legítimos en C ++. Sin embargo, los punteros inteligentes definitivamente deberían ser su opción predeterminada cuando se trata de la administración de memoria dinámica, pero su puntero inteligente predeterminado debería ser unique_ptr, no shared_ptr.
Jamin Gray

12
Editar aprobadores: no apruebe las ediciones si no puede garantizar su corrección. Revertí la edición incorrecta.
GManNickG

77
En aras de la posteridad, y para cualquier programador más nuevo que se meta en esto, no escribareturn new int .
ligereza corre en órbita el

3
En aras de la posteridad, y para cualquier programador más nuevo que cambie esto, simplemente devuelva la T de la función. RVO se encargará de todo.
Zapato

64

No. No, no, mil veces no.

Lo que es malo es hacer referencia a un objeto asignado dinámicamente y perder el puntero original. Cuando usted newobjeta, asume la obligación de tener una garantía delete.

Pero eche un vistazo, por ejemplo, operator<<que debe devolver una referencia, o

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

no funciona


23
Voté en contra porque esto no responde a la pregunta (en la que OP ha dejado en claro que él sabe la necesidad de eliminación) ni aborda el temor legítimo de que devolver una referencia a un objeto de almacén libre pueda generar confusión. Suspiro.

44
La práctica de devolver un objeto de referencia no es mala. Ergo no. El miedo que expresa es un miedo correcto, como señalo en el segundo graf.
Charlie Martin

En realidad no lo hiciste. Pero esto no vale mi tiempo.

2
Iraimbilanja @ Sobre el "No" -s no me importa. pero esta publicación señaló una información importante que faltaba en GMan.
Kobor42

48

Debe devolver una referencia a un objeto existente que no desaparecerá de inmediato y donde no tiene intención de transferir ninguna propiedad.

Nunca devuelva una referencia a una variable local o algo así, porque no estará allí para ser referenciada.

Puede devolver una referencia a algo independiente de la función, que no espera que la función de llamada asuma la responsabilidad de eliminar. Este es el caso de la operator[]función típica .

Si está creando algo, debe devolver un valor o un puntero (normal o inteligente). Puede devolver un valor libremente, ya que entra en una variable o expresión en la función de llamada. Nunca devuelva un puntero a una variable local, ya que desaparecerá.


1
Excelente respuesta pero para "Puede devolver un temporal como referencia constante". El siguiente código se compilará pero probablemente se bloqueará porque el temporal se destruye al final de la declaración de retorno: "int const & f () {return 42;} void main () {int const & r = f (); ++ r;} "
j_random_hacker

@j_random_hacker: C ++ tiene algunas reglas extrañas para referencias a temporarios, donde el tiempo de vida temporal podría extenderse. Lo siento, no lo entiendo lo suficientemente bien como para saber si cubre su caso.
Mark Ransom

3
@ Mark: Sí, tiene algunas reglas extrañas. La vida útil de un temporal solo se puede extender inicializando una referencia constante (que no es un miembro de la clase) con ella; luego vive hasta que el árbitro sale del alcance. Lamentablemente, devolver una referencia constante no está cubierto. Sin embargo, devolver una temperatura por valor es seguro.
j_random_hacker

Vea el Estándar C ++, 12.2, párrafo 5. También vea el Gurú de la semana perdido de Herb Sutter en herbalutter.wordpress.com/2008/01/01/… .
David Thornley

44
@David: cuando el tipo de retorno de la función es "T const &", lo que realmente sucede es que la declaración de retorno convierte implícitamente la temp, que es de tipo T, a "T const &" según 6.6.3.2 (una conversión legal pero una que no extiende la vida útil), y luego el código de llamada inicializa la referencia del tipo "T const &" con el resultado de la función, también del tipo "T const &" - ​​nuevamente, un proceso legal pero que no extiende la vida útil. Resultado final: sin extensión de la vida útil, y mucha confusión. :(
j_random_hacker

26

Las respuestas no me parecen satisfactorias, así que agregaré mis dos centavos.

Analicemos los siguientes casos:

Uso erróneo

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

Esto obviamente es un error

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

Uso con variables estáticas

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

Esto es correcto, porque las variables estáticas existen durante toda la vida de un programa.

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

Esto también es bastante común cuando se implementa el patrón 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

Los contenedores de biblioteca estándar dependen en gran medida del uso de operadores que devuelven referencias, por ejemplo

T & operator*();

puede usarse en lo siguiente

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

Acceso rápido a datos internos.

Hay momentos en los que & se puede usar para acceder rápidamente a datos internos

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

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

con uso:

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

SIN EMBARGO, esto puede conducir a dificultades 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!

Devolver la referencia a una variable estática puede conducir a un comportamiento no deseado, por ejemplo, considere un operador de multiplicación que devuelve una referencia a un miembro estático, lo siguiente siempre resultará en true:If((a*b) == (c*d))
SebNag

Container::data()La implementación de debería leerreturn m_data;
Xeaz

Esto fue muy útil, gracias! @Xeaz, ¿eso no causaría problemas con la llamada anexar?
Andrew

@SebTu ¿Por qué querrías hacer eso?
thorhunter

@ Andrew No, fue una sintaxis shenanigan. Si, por ejemplo, devolvió un tipo de puntero, utilizaría la dirección de referencia para crear y devolver un puntero.
thorhunter

14

No es malo Al igual que muchas cosas en C ++, es bueno si se usa correctamente, pero hay muchas dificultades que debe tener en cuenta al usarlo (como devolver una referencia a una variable local).

Hay cosas buenas que se pueden lograr con él (como map [name] = "hello world")


1
Solo tengo curiosidad, ¿qué tiene de bueno map[name] = "hello world"?
wrongusername

55
@wrongusername La sintaxis es intuitiva. ¿Alguna vez intentó aumentar el recuento de un valor almacenado en un HashMap<String,Integer>en Java? : P
Mehrdad Afshari

Jaja, todavía no, pero mirando ejemplos de HashMap, parece bastante retorcido: D
usuario incorrecto

Problema que tuve con esto: la función devuelve una referencia a un objeto en un contenedor, pero el código de la función de llamada lo asignó a una variable local. Luego modificó algunas propiedades del objeto. Problema: el objeto original en el contenedor permaneció intacto. El programador domina tan fácilmente y el en el valor de retorno, y luego se obtiene realmente comportamientos inesperados ...
flohack

10

"devolver una referencia es malo porque, simplemente, según tengo entendido, hace que sea más fácil no borrarla"

No es verdad. Devolver una referencia no implica semántica de propiedad. Es decir, solo porque haces esto:

Value& v = thing->getTheValue();

... no significa que ahora posee la memoria a la que se refiere v;

Sin embargo, este es un código horrible:

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

Si está haciendo algo como esto porque "no necesita un puntero en esa instancia", entonces: 1) simplemente desreferenciar el puntero si necesita una referencia, y 2) eventualmente necesitará el puntero, porque debe hacer coincidir un nuevo con una eliminación, y necesita un puntero para llamar a la eliminación.


7

Hay dos casos:

  • referencia constante: buena idea, a veces, especialmente para objetos pesados ​​o clases proxy, optimización del compilador

  • referencia no constante: una mala idea, a veces, rompe encapsulaciones

Ambos comparten el mismo problema: pueden señalar potencialmente objetos destruidos ...

Recomendaría usar punteros inteligentes para muchas situaciones en las que necesita devolver una referencia / puntero.

Además, tenga en cuenta lo siguiente:

Existe una regla formal: el estándar C ++ (sección 13.3.3.1.4 si está interesado) establece que un temporal solo puede vincularse a una referencia constante; si intenta utilizar una referencia no constante, el compilador debe marcar esto como un error.


1
la referencia no constante no necesariamente rompe la encapsulación. considere vector :: operador []

eso es un caso muy especial ... por eso he dicho algunas veces, aunque realmente debería reclamar MAYOR PARTE DEL TIEMPO :)

Entonces, ¿estás diciendo que la implementación del operador de subíndice normal es un mal necesario? No estoy en desacuerdo ni estoy de acuerdo con esto; como no soy el más sabio.
Nick Bolton el

No digo eso, pero puedo ser malo si se utilizan mal :))) :: vector a se debe utilizar siempre que sea posible ....

eh? vector :: at también devuelve una referencia no constante.

4

No solo no es malo, a veces es esencial. Por ejemplo, sería imposible implementar el operador [] de std :: vector sin usar un valor de retorno de referencia.


Ah, sí por supuesto; Creo que es por eso que comencé a usarlo; como cuando implementé por primera vez el operador de subíndice [] me di cuenta del uso de referencias. Me hacen creer que este es el hecho.
Nick Bolton el

Curiosamente, puede implementar operator[]para un contenedor sin usar una referencia ... y lo std::vector<bool>hace. (Y crea un verdadero desastre en el proceso)
Ben Voigt

@BenVoigt mmm, ¿por qué un desastre? Devolver un proxy también es un escenario válido para contenedores con almacenamiento complejo que no se asigna directamente a los tipos externos (como ::std::vector<bool>ha mencionado).
Sergey.quixoticaxis.Ivanov

1
@ Sergey.quixoticaxis.Ivanov: El desorden es que el uso del std::vector<T>código de plantilla está roto, si es Tposible bool, porque std::vector<bool>tiene un comportamiento muy diferente al de otras instancias. Es útil, pero debería haber recibido su propio nombre y no una especialización std::vector.
Ben Voigt

@BenVoight Estoy de acuerdo con el punto sobre la extraña decisión de hacer una especialización "realmente especial", pero sentí que su comentario original implica que devolver un proxy es extraño en general.
Sergey.quixoticaxis.Ivanov

2

Adición a la respuesta aceptada:

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

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

Yo diría que este ejemplo no está bien y debería evitarse si es posible. ¿Por qué? Es muy fácil terminar con una referencia colgante .

Para ilustrar el punto con un ejemplo:

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

entrando en la zona de peligro:

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

1

la referencia de retorno se usa generalmente en la sobrecarga de operadores en C ++ para objetos grandes, porque devolver un valor necesita una operación de copia (en la sobrecarga del perador, generalmente no usamos el puntero como valor de retorno)

Pero la referencia de retorno puede causar problemas de asignación de memoria. Debido a que una referencia al resultado se pasará de la función como una referencia al valor de retorno, el valor de retorno no puede ser una variable automática.

si desea usar referencias de retorno, puede usar un búfer de objeto estático. por ejemplo

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;
 }

de esta manera, puede usar la referencia de retorno de forma segura.

Pero siempre puede usar el puntero en lugar de la referencia para devolver el valor en functiong.


0

Creo que usar la referencia como valor de retorno de la función es mucho más sencillo que usar el puntero como valor de retorno de la función. En segundo lugar, siempre sería seguro utilizar una variable estática a la que se refiera el valor de retorno.


0

Lo mejor es crear un objeto y pasarlo como parámetro de referencia / puntero a una función que asigna esta variable.

Asignar un objeto en la función y devolverlo como referencia o puntero (sin embargo, el puntero es más seguro) es una mala idea debido a que libera memoria al final del bloque de funciones.


-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 
     }
  };

La función getPtr puede acceder a la memoria dinámica después de la eliminación o incluso a un objeto nulo. Lo que puede causar malas excepciones de acceso. En cambio, getter y setter deben implementarse y verificarse el tamaño antes de regresar.


-2

La función como lvalue (también conocido como devolución de referencias no constantes) debe eliminarse de C ++. Es terriblemente poco intuitivo. Scott Meyers quería un min () con este comportamiento.

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

lo cual no es realmente una mejora en

setmin (a, b, 0);

Este último incluso tiene más sentido.

Me doy cuenta de que la función como lvalue es importante para las secuencias de estilo C ++, pero vale la pena señalar que las secuencias de estilo C ++ son terribles. No soy el único que piensa esto ... ya que recuerdo que Alexandrescu tenía un gran artículo sobre cómo hacerlo mejor, y creo que boost también ha tratado de crear un mejor método de E / S seguro.


2
Claro que es peligroso, y debería haber una mejor comprobación de errores del compilador, pero sin ella no se podrían hacer algunas construcciones útiles, por ejemplo, el operador [] () en std :: map.
j_random_hacker

2
Devolver referencias no constantes es realmente increíblemente útil. vector::operator[]por ejemplo. ¿Prefieres escribir v.setAt(i, x)o v[i] = x? Este último es MUY superior.
Miles Rout

1
@MilesRout Me gustaría ir en v.setAt(i, x)cualquier momento. Es muy superior.
scravy

-2

Me encontré con un problema real donde realmente era malvado. Esencialmente, un desarrollador devolvió una referencia a un objeto en un vector. ¡¡¡Eso fue malo!!!

Los detalles completos sobre los que escribí en enero: http://developer-resource.blogspot.com/2009/01/pros-and-cons-of-returing-references.html


2
Si necesita modificar el valor original en el código de llamada, debe devolver una referencia. Y eso, de hecho, no es más ni menos peligroso que devolver un iterador a un vector: ambos se invalidan si se agregan o eliminan elementos del vector.
j_random_hacker

Ese problema particular fue causado al mantener una referencia a un elemento vectorial y luego modificar ese vector de una manera que invalida la referencia: Página 153, sección 6.2 de "Biblioteca estándar de C ++: un tutorial y referencia" - Josuttis, lee: "Insertar o eliminar elementos invalida referencias, punteros e iteradores que hacen referencia a los siguientes elementos. Si una inserción causa una reasignación, invalida todas las referencias, iteradores y punteros "
Trent

-15

Sobre código horrible:

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

Entonces, de hecho, el puntero de memoria se perdió después del regreso. Pero si usa shared_ptr así:

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

La memoria no se pierde después del regreso y se liberará después de la asignación.


12
Se pierde porque el puntero compartido se sale del alcance y libera el entero.

el puntero no se pierde, la dirección de la referencia es el puntero.
dgsomerton
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.