¿Por qué las referencias no se vuelven a ubicar en C ++?

Las referencias de C ++ tienen dos propiedades:

  • Siempre apuntan al mismo objeto.
  • No pueden ser 0.

Los indicadores son todo lo contrario:

  • Pueden señalar diferentes objetos.
  • Pueden ser 0.

¿Por qué no hay “referencia o puntero repetible que no se pueda nulos” en C ++? No puedo pensar en una buena razón por la cual las referencias no deberían ser rescatables.

Editar: La pregunta surge a menudo porque generalmente uso referencias cuando quiero asegurarme de que una “asociación” (evite las palabras “referencia” o “puntero” aquí) nunca sea inválida.

No creo haber pensado nunca “genial que esta referencia siempre se refiera al mismo objeto”. Si las referencias fueran resecables, todavía se podría obtener el comportamiento actual de esta manera:

int i = 3; int& const j = i; 

Esto ya es legal C ++, pero sin sentido.

Repito mi pregunta de esta manera: “¿Cuál fue la razón detrás del diseño ‘una referencia es el objeto’? ¿Por qué se consideró útil tener referencias siempre ser el mismo objeto, en lugar de solo cuando se declara como const?”

Saludos, Félix

La razón por la que C ++ no le permite volver a enlazar referencias se da en “Diseño y evolución de C ++” de Stroustrup:

No es posible cambiar lo que se refiere a una referencia después de la inicialización. Es decir, una vez que se inicializa una referencia de C ++, no se puede hacer que se refiera posteriormente a un objeto diferente; no puede ser reintegrado. En el pasado, había sido mordido por las referencias de Algol68, donde r1=r2 puede asignar a través de r1 al objeto referido o asignar un nuevo valor de referencia a r1 (volver a vincular r1 ) dependiendo del tipo de r2 . Quería evitar tales problemas en C ++.

En C ++, a menudo se dice que “la referencia es el objeto”. En cierto sentido, es cierto: aunque las referencias se manejan como punteros cuando se comstack el código fuente, la referencia pretende significar un objeto que no se copia cuando se llama a una función. Como las referencias no son directamente direccionables (por ejemplo, las referencias no tienen dirección, y devuelve la dirección del objeto), no tendría sentido semánticamente reasignarlas. Además, C ++ ya tiene punteros, que maneja la semántica de la reposición.

Porque entonces no tendrías ningún tipo resecable que no puede ser 0. A menos que incluyas 3 tipos de referencias / punteros. Lo cual complicaría el lenguaje con muy poca ganancia (¿Y por qué no agregar también el 4to tipo? ¿Referencia no repetible que puede ser 0?)

Una mejor pregunta puede ser, ¿por qué querrías que las referencias fueran resellables? Si lo fueran, eso los haría menos útiles en muchas situaciones. Sería más difícil para el comstackdor hacer un análisis de alias.

Parece que las principales razones por las que las referencias en Java o C # son resellables se deben a que hacen el trabajo de los punteros. Señalan a los objetos. No son alias para un objeto.

¿Cuál debería ser el efecto de lo siguiente?

 int i = 42; int& j = i; j = 43; 

En C ++ hoy en día, con referencias no rescatables, es simple. j es un alias para i, y termina con el valor 43.

Si las referencias hubieran sido resecables, entonces la tercera línea vincularía la referencia j a un valor diferente. Ya no alias i, sino el entero literal 43 (que no es válido, por supuesto). O tal vez un ejemplo más simple (o al menos sintácticamente válido):

 int i = 42; int k = 43; int& j = i; j = k; 

Con referencias resecables. j señalaría k después de evaluar este código. Con las referencias no rescatables de C ++, j aún apunta a i, y se le asigna el valor 43.

Hacer referencias reseables cambia la semántica del lenguaje. La referencia ya no puede ser un alias para otra variable. En cambio, se convierte en un tipo de valor separado, con su propio operador de asignación. Y entonces uno de los usos más comunes de referencias sería imposible. Y nada se ganaría a cambio. La funcionalidad recién adquirida para referencias ya existía en forma de punteros. Así que ahora tendríamos dos formas de hacer lo mismo y no hay forma de hacer lo que hacen las referencias en el lenguaje C ++ actual.

Una referencia no es un puntero, puede implementarse como un puntero en el fondo, pero su concepto central no es equivalente a un puntero. Se debe considerar una referencia como *is* el objeto al que se refiere. Por lo tanto, no puede cambiarlo y no puede ser NULO.

Un puntero es simplemente una variable que contiene una dirección de memoria. El puntero en sí tiene una dirección de memoria propia, y dentro de esa dirección de memoria tiene otra dirección de memoria a la que se dice que apunta. Una referencia no es la misma, no tiene una dirección propia y, por lo tanto, no se puede cambiar para “mantener” otra dirección.

Creo que el parashift C ++ FAQ sobre referencias lo dice mejor:

Nota importante: aunque a menudo se implementa una referencia usando una dirección en el lenguaje ensamblador subyacente, no piense en una referencia como un puntero de aspecto gracioso a un objeto. Una referencia es el objeto. No es un puntero al objeto, ni una copia del objeto. Es el objeto

y nuevamente en la Pregunta frecuente 8.5 :

A diferencia de un puntero, una vez que una referencia está vinculada a un objeto, no puede “volverse a unir” a otro objeto. La referencia en sí misma no es un objeto (no tiene identidad, tomando la dirección de una referencia te da la dirección del referente, recuerda: la referencia es su referente).

Una referencia resetable sería funcionalmente idéntica a un puntero.

Con respecto a la nulabilidad: no se puede garantizar que dicha “referencia reencadenable” sea no NULL en el momento de la comstackción, por lo que cualquier prueba de este tipo debería tener lugar en el tiempo de ejecución. Puede lograr esto usted mismo escribiendo una plantilla de clase de estilo de puntero inteligente que arroje una excepción cuando se inicialice o se le asigne NULL:

 struct null_pointer_exception { ... }; template struct non_null_pointer { // No default ctor as it could only sensibly produce a NULL pointer non_null_pointer(T* p) : _p(p) { die_if_null(); } non_null_pointer(non_null_pointer const& nnp) : _p(nnp._p) {} non_null_pointer& operator=(T* p) { _p = p; die_if_null(); } non_null_pointer& operator=(non_null_pointer const& nnp) { _p = nnp._p; } T& operator*() { return *_p; } T const& operator*() const { return *_p; } T* operator->() { return _p; } // Allow implicit conversion to T* for convenience operator T*() const { return _p; } // You also need to implement operators for +, -, +=, -=, ++, -- private: T* _p; void die_if_null() const { if (!_p) { throw null_pointer_exception(); } } }; 

Esto podría ser útil en ocasiones: una función que toma un non_null_pointer ciertamente comunica más información a la persona que llama que una función que toma int* .

¿Probablemente habría sido menos confuso nombrar “alias” de referencias de C ++? Como otros han mencionado, las referencias en C ++ deben considerarse como la variable a la que se refieren, no como un puntero / referencia a la variable. Como tal, no puedo pensar en una buena razón por la que deberían ser reiniciables.

cuando se trata de punteros, a menudo tiene sentido permitir nulo como un valor (y de lo contrario, es probable que desee una referencia en su lugar). Si desea específicamente no permitir el almacenamiento nulo, siempre puede codificar su propio tipo de puntero inteligente;)

De manera interesante, muchas respuestas aquí son un poco confusas o incluso al margen del punto (por ejemplo, no es porque las referencias no puedan ser cero o similares, de hecho, puedes construir fácilmente un ejemplo donde una referencia es cero).

La verdadera razón por la cual no es posible volver a establecer una referencia es bastante simple.

  • Los punteros le permiten hacer dos cosas: para cambiar el valor detrás del puntero (ya sea a través del operador -> o * ), y para cambiar el puntero en sí mismo (asignación directa = ). Ejemplo:

     int a; int * p = &a; 
    1. Cambiar el valor requiere desreferenciación: *p = 42;
    2. Cambiar el puntero: p = 0;
  • Las referencias le permiten cambiar solo el valor. ¿Por qué? Como no existe otra syntax para express el reajuste. Ejemplo:

     int a = 10; int b = 20; int & r = a; r = b; // re-set r to b, or set a to 20? 

En otras palabras, sería ambiguo si se le permitiera volver a establecer una referencia. Tiene aún más sentido al pasar por referencia:

 void foo(int & r) { int b = 20; r = b; // re-set r to a? or set a to 20? } void main() { int a = 10; foo(a); } 

Espero que ayude 🙂

Las referencias de C ++ a veces pueden forzarse a ser 0 con algunos comstackdores (es una mala idea hacerlo *, y viola el estándar *).

 int &x = *((int*)0); // Illegal but some compilers accept it 

EDITAR: según varias personas que conocen el estándar mucho mejor que yo, el código anterior produce un “comportamiento indefinido”. En al menos algunas versiones de GCC y Visual Studio, he visto esto hacer lo esperado: el equivalente de establecer un puntero a NULL (y provoca una excepción de puntero NULL cuando se accede).

No puedes hacer esto:

 int theInt = 0; int& refToTheInt = theInt; int otherInt = 42; refToTheInt = otherInt; 

… por la misma razón por la que secondInt y firstInt no tienen el mismo valor aquí:

 int firstInt = 1; int secondInt = 2; secondInt = firstInt; firstInt = 3; assert( firstInt != secondInt ); 

Esto no es en realidad una respuesta, sino una solución para esta limitación.

Básicamente, cuando intenta “volver a vincular” una referencia, en realidad está tratando de usar el mismo nombre para referirse a un nuevo valor en el siguiente contexto. En C ++, esto se puede lograr mediante la introducción de un scope de bloque.

En el ejemplo de jalf

 int i = 42; int k = 43; int& j = i; //change i, or change j? j = k; 

si quiere cambiar i, escríbalo como se indica arriba. Sin embargo, si quiere cambiar el significado de j para significar k , puede hacer esto:

 int i = 42; int k = 43; int& j = i; //change i, or change j? //change j! { int& j = k; //do what ever with j's new meaning } 

Me imagino que está relacionado con la optimización.

La optimización estática es mucho más fácil cuando se puede saber inequívocamente qué bit de memoria significa una variable. Los punteros rompen esta condición y la referencia reutilizable también lo haría.

Porque a veces las cosas no deberían ser re-imprimibles. (Por ejemplo, la referencia a Singleton).

Porque es genial en una función saber que tu argumento no puede ser nulo.

Pero sobre todo, porque permite usar algo que realmente es un puntero, pero que actúa como un objeto de valor local. C ++ se esfuerza mucho, para citar a Stroustrup, por hacer que las instancias de la clase “hagan lo que se dice ints d”. Pasar un int por vaue es barato, porque un int se ajusta a un registro de máquina. Las clases a menudo son más grandes que las de los ints, y pasarlas por valor tiene una sobrecarga significativa.

Ser capaz de pasar un puntero (que suele ser del tamaño de una int, o tal vez dos) que se “parece” a un objeto de valor nos permite escribir un código más limpio, sin el “detalle de implementación” de las desreferencias. Y, junto con la sobrecarga del operador, nos permite escribir clases con una syntax similar a la syntax utilizada con ints. En particular, nos permite escribir clases de plantillas con syntax que se pueden aplicar igualmente a primitivas, como ints y clases (como una clase de números Complejos).

Y, especialmente con la sobrecarga del operador, hay lugares en los que deberíamos devolver un objeto, pero una vez más, es mucho más económico devolver un puntero. Una vez más, devolver una referencia es nuestro “out”.

Y los indicadores son difíciles. No es para ti, tal vez, y no para nadie que se da cuenta de que un puntero es solo el valor de una dirección de memoria. Pero al recordar mi clase CS 101, tropezaron con varios estudiantes.

 char* p = s; *p = *s; *p++ = *s++; i = ++*p; 

puede ser confuso

Diablos, después de 40 años de C, la gente todavía no puede siquiera estar de acuerdo si una statement de puntero debería ser:

 char* p; 

o

 char *p; 

Siempre me pregunté por qué no hicieron un operador de asignación de referencia (por ejemplo: =) para esto.

Para poner nerviosos a alguien, escribí un código para cambiar el objective de una referencia en una estructura.

No, no recomiendo repetir mi truco. Se romperá si se transporta a una architecture suficientemente diferente.

Ser medio serio: en mi humilde opinión para hacerlos un poco más diferentes de los punteros;) Usted sabe que puede escribir:

 MyClass & c = *new MyClass(); 

Si también pudiera escribir más tarde:

 c = *new MyClass("other") 

¿Tendría sentido tener alguna referencia junto con los punteros?

 MyClass * a = new MyClass(); MyClass & b = *new MyClass(); a = new MyClass("other"); b = *new MyClass("another"); 

El hecho de que las referencias en C ++ no sean nulables es un efecto secundario de que sean solo un alias.

Estoy de acuerdo con la respuesta aceptada. Pero para la constness, se comportan como punteros.

 struct A{ int y; int& x; A():y(0),x(y){} }; int main(){ A a; const A& ar=a; ar.x++; } 

trabajos. Ver

Motivos de diseño para el comportamiento de los miembros de referencia de las clases aprobadas por const reference

Hay una solución si desea una variable miembro que sea una referencia y desea poder volver a vincularla. Si bien considero que es útil y confiable, tenga en cuenta que utiliza algunas suposiciones (muy débiles) en el diseño de la memoria. Depende de usted decidir si está dentro de sus estándares de encoding.

 #include  struct Field_a_t { int& a_; Field_a_t(int& a) : a_(a) {} Field_a_t& operator=(int& a) { // a_.~int(); // do this if you have a non-trivial destructor new(this)Field_a_t(a); } }; struct MyType : Field_a_t { char c_; MyType(int& a, char c) : Field_a_t(a) , c_(c) {} }; int main() { int i = 1; int j = 2; MyType x(i, 'x'); std::cout < < x.a_; x.a_ = 3; std::cout << i; ((Field_a_t&)x) = j; std::cout << x.a_; x.a_ = 4; std::cout << j; } 

Esto no es muy eficiente, ya que necesita un tipo separado para cada campo de referencia reasignable y hacer que sean clases base; Además, existe una suposición débil aquí de que una clase que tiene un único tipo de referencia no tendrá un campo __vfptr o cualquier otro type_id relacionado con type_id que podría potencialmente destruir los enlaces de tiempo de ejecución de MyType. Todos los comstackdores que conozco satisfacen esa condición (y tendría poco sentido no hacerlo).