¿Qué expresiones producen un tipo de referencia cuando se aplica decltype a ellas?

Estaba leyendo C ++ Primer y no pude entender bien cuándo una expresión produce un tipo de objeto y cuándo produce un tipo de referencia para el objeto.

Cito del libro:

  1. Cuando aplicamos decltype a una expresión que no es una variable, obtenemos el tipo que produce esa expresión.
  2. En general, decltype devuelve un tipo de referencia para las expresiones que producen objetos que se pueden ubicar en el lado izquierdo de la asignación.

Teniendo en cuenta el código a continuación:

int i = 3, *ptr = &i, &ref = i; decltype(ref + 0) j; 

En el código anterior, la expresión “ref + 0” da como resultado una operación inherente de adición de valor del objeto al que ref se refiere, i y 0. Por lo tanto, pasando por la primera regla, la expresión produce un tipo int. Pero yendo por la segunda regla, como la expresión produce el tipo de un objeto que puede pararse en el lado izquierdo de una asignación (en este caso int), ¿no debería el tipo de decltype dar un ref al tipo int (int)?

El libro también dice, para el siguiente código

 decltype(*ptr) k; 

k tiene tipo int & y no int, el tipo en el que se genera la expresión.

También dice que para una expresión de asignación como en el código a continuación

 decltype(a = b) l; 

Tendría el tipo de referencia al objeto en el lado izquierdo de la operación de asignación.

¿Cómo sabríamos qué expresiones producen el tipo de objeto y qué ceden la referencia al tipo de objeto?

No es fácil entender estos conceptos sin ser formal. El iniciador probablemente no quiere confundirlo y evita introducir términos como ” lvalue “, ” rvalue ” y ” xvalue “. Desafortunadamente, estos son fundamentales para comprender cómo funciona el decltype .

En primer lugar, el tipo de expresión evaluada nunca es un tipo de referencia, ni un tipo const alto nivel para tipos no de clase (por ejemplo, int const o int& ). Si el tipo de una expresión resulta ser int& o int const , inmediatamente se transforma en int antes de cualquier evaluación adicional.

Esto se especifica en los párrafos 5/5 y 5/6 de la Norma C ++ 11:

5 Si una expresión tiene inicialmente el tipo “referencia a T” (8.3.2, 8.5.3), el tipo se ajusta a T antes de cualquier análisis posterior. La expresión designa el objeto o función denotada por la referencia, y la expresión es un valor l o un valor x , dependiendo de la expresión.

6 Si un prvalue tiene inicialmente el tipo “cv T”, donde T es un tipo no-cv no calificado, que no es de clase, el tipo de expresión se ajusta a T antes de cualquier análisis posterior.

Tanto para las expresiones ¿Qué hace decltype ? Bueno, las reglas que determinan el resultado de decltype(e) para una expresión dada e se especifican en el párrafo 7.1.6.2/4:

El tipo indicado por decltype(e) se define de la siguiente manera:

– si e es una expresión de id no aparente o un acceso de miembro de clase no apareado (5.2.5), decltype(e) es el tipo de la entidad nombrada por e . Si no existe tal entidad, o si e nombra un conjunto de funciones sobrecargadas, el progtwig está mal formado;

– de lo contrario, si e es un valor decltype(e) , decltype(e) es T&& , donde T es el tipo de e ;

– de lo contrario, si e es un valor decltype(e) , decltype(e) es T& , donde T es el tipo de e ;

– De lo contrario, decltype(e) es el tipo de e .

El operando del especificador de decltype es un operando no evaluado (Cláusula 5).

Esto puede sonar confuso. Tratemos de analizarlo parte por parte. Ante todo:

– si e es una expresión de id no aparente o un acceso de miembro de clase no apareado (5.2.5), decltype(e) es el tipo de la entidad nombrada por e . Si no existe tal entidad, o si e nombra un conjunto de funciones sobrecargadas, el progtwig está mal formado;

Esto es simple. Si e es solo el nombre de una variable y no la coloca entre paréntesis, entonces el resultado de decltype es el tipo de esa variable. Asi que

 bool b; // decltype(b) = bool int x; // decltype(x) = int int& y = x; // decltype(y) = int& int const& z = y; // decltype(z) = int const& int const t = 42; // decltype(t) = int const 

Observe que el resultado de decltype(e) aquí no es necesariamente el mismo que el tipo de la expresión evaluada e . Por ejemplo, la evaluación de la expresión z produce un valor de tipo int const , not int const& (porque en el párrafo 5/5 el & se elimina, como hemos visto anteriormente).

Veamos qué pasa cuando la expresión no es solo un identificador:

– de lo contrario, si e es un valor decltype(e) , decltype(e) es T&& , donde T es el tipo de e ;

Esto se está complicando. ¿Qué es un valor x ? Básicamente, es una de las tres categorías a las que puede pertenecer una expresión ( xvalue , lvalue o prvalue ). Normalmente, se obtiene un valor x cuando se invoca una función con un tipo de retorno que es un tipo de referencia de valor r , o como resultado de un modelo estático a un tipo de referencia de valor r . El ejemplo típico es una llamada a std::move() .

Para usar la redacción del Estándar:

[Nota: una expresión es un valor x si es:

– el resultado de llamar a una función, ya sea implícita o explícitamente, cuyo tipo de devolución es una referencia rvalue al tipo de objeto,

– un lanzamiento a una referencia rvalue al tipo de objeto,

– una expresión de acceso de miembro de clase que designa un miembro de datos no estáticos de tipo no de referencia en el que la expresión de objeto es un valor x , o

– a .* Expresión de puntero a miembro en la que el primer operando es un valor xy el segundo operando es un puntero al miembro de datos.

En general, el efecto de esta regla es que las referencias rvalue nombradas se tratan como lvalues y las referencias rvalue sin nombre a los objetos se tratan como xvalues ; Las referencias a las funciones de rvalue se tratan como lvalues, ya sean nombrados o no. -Finalizar nota]

Entonces, por ejemplo, las expresiones std::move(x) , static_cast(x) y std::move(p).first (para un objeto p de tipo pair ) son xvalues. Cuando aplica decltype a una expresión xvalue , decltype agrega && al tipo de la expresión:

 int x; // decltype(std::move(x)) = int&& // decltype(static_cast(x)) = int&& 

Continuemos:

– de lo contrario, si e es un valor decltype(e) , decltype(e) es T& , donde T es el tipo de e ;

¿Qué es un lvalue ? Bueno, informalmente, la expresión lvalue son expresiones que denotan objetos a los que se puede hacer referencia de forma repetitiva en el progtwig, por ejemplo, variables con un nombre u objetos de los que se puede tomar la dirección.

Para una expresión e de tipo T que es una expresión decltype(e) , decltype(e) produce T& . Entonces, por ejemplo:

 int x; // decltype(x) = int (as we have seen) // decltype((x)) = int& - here the expression is parenthesized, so the // first bullet does not apply and decltype appends & to the type of // the expression (x), which is int 

Una llamada de función para una función cuyo tipo de devolución es T& también es una expresión lvalue , entonces:

 int& foo() { return x; } // decltype(foo()) = int& 

Finalmente:

– De lo contrario, decltype(e) es el tipo de e .

Si la expresión no es un valor x ni un valor l (en otras palabras, si es un valor prve ), el resultado de decltype(e) es simplemente el tipo de e . Las palabras temporales y literales sin nombre son valores prvaluados . Entonces, por ejemplo:

 int foo() { return x; } // Function calls for functions that do not return // a reference type are prvalue expressions // decltype(foo()) = int // decltype(42) = int 

Vamos a aplicar lo anterior a los ejemplos de su pregunta. Dadas estas declaraciones:

 int i = 3, *ptr = &i, &ref = i; decltype(ref + 0) j; decltype(*ptr) k; decltype(a = b) l; 

El tipo de j será int , porque operator + devuelve un prvalue de tipo int . El tipo de k será int& , porque el operator * unario operator * arroja un valor l (ver el párrafo 5.3.1 / 1). El tipo de l también es int& porque el resultado de operator = es un lvalue (ver párrafo 5.17 / 1).

En relación con esta parte de tu pregunta:

Pero yendo por la segunda regla, como la expresión produce el tipo de un objeto que puede pararse en el lado izquierdo de una asignación (en este caso int), ¿no debería el tipo de decltype dar un ref al tipo int (int)?

Probablemente malinterpretaste ese pasaje del libro. No todos los objetos de tipo int pueden estar en el lado izquierdo de una tarea. Por ejemplo, la asignación siguiente es ilegal:

 int foo() { return 42; } foo() = 24; // ERROR! foo() is a prvalue expression, cannot be on the left // side of an assignment 

Si una expresión puede aparecer en el lado izquierdo de una tarea (observe que estamos hablando del operador de asignación incorporado para tipos de datos fundamentales aquí) depende de la categoría de valor de esa expresión ( lvalue , xvalue o prvalue ) , y la categoría de valor de una expresión es independiente de su tipo.

Para las expresiones, como en los ejemplos, decltype proporcionará un tipo de referencia si el argumento es lvalue.

7.1.6.2p4:

 The type denoted by decltype(e) is defined as follows: — if e is an unparenthesized id-expression or an unparenthesized class member access (5.2.5), decltype(e) is the type of the entity named by e. If there is no such entity, or if e names a set of overloaded functions, the program is ill-formed; — otherwise, if e is an xvalue, decltype(e) is T&&, where T is the type of e; — otherwise, if e is an lvalue, decltype(e) is T&, where T is the type of e; — otherwise, decltype(e) is the type of e. The operand of the decltype specifier is an unevaluated operand (Clause 5). [ Example: const int&& foo(); int i; struct A { double x; }; const A* a = new A(); decltype(foo()) x1 = i; // type is const int&& decltype(i) x2; // type is int decltype(a->x) x3; // type is double decltype((a->x)) x4 = x3; // type is const double& —end example ]