condicional - ¿Es seguro crear una referencia constante al resultado del operador ternario en C++?
return operador ternario (3)
Hay algo bastante obvio en este código:
float a = 1.;
const float & x = true ? a : 2.; // Note: `2.` is a double
a = 4.;
std::cout << a << ", " << x;
Salida de Clang y Gcc:
4, 1
Uno esperaría ingenuamente el mismo valor impreso dos veces, pero este no es el caso. El tema aquí no tiene nada que ver con la referencia. Hay algunas reglas interesantes que dictan el tipo de ? :
? :
. Si los dos argumentos son de diferente tipo y pueden ser emitidos, lo harán usando un temporal. La referencia apuntará a lo temporal de ? :
? :
.
El ejemplo anterior compila bien y puede o no emitir una advertencia al compilar con -Wall
dependiendo de la versión de su compilador.
Aquí hay un ejemplo de lo fácil que es equivocarse con un código de apariencia legítima:
template<class Iterator, class T>
const T & min(const Iterator & iter, const T & b)
{
return *iter < b ? *iter : b;
}
int main()
{
// Try to remove the const or convert to vector of floats
const std::vector<double> a(1, 3.0);
const double & result = min(a.begin(), 4.);
cout << &a[0] << ", " << &result;
}
Si su lógica después de este código asume que cualquier cambio en a[0]
se reflejará en el result
, será incorrecto en los casos donde ?:
Crea un temporal. Además, si en algún momento hace un puntero al result
y lo usa después de que el result
salga del alcance, habrá una falla de segmentación a pesar del hecho de que su original no se ha salido del alcance.
Siento que hay razones serias para NO usar este formulario más allá de los "problemas de mantenimiento y lectura" mencionados here especialmente al escribir código con plantilla donde algunos de sus tipos y su constancia podrían estar fuera de su control.
Así que mi pregunta es, ¿es seguro usar const &
s en operadores ternarios?
Ejemplo 1 de PS Bonus, complicaciones adicionales (ver también here ):
float a = 0;
const float b = 0;
const float & x = true ? a : b;
a = 4;
cout << a << ", " << x;
salida del clang:
4, 4
Salida gcc 4.9.3:
4, 0
Con clang, este ejemplo se compila y ejecuta como se esperaba pero con versiones recientes de gcc (
PS2 Bonus ejemplo 2, ideal para entrevistas;):
double a = 3;
const double & a_ref = a;
const double & x = true ? a_ref : 2.;
a = 4.;
std::cout << a << ", " << x;
salida:
4, 3
¿Es seguro crear una referencia constante al resultado del operador ternario en C ++?
Como el que pregunta, resumiría la discusión a; Está bien para código sin plantilla, en compiladores bastante modernos, con Advertencias activadas. Para el código de plantilla, como revisor de código, en general lo desalentaría.
En primer lugar, el resultado del operador condicional es un glvalue que designa el operando seleccionado o un prvalue cuyo valor proviene del operando seleccionado.
Excepción según lo señalado por TC: si al menos un operando es de clase y tiene un operador de conversión a referencia, el resultado puede ser un lvalor que designa el objeto designado por el valor de retorno de ese operador; y si el objeto designado es en realidad un temporal, puede resultar una referencia colgante. Este es un problema con los operadores que ofrecen la conversión implícita de valores predefinidos a valores, no un problema introducido por el operador condicional per se.
En ambos casos, es seguro vincular una referencia al resultado, se aplican las reglas habituales para vincular una referencia a un valor o un valor predeterminado. Si la referencia se enlaza con un prvalue (ya sea el resultado prvalue del condicional o un prvalue inicializado a partir del valor lvalue del condicional), el tiempo de vida del prvalue se extiende para que coincida con el tiempo de vida de la referencia.
En su caso original, el condicional es:
true ? a : 2.
El segundo y tercer operando son: "lvalue of type float
" y "prvalue of type double
". Este es el caso 5 en el resumen de referencia de cpp , con el resultado "prvalue of type double
".
Luego, su código inicializa una referencia constante con un prvalor de un tipo diferente (no relacionado con la referencia). El comportamiento de esto es copiar-inicializar un temporal del mismo tipo que la referencia.
En resumen, después de const float & x = true ? a : 2.;
const float & x = true ? a : 2.;
, x
es un lvalor que denota un valor float
cuyo valor es el resultado de convertir a
a double
y back. (No estoy seguro si estoy seguro de que eso sea igual a a
). x
no está vinculado a a
.
En el caso de bonificación 1, el segundo y tercer operando del operador condicional son "lvalue of type float
" y "lvalue of type const float
". Este es el caso 3 del mismo enlace cppreference,
ambos son valores de la misma categoría de valor y tienen el mismo tipo, excepto para la calificación cv
El comportamiento es que el segundo operando se convierte a "lvalue of type const float
" (que denota el mismo objeto), y el resultado del condicional es "lvalue of type const float
" que denota el objeto seleccionado.
A continuación, debe enlazar const float &
a "lvalue of type const float
", que se enlaza directamente.
Así que después de const float & x = true ? a : b;
const float & x = true ? a : b;
, x
está directamente vinculado a a
o b
.
En caso de bonificación 2, true ? a_ref : 2.
true ? a_ref : 2.
.. El segundo y tercer operandos son "lvalue of type const double
" y "prvalue of type double
", por lo que el resultado es "prvalue of type double
".
Luego, enlaza esto con const double & x
, que es un enlace directo, ya que const double
está relacionado con la referencia con double
.
Así que después de const double & x = true ? a_ref : 2.;
const double & x = true ? a_ref : 2.;
, entonces x
es un lvalor que denota un doble con el mismo valor que a_ref
(pero x
no está vinculado a a
).
En resumen: sí, puede ser seguro. Pero necesitas saber qué esperar.
Las referencias const y las referencias rvalue se pueden usar para prolongar la vida útil de las variables temporales (menos las excepciones a las que se hace referencia a continuación).
Por cierto, ya hemos aprendido de su here que la serie gcc 4.9 no es la mejor referencia para este tipo de prueba. El ejemplo de bonificación 1 compilado con gcc 6.1 o 5.3 da exactamente el mismo resultado que compilado con clang. Como se supone que debe
Citas de N4140 (fragmentos seleccionados):
[class.temporary]
Hay dos contextos en los que los temporales se destruyen en un punto diferente al final de la expresión completa. [...]
El segundo contexto es cuando una referencia está vinculada a un temporal. El temporal al que está vinculada la referencia o el temporal que es el objeto completo de un subobjeto al que está vinculada la referencia persiste durante toda la vida útil de la referencia, excepto: [no hay cláusulas relevantes para esta pregunta]
En
[expr.cond]
3) De lo contrario, si el segundo y tercer operando tienen tipos diferentes y tienen un tipo de clase (posiblemente cv-calificado), o si ambos son valores de la misma categoría de valor y el mismo tipo, excepto para la calificación de cv, se intenta Convierte cada uno de esos operandos al tipo del otro.
Si
E2
es un lvalue:E1
se puede convertir para que coincida conE2
siE1
se puede convertir implícitamente (Cláusula 4) al tipo "lvalue reference toT2
", sujeto a la restricción de que en la conversión la referencia debe vincularse directamente a un lvalue[...]
Si
E2
es un prvalue o si no se puede realizar ninguna de las conversiones anteriores y al menos uno de los operandos tiene un tipo de clase (posiblemente cv-calificado):
- De lo contrario (es decir, si
E1
oE2
tienen un tipo que no sea de clase, o si ambos tienen tipos de clase pero las clases subyacentes no son las mismas o una clase base de la otra):E1
se puede convertir para que coincida conE2
siE1
puede ser implícitamente convertido al tipo que tendría la expresiónE2
siE2
se convirtiera en un prvalue (o el tipo que tiene, siE2
es un prvalue)[...] Si ninguno de los dos puede convertirse, los operandos no se modifican y se realiza una verificación adicional como se describe a continuación. Si es posible exactamente una conversión, esa conversión se aplica al operando elegido y el operando convertido se usa en lugar del operando original por el resto de esta sección.
4) Si los segundo y tercer operandos son valores de la misma categoría de valor y tienen el mismo tipo, el resultado es de esa categoría de tipo y valor [...]
5) De lo contrario, el resultado es un prvalue. Si el segundo y tercer operandos no tienen el mismo tipo, y cualquiera de los dos tiene clase de clase (posiblemente calificada para cv) [...]. De lo contrario, las conversiones así determinadas se aplican y los operandos convertidos se usan en lugar de los operandos originales para el resto de esta sección.
6) Las conversiones estándar de Lvalue-to-rvalue, array-to-pointer y function-to-pointer se realizan en el segundo y tercer operandos. Después de esas conversiones, se mantendrá una de las siguientes:
- El segundo y tercer operandos tienen aritmética o tipo de enumeración; Las conversiones aritméticas habituales se realizan para llevarlas a un tipo común, y el resultado es de ese tipo.
Entonces el primer ejemplo está bien definido para hacer exactamente lo que experimentaste:
float a = 1.;
const float & x = true ? a : 2.; // Note: `2.` is a double
a = 4.;
std::cout << a << ", " << x;
x
es una referencia vinculada a un objeto temporal de tipo float
. ¿No se refiere a, porque la expresión es true ? float : double
true ? float : double
se define para producir un double
, y solo entonces se está convirtiendo ese double
nuevo en un nuevo y diferente float
al asignarlo a x
.
En tu segundo ejemplo (bonus 1):
float a = 0;
const float b = 0;
const float & x = true ? a : b;
a = 4;
cout << a << ", " << x;
el operador ternario no tiene que hacer conversiones entre a
y b
(excepto para los calificadores de cv coincidentes) y produce un valor de l que se refiere a una constante flotante. x
alias x
y deben reflejar los cambios realizados en a.
En el tercer ejemplo (bonus 2):
double a = 3;
const double & a_ref = a;
const double & x = true ? a_ref : 2.;
a = 4.;
std::cout << a << ", " << x;
En este caso, E1
se puede convertir para que coincida con E2
si E1
se puede convertir implícitamente al tipo que [...] tiene [ E2
], si E2
es un prvalue . Ahora, ese valor predeterminado tiene el mismo valor que a
, pero es un objeto diferente. x
no alias a
.