Variables de miembro de referencia como miembros de clase

81

En mi lugar de trabajo veo que este estilo se usa ampliamente:

#include <iostream>

using namespace std;

class A
{
public:
   A(int& thing) : m_thing(thing) {}
   void printit() { cout << m_thing << endl; }

protected:
   const int& m_thing; //usually would be more complex object
};


int main(int argc, char* argv[])
{
   int myint = 5;
   A myA(myint);
   myA.printit();
   return 0;
}

¿Hay un nombre para describir este idioma? ¿Supongo que es para evitar la sobrecarga posiblemente grande de copiar un objeto complejo grande?

¿Es esta una buena práctica en general? ¿Hay dificultades en este enfoque?

Angus Comber
fuente
4
Un posible error es si el objeto al que tiene una referencia en la variable miembro se destruye en otro lugar e intenta acceder a él a través de su clase
mathematician1975

Respuestas:

114

¿Hay un nombre para describir este idioma?

En UML se llama agregación. Se diferencia de la composición en que el objeto miembro no es propiedad de la clase de referencia. En C ++ puede implementar la agregación de dos formas diferentes, mediante referencias o punteros.

¿Supongo que es para evitar la sobrecarga posiblemente grande de copiar un objeto complejo grande?

No, esa sería una muy mala razón para usar esto. La razón principal de la agregación es que el objeto contenido no es propiedad del objeto contenedor y, por lo tanto, su vida útil no está limitada. En particular, la vida útil del objeto referenciado debe sobrevivir a la de referencia. Es posible que se haya creado mucho antes y que viva más allá del final de la vida útil del contenedor. Además de eso, el estado del objeto referenciado no está controlado por la clase, pero puede cambiar externamente. Si la referencia no lo es const, entonces la clase puede cambiar el estado de un objeto que vive fuera de él.

¿Es esta una buena práctica en general? ¿Hay dificultades en este enfoque?

Es una herramienta de diseño. En algunos casos será una buena idea, en otros no. El error más común es que la vida útil del objeto que contiene la referencia nunca debe exceder la vida útil del objeto referenciado. Si el objeto adjunto usa la referencia después de que se destruyó el objeto al que se hace referencia, tendrá un comportamiento indefinido. En general, es mejor preferir la composición a la agregación, pero si lo necesita, es una herramienta tan buena como cualquier otra.

David Rodríguez - dribeas
fuente
7
"No, esa sería una muy mala razón para usar esto". ¿Podría dar más detalles sobre este punto? ¿Qué podría usarse en su lugar para lograr eso?
Moneda
@coincoin: ¿Para lograr qué exactamente?
David Rodríguez - dribeas
3
to prevent the possibly large overhead of copying a big complex object?
Moneda el
3
@underscore_d gracias por tu respuesta. Entonces, ¿qué sucede cuando no puede usar ninguno de ellos? Imagina que queremos compartir el mismo objeto dentro de diferentes clases. ¿Terminas con una copia del objeto para cada clase si pasas este objeto miembro por valor? Entonces, la solución es usar punteros inteligentes o referencias para evitar la copia. No ?
Moneda
2
@ plats1 eso es lo que acabo de escribir, lo sé. Mi punto es que puede usar punteros inteligentes o referencias.
moneda el
33

Se llama inyección de dependencia mediante inyección de constructor : la clase Aobtiene la dependencia como argumento para su constructor y guarda la referencia a la clase dependiente como una variable privada.

Hay una interesante introducción en wikipedia .

Para una corrección constante , escribiría:

using T = int;

class A
{
public:
  A(const T &thing) : m_thing(thing) {}
  // ...

private:
   const T &m_thing;
};

pero un problema con esta clase es que acepta referencias a objetos temporales:

T t;
A a1{t};    // this is ok, but...

A a2{T()};  // ... this is BAD.

Es mejor agregar (requiere C ++ 11 como mínimo):

class A
{
public:
  A(const T &thing) : m_thing(thing) {}
  A(const T &&) = delete;  // prevents rvalue binding
  // ...

private:
  const T &m_thing;
};

De todos modos si cambia el constructor:

class A
{
public:
  A(const T *thing) : m_thing(*thing) { assert(thing); }
  // ...

private:
   const T &m_thing;
};

está prácticamente garantizado que no tendrá un puntero a un archivo temporal .

Además, dado que el constructor toma un puntero, es más claro para los usuarios Aque deben prestar atención a la vida útil del objeto que pasan.


Temas algo relacionados son:

manlio
fuente
20

¿Hay un nombre para describir este idioma?

No hay un nombre para este uso, simplemente se conoce como "Referencia como miembro de clase" .

¿Supongo que es para evitar la sobrecarga posiblemente grande de copiar un objeto complejo grande?

Sí, y también escenarios en los que desea asociar la vida útil de un objeto con otro objeto.

¿Es esta una buena práctica en general? ¿Hay dificultades en este enfoque?

Depende de su uso. Usar cualquier característica del idioma es como "elegir caballos para los cursos" . Es importante tener en cuenta que todas ( casi todas ) las características del idioma existen porque son útiles en algunos escenarios.
Hay algunos puntos importantes a tener en cuenta al usar referencias como miembros de la clase:

  • Debe asegurarse de que se garantice la existencia del objeto referido hasta que exista su objeto de clase.
  • Necesita inicializar el miembro en la lista de inicializadores de miembros constructores. No puede tener una inicialización diferida , lo que podría ser posible en el caso de un miembro puntero.
  • El compilador no generará la asignación de copia operator=()y tendrá que proporcionar una usted mismo. Es complicado determinar qué acción =tomará su operador en tal caso. Entonces, básicamente, su clase se vuelve no asignable .
  • No se pueden NULLhacer referencias ni hacer referencia a ningún otro objeto. Si necesita volver a colocarlo, entonces no es posible con una referencia como en el caso de un puntero.

Para la mayoría de los propósitos prácticos (a menos que esté realmente preocupado por el alto uso de memoria debido al tamaño del miembro), basta con tener una instancia de miembro, en lugar de un puntero o miembro de referencia. Esto le ahorra una gran cantidad de preocupaciones sobre otros problemas que traen consigo los miembros de referencia / puntero, aunque a costa del uso de memoria adicional.

Si debe utilizar un puntero, asegúrese de utilizar un puntero inteligente en lugar de un puntero sin formato. Eso haría tu vida mucho más fácil con los consejos.

Alok Save
fuente
"¿ Supongo que es para evitar la posible sobrecarga de copiar un objeto grande y complejo? Sí". Por favor, explique por qué cree que este patrón tiene alguna relevancia para evitar la copia, ya que no puedo ver ninguno. Si este no-'idiom 'de alguna manera guarda copias para cualquiera como un efecto secundario de sus propósitos reales, entonces su diseño original fue fatalmente defectuoso para empezar, y es poco probable que reemplazarlo en su lugar con este patrón haga lo que esperan. .
underscore_d
@underscore_d Digamos que una clase necesita una cantidad no trivial de datos constantes y puede haber muchas instancias de esta clase al mismo tiempo, podría ser inaceptablemente un desperdicio para cada instancia tener su propia copia de esos datos constantes. Por lo tanto, guardar una referencia constante a una ubicación externa de esos datos que se pueden compartir ahorra muchas copias. shared_ptr no es necesariamente una solución porque el identificador de datos no necesita ser asignado de forma dinámica.
importancia
@Unimportant No estoy seguro de cuál fue mi objeción en 2016. Uso referencias como miembros de la clase todo el tiempo. Tal vez me preocupaba que simplemente reemplazar la propiedad por valor con referencias daría lugar a preguntas de por vida y no es necesariamente una panacea o algo que siempre se puede hacer 1: 1. No se.
underscore_d
1

C ++ proporciona un buen mecanismo para administrar el tiempo de vida de un objeto a través de construcciones de clase / estructura. Esta es una de las mejores características de C ++ sobre otros lenguajes.

Cuando tiene variables miembro expuestas a través de ref o puntero, viola la encapsulación en principio. Este modismo permite al consumidor de la clase cambiar el estado de un objeto de A sin que (A) tenga conocimiento o control del mismo. También permite al consumidor aferrarse a una referencia / puntero al estado interno de A, más allá de la vida útil del objeto de A. Este es un mal diseño. En su lugar, la clase podría refactorizarse para contener una referencia / puntero al objeto compartido (no poseerlo) y estos podrían establecerse usando el constructor (Exigir las reglas de tiempo de vida). La clase del objeto compartido puede diseñarse para admitir subprocesos múltiples / concurrencia, según corresponda.

Indy9000
fuente
2
pero el código en el OP no contiene una referencia a ninguna variable miembro. tiene una variable miembro que es una referencia. entonces, grandes puntos, pero aparentemente sin relación.
underscore_d
-2

Las referencias de miembros generalmente se consideran malas. Hacen la vida más difícil en comparación con los indicadores de los miembros. Pero no es particularmente inusual, ni es un modismo especial con nombre o algo. Es solo un alias.

Perrito
fuente
23
¿Puede proporcionar referencias de soporte que generalmente se consideran malas ?
David Rodríguez - dribeas