Informática
Puntero frente a referencia
Notas originales de SunDiaL · Ver fuente en inglés
%% Fuente: https://www.reddit.com/r/cpp/comments/18186it/comment/kab1in7/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button %% Creo que el problema es que estas citas mezclan en cierta medida la semántica con la implementación. Separemos ambas de forma más explícita; así quedará más claro por qué las citas no son realmente incompatibles ni contradictorias:
Semántica del puntero
Un puntero es un tipo de valor distinto. Tiene un tamaño definido en cada plataforma, que se obtiene mediante sizeof(void*). En una plataforma con un espacio de direcciones real de 64 bits, devuelve 8. Todos los punteros a cualquier tipo de objeto tienen el mismo tamaño: sizeof(T*) == sizeof(U*). El valor de un puntero es una ubicación real de memoria. Puedes inspeccionarlo directamente: si imprimes un puntero sin desreferenciarlo, el valor mostrado es la dirección de memoria que contiene.
En esencia, los punteros son tipos de valor «auténticos» por sí mismos y pueden existir sin tener un objeto al que apuntar. Por eso puede haber un puntero nulo. Y, como son tipos de valor auténticos, tienen su propia definición de las siguientes operaciones, independiente del tipo al que apuntan:
-
Comparar un puntero significa comparar la dirección de memoria, no el contenido de esa dirección.
-
Copiar o asignar un puntero significa copiar o asignar la dirección de memoria, no el contenido de esa dirección.
-
Obtener la dirección de un puntero produce la dirección del propio objeto puntero, no la del contenido apuntado.
-
«Desreferenciar» un puntero produce una referencia al contenido apuntado. Lo sé, es confuso. Tenía más sentido en C, donde no existía un elemento distinto llamado referencia; por eso se decía que los punteros tenían «semántica de referencia» y que «desreferenciarlos» producía el contenido real.
Cuando decimos que un tipo es «similar a un puntero», queremos decir que modela esas propiedades. Por eso los punteros inteligentes se comportan como lo hacen. También por eso std::span resulta tan confuso. std::span parece un contenedor, así que esperas que se comporte como una referencia a un contenedor: compararlo debería comparar los elementos del span. Pero puedes reasignar spans, y esa asignación no reasigna los elementos subyacentes, sino el puntero subyacente para que apunte a otros elementos. Para mantener la coherencia, el estándar eligió el menor de dos males e hizo que span modelara un puntero, con semántica de puntero en todos los casos. Al comparar un span preguntas «¿ofrecen estos spans una vista del mismo contenido exacto de memoria?», como al comparar un puntero. En C++20 esta idea de «API de referencia a un contenedor, pero semántica de puntero» se formalizó con el concepto view.
Semántica de la referencia
Una referencia no es realmente un tipo distinto, como afirma la segunda cita: no existe un objeto real que «represente» una referencia. No puedes obtener la dirección de la referencia en sí. Si utilizas sizeof(T&), obtendrás el mismo resultado que con sizeof(T). Por tanto, lo más preciso es decir que una referencia es un alias de la instancia real de un objeto.
Como las referencias no son tipos de valor auténticos por sí mismas, sino alias de la instancia, tienen la siguiente semántica:
-
Deben «vincularse» a una instancia durante la inicialización. Una referencia nunca puede ser «nula».
-
No pueden reasignarse. Asignar a una referencia después de vincularla a una instancia equivale a asignar a la instancia vinculada, no a la referencia.
-
Comparar referencias equivale a comparar las instancias referenciadas.
-
Obtener la dirección de una referencia devuelve la dirección de la instancia vinculada.
Implementaciones
Como hemos visto, un puntero es un tipo de valor definido explícitamente. Ocupa memoria en la pila o el montículo.
Una referencia, en cambio, no es un tipo de valor definido explícitamente. Como no es un tipo de valor «real», sino un alias de la instancia vinculada, el compilador puede implementar las referencias como quiera, siempre que mantenga su semántica.
Esto significa que el compilador puede eliminar completamente una referencia mediante optimización. No puede hacer lo mismo de forma inherente con un puntero: este tiene que existir porque puedes obtener su dirección, reasignarlo, etc. Sin embargo, el compilador también puede implementar una referencia como un puntero en el sentido de la máquina, explicado más abajo, y a menudo lo hace cuando la referencia pasa entre funciones que no se pueden expandir en línea.
¿Por qué resulta tan confuso?
Por dos razones:
Confundir un puntero con la referencia indirecta
Cuando se habla de punteros, a menudo se usa el término indistintamente con la noción de dirección de memoria y referencia indirecta de la máquina, porque eso es lo que modela un puntero sin procesar. Pero son dos cosas distintas. El tipo puntero de C/C++ es una noción abstracta de dirección de memoria que se puede utilizar para referenciar indirectamente. Tiene significado independientemente de cómo implemente esa referencia indirecta una plataforma concreta; en ensamblador no existe un tipo «puntero», sino instrucciones que realizan operaciones indirectamente a través de una dirección de memoria. Así que, cuando alguien dice que «una referencia y un puntero son lo mismo», está pensando en el código ensamblador generado para la referencia indirecta y en cómo puede almacenar las direcciones utilizadas.
La expresión coloquial «semántica de referencia»
Es habitual decir coloquialmente que tanto los punteros como las referencias tienen «semántica de referencia». Con ello se quiere decir que el nombre de una variable representa indirectamente una instancia, sin ser un tipo de valor de esa instancia. Permite manipular la instancia a través del nombre de la variable y transmitirla entre funciones sin copiarla. Sin embargo, la expresión es imprecisa. Un puntero no tiene realmente semántica de referencia: es un tipo de valor. Ese tipo de valor modela la semántica de referencia mediante los operadores * y ->.
Esta distinción aparece a menudo al preguntar «¿Python/Java tiene semántica de referencia o de valor?». Es una pregunta trampa: la respuesta correcta es «tienen semántica de valor, pero el único tipo de valor que puedes asignar directamente a un nombre es un puntero». La ausencia de una referencia auténtica o de un tipo de valor asignable distinto de un puntero es clave para entender por qué Java y Python se comportan como lo hacen.