¿Por qué las referencias no son "const" en C ++?

86

Sabemos que una "variable constante" indica que una vez asignada, no se puede cambiar la variable, así:

int const i = 1;
i = 2;

El programa anterior no se podrá compilar; gcc muestra un error:

assignment of read-only variable 'i'

No hay problema, puedo entenderlo, pero el siguiente ejemplo está más allá de mi comprensión:

#include<iostream>
using namespace std;
int main()
{
    boolalpha(cout);
    int const i = 1;
    cout << is_const<decltype(i)>::value << endl;
    int const &ri = i;
    cout << is_const<decltype(ri)>::value << endl;
    return 0;
}

Se produce

true
false

Extraño. Sabemos que una vez que una referencia está vinculada a un nombre / variable, no podemos cambiar esta vinculación, cambiamos su objeto vinculado. Así que supongo que el tipo de ridebería ser el mismo que i: ¿cuándo ies un int const, por qué rino const?

Troskyvs
fuente
3
Además, boolalpha(cout)es muy inusual. Podría hacerlo en su std::cout << boolalphalugar.
isanae
6
Tanto por riser un "alias" indistinguible de i.
Kaz
1
Esto se debe a que una referencia siempre está delimitada por el mismo objeto. itambién es una referencia pero por razones históricas no lo declaras como tal de forma explícita. Por tanto, ies referencia que se refiere a un almacenamiento y ries una referencia que se refiere al mismo almacenamiento. Pero no hay diferencia en la naturaleza entre i y ri. Como no puede cambiar la vinculación de una referencia, no es necesario calificarla como const. Y permítanme afirmar que el comentario de @Kaz es mucho mejor que la respuesta validada (nunca explique las referencias usando punteros, una referencia es un nombre, un ptr es una variable).
Jean-Baptiste Yunès
1
Fantástica pregunta. Antes de ver este ejemplo, habría esperado is_constvolver también trueen este caso. En mi opinión, este es un buen ejemplo de por qué constes fundamentalmente al revés; un atributo "mutable" (a la Rust mut) parece que sería más consistente.
Kyle Strand
1
Quizás el título debería cambiarse por "¿por qué es is_const<int const &>::valuefalso?" o similar; Estoy luchando por ver algún significado a la pregunta que no sea sobre el comportamiento de los rasgos de tipo
MM

Respuestas:

52

Esto puede parecer contrario a la intuición, pero creo que la forma de entenderlo es darse cuenta de que, en ciertos aspectos, las referencias se tratan sintácticamente como punteros .

Esto parece lógico para un puntero :

int main()
{
    boolalpha(cout);

    int const i = 1;
    cout << is_const<decltype(i)>::value << endl;

    int const* ri = &i;
    cout << is_const<decltype(ri)>::value << endl;
}

Salida:

true
false

Esto es lógico porque sabemos que no es el objeto puntero el que es constante (se puede hacer que apunte a otra parte), es el objeto al que se apunta.

Entonces vemos correctamente que la constness del puntero se devuelve como false.

Si queremos hacer el propio punteroconst tenemos que decir:

int main()
{
    boolalpha(cout);

    int const i = 1;
    cout << is_const<decltype(i)>::value << endl;

    int const* const ri = &i;
    cout << is_const<decltype(ri)>::value << endl;
}

Salida:

true
true

Y entonces creo que vemos una analogía sintáctica con la referencia .

Sin embargo, las referencias son semánticamente diferentes a los punteros, especialmente en un aspecto crucial, no se nos permite volver a vincular una referencia a otro objeto una vez vinculado.

Entonces, aunque las referencias comparten la misma sintaxis que los punteros, las reglas son diferentes y, por lo tanto, el lenguaje nos impide declarar la referencia en sí de constesta manera:

int main()
{
    boolalpha(cout);

    int const i = 1;
    cout << is_const<decltype(i)>::value << endl;

    int const& const ri = i; // COMPILE TIME ERROR!
    cout << is_const<decltype(ri)>::value << endl;
}

Supongo que no se nos permite hacer esto porque no parece ser necesario cuando las reglas del lenguaje impiden que la referencia rebote de la misma manera que lo haría un puntero (si no se declara const).

Entonces, para responder a la pregunta:

P) ¿Por qué "referencia" no es una "constante" en C ++?

En su ejemplo, la sintaxis hace que se haga referencia a la cosa de constla misma manera que lo haría si estuviera declarando un puntero .

Correcta o incorrectamente, no podemos hacer la referencia en sí, constpero si lo hiciéramos, se vería así:

int const& const ri = i; // not allowed

P) sabemos que una vez que una referencia se vincula a un nombre / variable, no podemos cambiar esta vinculación, cambiamos su objeto vinculado. Así que supongo que el tipo de ridebería ser el mismo que i: ¿cuándo ies a int const, por qué rino const?

¿Por qué decltype()no se transfiere al objeto al que está vinculada la referencia ?

Supongo que esto es para la equivalencia semántica con punteros y tal vez también la función de decltype()(tipo declarado) es mirar hacia atrás en lo que se declaró antes de que tuviera lugar el enlace.

Galik
fuente
13
¿Parece que estás diciendo "las referencias no pueden ser constantes porque siempre son constantes"?
ruakh
2
Puede ser cierto que " semánticamente todas las acciones sobre ellos después de que están vinculados se transfieren al objeto al que se refieren", pero el OP esperaba que eso también se aplicara decltypey descubrió que no es así.
ruakh
10
Hay muchas suposiciones serpenteantes y analogías dudosamente aplicables a los punteros aquí, lo que creo que confunde el problema más de lo necesario, cuando la verificación del estándar como lo hizo sonyuanyao va directo al punto real: una referencia no es un tipo calificable de CV, por lo tanto no puede ser const, por std::is_constlo tanto debe regresar false. En su lugar, podrían haber usado una redacción que significara que debe regresar true, pero no lo hicieron. ¡Eso es! Todo esto sobre punteros, "supongo", "supongo", etc., no proporciona ninguna aclaración real.
underscore_d
1
@underscore_d Esta es probablemente una mejor pregunta para el chat que para la sección de comentarios aquí, pero ¿tiene un ejemplo de "confusión innecesaria" de esta forma de pensar sobre las referencias? Si se pueden implementar de esa manera, ¿cuál es el problema de pensar en ellos de esa manera? (No creo que esta pregunta cuente porque decltypeno es una operación en tiempo de ejecución, por lo que la idea de "en tiempo de ejecución, las referencias se comportan como punteros", sea correcta o no, no se aplica realmente.
Kyle Strand
1
Las referencias tampoco se tratan sintácticamente como punteros. Tienen una sintaxis diferente para declarar y usar. Así que ni siquiera estoy seguro de a qué te refieres.
Jordan Melo
55

¿Por qué "ri" no es "const"?

std::is_const comprueba si el tipo está calificado const o no.

Si T es un tipo calificado const (es decir, const o const volátil), proporciona el valor de la constante del miembro igual a verdadero. Para cualquier otro tipo, el valor es falso.

Pero la referencia no puede calificarse const. Referencias [dcl.ref] / 1

Las referencias calificadas por CV están mal formadas, excepto cuando los calificadores-CV se introducen mediante el uso de un nombre-typedef ([dcl.typedef], [temp.param]) o un especificador-decltype ([dcl.type.simple]) , en cuyo caso se ignoran los calificadores cv.

Por is_const<decltype(ri)>::valuelo tanto , volverá falseporque ri(la referencia) no es un tipo calificado const. Como dijiste, no podemos volver a vincular una referencia después de la inicialización, lo que implica que la referencia siempre es "const", por otro lado, la referencia const-calificada o la referencia const-no calificada podrían no tener sentido en realidad.

Songyuanyao
fuente
5
Directo al estándar y, por lo tanto, directo al grano, en lugar de toda la suposición vacilante de la respuesta aceptada.
underscore_d
5
@underscore_d Esta es una buena respuesta hasta que reconozca que el operador también preguntaba por qué. "Porque lo dice" no es realmente útil para ese aspecto de la pregunta.
usuario simple
5
@ user9999999 La otra respuesta tampoco responde a ese aspecto. Si una referencia no puede recuperarse, ¿por qué no is_constregresa true? Esa respuesta intenta establecer una analogía de cómo los punteros se pueden volver a cerrar opcionalmente, mientras que las referencias no lo son, y al hacerlo, conduce a la autocontradicción por la misma razón. No estoy seguro de que haya una explicación real de cualquier manera, aparte de una decisión algo arbitraria de quienes escriben el Estándar, y a veces eso es lo mejor que podemos esperar. De ahí esta respuesta.
underscore_d
2
Otro aspecto importante, creo, es que nodecltype es una función y, por lo tanto, trabaja directamente en la referencia en sí y no en el objeto referido. (Esto es quizás más relevante para la respuesta de "las referencias son básicamente indicadores", pero sigo pensando que es parte de lo que hace que este ejemplo sea confuso y, por lo tanto, vale la pena mencionarlo aquí).
Kyle Strand
3
Para aclarar aún más el comentario de @KyleStrand: decltype(name)actúa de manera diferente a un general decltype(expr). Por ejemplo, decltype(i)es el tipo declarado de iwhich is const int, while decltype((i))would be int const &.
Daniel Schepler
18

Necesita usar std::remove_referencepara obtener el valor que está buscando.

std::cout << std::is_const<std::remove_reference<decltype(ri)>::type>::value << std::endl;

Para obtener más información, consulte esta publicación .

chema989
fuente
7

¿Por qué no lo son las macros const? Funciones Literales ¿Los nombres de los tipos?

const las cosas son solo un subconjunto de cosas inmutables.

Dado que los tipos de referencia son solo eso, tipos, puede haber tenido sentido requerir el constcalificador-en todos ellos para la simetría con otros tipos (particularmente con tipos de puntero), pero esto se volvería muy tedioso muy rápidamente.

Si C ++ tuviera objetos inmutables por defecto, requiriendo la mutablepalabra clave en cualquier cosa que no quisiera const, entonces esto hubiera sido fácil: simplemente no permita que los programadores agreguen mutabletipos de referencia.

Tal como están las cosas, son inmutables sin calificación.

Y, dado que no están constcalificados, probablemente sería más confuso que is_constun tipo de referencia arrojara verdadero.

Encuentro que esto es un compromiso razonable, especialmente porque la inmutabilidad es impuesta de todos modos por el mero hecho de que no existe sintaxis para mutar una referencia.

Carreras de ligereza en órbita
fuente
5

Esta es una peculiaridad / característica en C ++. Aunque no pensamos en las referencias como tipos, de hecho se "sientan" en el sistema de tipos. Aunque esto parece incómodo (dado que cuando se utilizan referencias, la semántica de referencia se produce automáticamente y la referencia "se sale del camino"), existen algunas razones defendibles por las que las referencias se modelan en el sistema de tipos en lugar de como un atributo separado fuera de tipo.

En primer lugar, consideremos que no todos los atributos de un nombre declarado deben estar en el sistema de tipos. Del lenguaje C, tenemos "clase de almacenamiento" y "vinculación". Se puede introducir un nombre como extern const int ri, donde externindica la clase de almacenamiento estático y la presencia de enlace. El tipo es justo const int.

C ++ obviamente abraza la noción de que las expresiones tienen atributos que están fuera del sistema de tipos. El lenguaje ahora tiene un concepto de "clase de valor" que es un intento de organizar el número creciente de atributos no tipográficos que puede exhibir una expresión.

Sin embargo, las referencias son tipos. ¿Por qué?

Se solía explicar en los tutoriales de C ++ que una declaración como const int &riintroducida ritiene tipo const int, pero semántica de referencia. Esa semántica de referencia no era un tipo; era simplemente una especie de atributo que indicaba una relación inusual entre el nombre y la ubicación de almacenamiento. Además, el hecho de que las referencias no son tipos se utilizó para racionalizar por qué no se pueden construir tipos basados ​​en referencias, aunque la sintaxis de construcción de tipos lo permita. Por ejemplo, matrices o punteros a referencias que no son posibles: const int &ari[5]y const int &*pri.

Pero, de hecho, las referencias son tipos y, por lo tanto, decltype(ri)recupera algún nodo de tipo de referencia que no está calificado. Debe descender más allá de este nodo en el árbol de tipos para llegar al tipo subyacente con remove_reference.

Cuando se utiliza ri, la referencia se resuelve de forma transparente, de modo que ri"se ve y se siente como i" y se le puede llamar un "alias". En el sistema de tipos, sin embargo, ride hecho hay un tipo que es " referencia a const int ".

¿Por qué son tipos de referencias?

Tenga en cuenta que si las referencias no fueran tipos, se consideraría que estas funciones tienen el mismo tipo:

void foo(int);
void foo(int &);

Eso simplemente no puede ser por razones que son bastante evidentes. Si tuvieran el mismo tipo, eso significa que cualquier declaración sería adecuada para cualquiera de las definiciones, por (int)lo que se debería sospechar que cada función toma una referencia.

Del mismo modo, si las referencias no fueran tipos, estas dos declaraciones de clase serían equivalentes:

class foo {
  int m;
};

class foo {
  int &m;
};

Sería correcto que una unidad de traducción usara una declaración, y otra unidad de traducción en el mismo programa usara la otra declaración.

El hecho es que una referencia implica una diferencia en la implementación y es imposible separarla del tipo, porque el tipo en C ++ tiene que ver con la implementación de una entidad: su "diseño" en bits por así decirlo. Si dos funciones tienen el mismo tipo, se pueden invocar con las mismas convenciones de llamadas binarias: la ABI es la misma. Si dos estructuras o clases tienen el mismo tipo, su diseño es el mismo, así como la semántica de acceso a todos los miembros. La presencia de referencias cambia estos aspectos de los tipos, por lo que es una decisión de diseño sencilla incorporarlos al sistema de tipos. (Sin embargo, tenga en cuenta un contraargumento aquí: un miembro de estructura / clase puede ser static, lo que también cambia la representación; ¡pero ese no es el tipo!)

Por lo tanto, las referencias están en el sistema de tipos como "ciudadanos de segunda clase" (no a diferencia de las funciones y matrices en ISO C). Hay ciertas cosas que no podemos "hacer" con referencias, como declarar punteros a referencias o matrices de ellas. Pero eso no significa que no sean tipos. Simplemente no son tipos de una manera que tenga sentido.

No todas estas restricciones de segunda clase son esenciales. Dado que hay estructuras de referencias, ¡podría haber matrices de referencias! P.ej

// fantasy syntax
int x = 0, y = 0;
int &ar[2] = { x, y };

// ar[0] is now an alias for x: could be useful!

Esto simplemente no está implementado en C ++, eso es todo. Sin embargo, los punteros a referencias no tienen ningún sentido, porque un puntero levantado de una referencia simplemente va al objeto referenciado. La razón probable por la que no hay matrices de referencias es que la gente de C ++ considera que las matrices son una especie de característica de bajo nivel heredada de C que está rota de muchas formas que son irreparables, y no quieren tocar las matrices como el base para cualquier cosa nueva. Sin embargo, la existencia de matrices de referencias sería un claro ejemplo de cómo las referencias tienen que ser tipos.

constTipos no calificables: ¡también se encuentran en ISO C90!

Algunas respuestas apuntan al hecho de que las referencias no requieren un constcalificativo. Eso es más bien una pista falsa, porque la declaración const int &ri = ini siquiera intenta hacer una constreferencia calificada: es una referencia a un tipo calificado const (que en sí mismo no lo es const). Al igual que const in *rideclara un puntero a algo const, pero ese puntero no lo es en sí mismo const.

Dicho esto, es cierto que las referencias no pueden llevar el constcalificador en sí mismas.

Sin embargo, esto no es tan extraño. Incluso en el lenguaje ISO C 90, no todos los tipos pueden serlo const. Es decir, las matrices no pueden serlo.

En primer lugar, la sintaxis no existe para declarar una matriz const: int a const [42]es errónea.

Sin embargo, lo que la declaración anterior intenta hacer se puede expresar a través de un intermedio typedef:

typedef int array_t[42];
const array_t a;

Pero esto no hace lo que parece. En esta declaración, no es lo aque se constcalifica, ¡sino los elementos! Es decir, a[0]es un const int, pero aes simplemente "matriz de int". En consecuencia, esto no requiere un diagnóstico:

int *p = a; /* surprise! */

Esto hace:

a[0] = 1;

Nuevamente, esto subraya la idea de que las referencias son, en cierto sentido, de "segunda clase" en el sistema de tipos, como las matrices.

Tenga en cuenta que la analogía es aún más profunda, ya que las matrices también tienen un "comportamiento de conversión invisible", como las referencias. Sin que el programador tenga que utilizar ningún operador explícito, el identificador ase convierte automáticamente en un int *puntero, como si se &a[0]hubiera utilizado la expresión . Esto es análogo a cómo una referencia ri, cuando la usamos como expresión primaria, denota mágicamente el objeto ial que está vinculada. Es sólo otro "decaimiento" como el "arreglo al decaimiento del puntero".

Y al igual que no debemos confundirnos con la decadencia de "arreglo a puntero" y pensar erróneamente que "los arreglos son solo punteros en C y C ++", tampoco debemos pensar que las referencias son solo alias que no tienen ningún tipo propio.

Cuando decltype(ri)suprime la conversión habitual de la referencia a su objeto de referencia, esto no es tan diferente de sizeof asuprimir la conversión de matriz a puntero y operar en el tipo de matriz en sí mismo para calcular su tamaño.

Kaz
fuente
Tiene mucha información interesante y útil aquí (+1), pero está más o menos limitado al hecho de que "las referencias están en el sistema de tipos"; esto no responde por completo a la pregunta de OP. Entre esta respuesta y la respuesta de @ songyuanyao, diría que hay información más que suficiente para comprender la situación, pero se siente como el trasfondo necesario para una respuesta en lugar de una respuesta completa.
Kyle Strand
1
Además, creo que vale la pena resaltar esta oración: "Cuando usas ri, la referencia se resuelve de manera transparente ..." Un punto clave (que he mencionado en los comentarios pero que hasta ahora no ha aparecido en ninguna de las respuestas ) es que decltype no realiza esta resolución transparente (no es una función, por rilo que no se "usa" en el sentido que usted describe). Esto encaja muy bien con todo su enfoque en el sistema de tipos : la conexión clave es que decltypees una operación del sistema de tipos .
Kyle Strand
Mmmm, lo de las matrices parece una tangente, pero la oración final es útil.
Kyle Strand
..... aunque en este punto ya te he votado a favor y no soy el OP, así que no estás ganando ni perdiendo nada al escuchar mis críticas ...
Kyle Strand
Obviamente, una respuesta fácil (y correcta) solo nos apunta al estándar, pero me gusta el pensamiento de fondo aquí, aunque algunos podrían argumentar que partes de él son suposiciones. +1.
Steve Kidd
-5

const X & x ”significa que x asigna un alias a un objeto X, pero no puedes cambiar ese objeto X mediante x.

Y vea std :: is_const .

Sotter.liu
fuente
esto ni siquiera intenta responder a la pregunta.
bolov