¿Debo preferir punteros o referencias en los datos de los miembros?

138

Este es un ejemplo simplificado para ilustrar la pregunta:

class A {};

class B
{
    B(A& a) : a(a) {}
    A& a;
};

class C
{
    C() : b(a) {} 
    A a;
    B b; 
};

Entonces B es responsable de actualizar una parte de C. Ejecuté el código a través de lint y se quejó sobre el miembro de referencia: lint # 1725 . Esto habla de tener cuidado con la copia predeterminada y las asignaciones, lo cual es bastante justo, pero la copia predeterminada y la asignación también son malas con los punteros, por lo que hay poca ventaja allí.

Siempre trato de usar referencias donde puedo, ya que los punteros desnudos introducen incertidumbre sobre quién es responsable de eliminar ese puntero. Prefiero incrustar objetos por valor, pero si necesito un puntero, uso auto_ptr en los datos de miembros de la clase que posee el puntero y paso el objeto como referencia.

En general, solo usaría un puntero en los datos del miembro cuando el puntero podría ser nulo o podría cambiar. ¿Hay alguna otra razón para preferir punteros sobre referencias para miembros de datos?

¿Es cierto decir que un objeto que contiene una referencia no debe ser asignable, ya que una referencia no debe cambiarse una vez inicializada?

markh44
fuente
"pero la copia y la asignación predeterminadas también son malas con los punteros": Esto no es lo mismo: un puntero se puede cambiar cuando lo desee si no es constante. Una referencia es normalmente siempre constante! (Verá esto si intenta cambiar su miembro a "A & const a;". El compilador (al menos GCC) le advertirá que la referencia es constante de todos modos, incluso sin la palabra clave "const".
mmmmmmmm
El principal problema con esto es que si alguien hace B b (A ()), estás jodido porque estás terminando con una referencia colgante.
log0

Respuestas:

68

Evite los miembros de referencia, ya que restringen lo que puede hacer la implementación de una clase (incluido, como usted menciona, evitar la implementación de un operador de asignación) y no proporcionan beneficios a lo que la clase puede proporcionar.

Problemas de ejemplo:

  • está obligado a inicializar la referencia en la lista de inicializadores de cada constructor: no hay forma de factorizar esta inicialización en otra función ( hasta C ++ 0x, de todos modos edite: C ++ ahora tiene delegadores de constructores )
  • la referencia no puede ser rebote o ser nula. Esto puede ser una ventaja, pero si el código necesita cambiar alguna vez para permitir volver a vincular o para que el miembro sea nulo, todos los usos del miembro deben cambiar
  • A diferencia de los miembros de puntero, las referencias no pueden reemplazarse fácilmente por punteros o iteradores inteligentes, ya que la refactorización puede requerir
  • Cada vez que se utiliza una referencia, parece un tipo de valor ( .operador, etc.), pero se comporta como un puntero (puede colgar), por lo que, por ejemplo, la Guía de estilo de Google lo desalienta
James Hopkin
fuente
65
Todas las cosas que ha mencionado son buenas para evitar, por lo que si las referencias ayudan en esto, son buenas y no están mal. La lista de inicialización es el mejor lugar para iniciar los datos. Muy a menudo tiene que ocultar el operador de asignación, con referencias que no tiene que hacerlo. "No se puede rebotar": bueno, la reutilización de variables es una mala idea.
Mykola Golubyev
44
@ Mykola: Estoy de acuerdo contigo. Prefiero que los miembros se inicialicen en las listas de inicializadores, evito los punteros nulos y ciertamente no es bueno que una variable cambie su significado. Donde nuestras opiniones difieren es que no necesito o no quiero que el compilador haga cumplir esto por mí. No hace que las clases sean más fáciles de escribir, no he encontrado errores en esta área que pueda detectar, y ocasionalmente aprecio la flexibilidad que obtengo del código que usa constantemente miembros de puntero (punteros inteligentes cuando corresponde).
James Hopkin
Creo que no tener que ocultar la asignación es un argumento falso, ya que no tiene que ocultar la asignación si la clase contiene un puntero que no es responsable de eliminar de todos modos; el valor predeterminado lo hará. Creo que esta es la mejor respuesta porque brinda buenas razones por las cuales los punteros deberían preferirse a las referencias en los datos de los miembros.
markh44
44
James, no es que haga que el código sea más fácil de escribir, es que hace que el código sea más fácil de leer. Con una referencia como miembro de datos, nunca tendrá que preguntarse si puede ser nulo en el punto de uso. Esto significa que puede mirar el código con menos contexto requerido.
Len Holgate
44
Hace que las pruebas unitarias y la burla sean mucho más difíciles, casi imposible dependiendo de cuántas se usen, combinadas con 'explosión de gráfico de afecto' son, en mi humilde opinión, razones suficientes para nunca usarlas.
Chris Huang-Leaver
155

Mi propia regla de oro:

  • Utilice un miembro de referencia cuando desee que la vida de su objeto dependa de la vida de otros objetos : es una forma explícita de decir que no permite que el objeto esté vivo sin una instancia válida de otra clase, debido a que no asignación y la obligación de obtener la inicialización de referencias a través del constructor. Es una buena manera de diseñar su clase sin asumir nada acerca de que su instancia sea miembro o no de otra clase. Solo asume que sus vidas están directamente vinculadas a otras instancias. Le permite cambiar más adelante cómo usa su instancia de clase (con nueva, como instancia local, como miembro de clase, generada por un grupo de memoria en un administrador, etc.)
  • Use el puntero en otros casos : cuando desee que el miembro se cambie más tarde, use un puntero o un puntero constante para asegurarse de leer solo la instancia puntiaguda. Si se supone que ese tipo es copiable, no puede utilizar referencias de todos modos. A veces también necesita inicializar el miembro después de una llamada de función especial (init (), por ejemplo) y luego simplemente no tiene más remedio que usar un puntero. PERO: ¡use afirmaciones en todas sus funciones miembro para detectar rápidamente un estado de puntero incorrecto!
  • En los casos en los que desea que la vida útil del objeto dependa de la vida útil de un objeto externo, y también necesita que ese tipo sea copiable, use los miembros del puntero pero el argumento de referencia en el constructor De esa manera, indica en la construcción que la vida útil de este objeto depende en la vida útil del argumento PERO la implementación usa punteros para que todavía se pueda copiar. Siempre y cuando estos miembros solo se cambien por copia, y su tipo no tenga un constructor predeterminado, el tipo debe cumplir ambos objetivos.
Klaim
fuente
1
Creo que tu y Mykola son respuestas correctas y promueven la programación sin puntero (leer menos puntero).
amit
37

Los objetos rara vez deberían permitir la asignación y otras cosas como la comparación. Si considera algún modelo de negocio con objetos como 'Departamento', 'Empleado', 'Director', es difícil imaginar un caso en el que un empleado sea asignado a otro.

Por lo tanto, para los objetos comerciales es muy bueno describir las relaciones uno a uno y uno a muchos como referencias y no como indicadores.

Y probablemente esté bien describir una relación de uno o cero como un puntero.

Entonces no 'no podemos asignar' entonces factor.
Muchos programadores simplemente se acostumbran a los punteros y es por eso que encontrarán cualquier argumento para evitar el uso de referencias.

Tener un puntero como miembro lo obligará a usted o al miembro de su equipo a verificar el puntero una y otra vez antes de usarlo, con un comentario "por si acaso". Si un puntero puede ser cero, entonces el puntero probablemente se usa como un tipo de indicador, lo cual es malo, ya que cada objeto tiene que desempeñar su propio papel.

Mykola Golubyev
fuente
2
+1: Lo mismo que experimenté. Solo algunas clases genéricas de almacenamiento de datos necesitan assignemtn y copy-c'tor. Y para marcos de objetos de negocios de más alto nivel, debería haber otras técnicas para copiar que también manejen cosas como "no copiar campos únicos" y "agregar a otro padre", etc. Así que usa el marco de alto nivel para copiar sus objetos de negocio y no permitir las asignaciones de bajo nivel.
mmmmmmmm
2
+1: Algunos puntos de acuerdo: los miembros de referencia que impiden que se defina un operador de asignación no es un argumento importante. Como usted indica correctamente una forma diferente, no todos los objetos tienen semántica de valor. También estoy de acuerdo en que no queremos 'if (p)' disperso por todo el código sin ninguna razón lógica. Pero el enfoque correcto para eso es a través de invariantes de clase: una clase bien definida no dejará lugar a dudas sobre si los miembros pueden ser nulos. Si algún puntero puede ser nulo en el código, espero que esté bien comentado.
James Hopkin
@JamesHopkin Estoy de acuerdo en que las clases bien diseñadas pueden usar punteros de manera efectiva. Además de protegerse contra NULL, las referencias también garantizan que su referencia se inicializó (no es necesario inicializar un puntero, por lo que podría apuntar a cualquier otra cosa). Las referencias también le informan sobre la propiedad, es decir, que el objeto no es propietario del miembro. Si usa constantemente referencias para miembros agregados, tendrá un alto grado de confianza en que los miembros punteros son objetos compuestos (aunque no hay muchos casos en los que un miembro compuesto esté representado por un puntero).
weberc2
11

Utilice referencias cuando pueda y punteros cuando sea necesario.

ebo
fuente
77
Había leído eso pero pensé que se trataba de pasar parámetros, no de datos de miembros. "Las referencias suelen aparecer en la piel de un objeto y los punteros en el interior".
markh44
Sí, no creo que este sea un buen consejo en el contexto de las referencias de los miembros.
Tim Angus
6

En algunos casos importantes, la asignabilidad simplemente no es necesaria. Estos son a menudo envoltorios de algoritmos livianos que facilitan el cálculo sin salir del alcance. Dichos objetos son candidatos principales para miembros de referencia, ya que puede estar seguro de que siempre tienen una referencia válida y nunca necesitan ser copiados.

En tales casos, asegúrese de que el operador de asignación (y, a menudo, también el constructor de la copia) no se pueda usar (heredando boost::noncopyableo declarándolos privados).

Sin embargo, como los usuarios ya comentaron, lo mismo no es cierto para la mayoría de los otros objetos. Aquí, el uso de miembros de referencia puede ser un gran problema y, por lo general, debe evitarse.

Konrad Rudolph
fuente
6

Como todo el mundo parece estar entregando reglas generales, ofreceré dos:

  • Nunca, nunca use referencias de uso como miembros de la clase. Nunca lo hice en mi propio código (excepto para demostrarme a mí mismo que estaba en lo cierto en esta regla) y no puedo imaginar un caso en el que lo haría. La semántica es demasiado confusa, y realmente no es para lo que se diseñaron las referencias.

  • Siempre, siempre, use referencias cuando pase parámetros a funciones, excepto los tipos básicos, o cuando el algoritmo requiera una copia.

Estas reglas son simples y me han servido de mucho. Dejo hacer reglas sobre el uso de punteros inteligentes (pero, por favor, no auto_ptr) como miembros de la clase para otros.


fuente
2
No estoy totalmente de acuerdo con el primero, pero el desacuerdo no es un problema para valorar la justificación. +1
David Rodríguez - dribeas
(Parece que estamos perdiendo este argumento con los buenos votantes de SO, sin embargo)
James Hopkin
2
Voté en contra porque "Nunca, nunca use referencias como miembros de la clase" y la siguiente justificación no es correcta para mí. Como se explica en mi respuesta (y comentar la respuesta superior) y según mi propia experiencia, hay casos en los que es simplemente algo bueno. Pensé como tú hasta que me contrataron en una empresa donde lo usaban de buena manera y me dejó claro que hay casos en los que ayuda. De hecho, creo que el verdadero problema es más acerca de la sintaxis y la implicación oculta que hace uso de referencias ya que los miembros requieren que el programador entienda lo que está haciendo.
Klaim
1
Neil> Estoy de acuerdo, pero no es la "opinión" lo que me hizo votar, es la afirmación de "nunca". Como discutir aquí no parece ser útil de todos modos, cancelaré mi voto negativo.
Klaim
2
Esta respuesta podría mejorarse explicando qué confunden las semánticas de los miembros de referencia. Este razonamiento es importante porque, a primera vista, los punteros parecen semánticamente más ambiguos (es posible que no se inicialicen y no le dicen nada sobre la propiedad del objeto subyacente).
weberc2
4

Sí a: ¿Es cierto decir que un objeto que contiene una referencia no debe ser asignable, ya que una referencia no debe cambiarse una vez inicializada?

Mis reglas generales para los miembros de datos:

  • nunca use una referencia, porque impide la asignación
  • si su clase es responsable de eliminar, use scoped_ptr de boost (que es más seguro que un auto_ptr)
  • de lo contrario, use un puntero o un puntero constante
pts
fuente
2
Ni siquiera puedo nombrar un caso en el que necesitaría una asignación del objeto comercial.
Mykola Golubyev
1
+1: Solo agregaría tener cuidado con los miembros de auto_ptr: cualquier clase que los tenga debe tener una construcción de asignación y copia definida explícitamente (es muy poco probable que las generadas sean lo que se requiere)
James Hopkin
auto_ptr también evita la asignación (sensible).
2
@ Mykola: También hice la experiencia de que el 98% de mis objetos comerciales no necesitan asignación o copiadores. Lo evito con una pequeña macro que agrego a los encabezados de esas clases, lo que hace que copy c'tors y operator = () sean privados sin implementación. Entonces, en el 98% de los objetos también uso referencias y es seguro.
mmmmmmmm
1
Las referencias como miembros se pueden utilizar para indicar explícitamente que la vida de la instancia de la clase depende directamente de la vida de las instancias de otras clases (o lo mismo). "Nunca use una referencia, porque impide la asignación" supone que todas las clases tienen que permitir la asignación. Es como si se asume que todas las clases tienen que permitir la operación de copia ...
Klaim
2

En general, solo usaría un puntero en los datos del miembro cuando el puntero podría ser nulo o podría cambiar. ¿Hay alguna otra razón para preferir punteros sobre referencias para miembros de datos?

Si. Legibilidad de su código. Un puntero hace que sea más obvio que el miembro es una referencia (irónicamente :)) y no un objeto contenido, porque cuando lo usa, debe desreferenciarlo. Sé que algunas personas piensan que está pasado de moda, pero sigo pensando que simplemente evita la confusión y los errores.

Dani van der Meer
fuente
1
Lakos también argumenta a favor de esto en "Diseño de software C ++ a gran escala".
Johan Kotlinski
Creo que Code Complete aboga por esto también y no estoy en contra. Sin embargo, no es demasiado convincente ...
markh44
2

Aconsejo a los miembros de datos de referencia que nunca sepan quién va a derivar de su clase y qué podrían querer hacer. Es posible que no quieran hacer uso del objeto referenciado, pero al ser una referencia, los ha obligado a proporcionar un objeto válido. Me he hecho esto lo suficiente como para dejar de usar miembros de datos de referencia.

StephenD
fuente
0

Si entiendo la pregunta correctamente ...

Referencia como parámetro de función en lugar de puntero: como señaló, un puntero no deja en claro quién posee la limpieza / inicialización del puntero. Prefiere un punto compartido cuando desea un puntero, es parte de C ++ 11 y un punto débil cuando la validez de los datos no está garantizada durante la vida útil de la clase que acepta los datos señalados .; también es parte de C ++ 11. El uso de una referencia como parámetro de función garantiza que la referencia no sea nula. Tienes que subvertir las características del lenguaje para evitar esto, y no nos importan los codificadores de cañones sueltos.

Referencia aa variable miembro: Como se indicó anteriormente con respecto a la validez de los datos. Esto garantiza que los datos señalados, luego referenciados, son válidos.

Mover la responsabilidad de la validez de la variable a un punto anterior en el código no solo limpia el código posterior (la clase A en su ejemplo), sino que también lo deja claro para la persona que lo usa.

En su ejemplo, que es un poco confuso (realmente trataría de encontrar una implementación más clara), la A utilizada por B está garantizada durante toda la vida de la B, ya que B es miembro de A, por lo que una referencia refuerza esto y es (quizás) más claro.

Y por si acaso (que es una campana de baja probabilidad, ya que no tendría ningún sentido en el contexto de sus códigos), otra alternativa, usar un parámetro A no referenciado, sin puntero, copiaría A y haría que el paradigma fuera inútil: I Realmente no creo que te refieres a esto como una alternativa.

Además, esto también garantiza que no puede cambiar los datos a los que se hace referencia, donde se puede modificar un puntero. Un puntero constante funcionaría solo si el puntero / referenciado a los datos no fuera mutable.

Los punteros son útiles si no se garantizó que el parámetro A para B se estableciera o si pudiera reasignarse. Y a veces un puntero débil es demasiado detallado para la implementación, y un buen número de personas no sabe qué es un débil_ptr o simplemente no les gusta.

Separe esta respuesta, por favor :)

Kit10
fuente