c++ - ¿Por qué no se puede enlazar una referencia no constante a un objeto temporal?
reference temporary (11)
"Está claro que el objeto temporal no es constante en el ejemplo anterior, porque las llamadas a funciones no constantes están permitidas. Por ejemplo, ref () podría modificar el objeto temporal".
En su ejemplo, getX () no devuelve una const. X, por lo que puede llamar a ref () de la misma manera que podría llamar a X (). Ref (). Está devolviendo una referencia no constante y, por lo tanto, puede llamar a métodos no constantes, lo que no puede hacer es asignar la referencia a una referencia no constante.
Junto con el comentario de SadSidos, esto hace que tus tres puntos sean incorrectos.
¿Por qué no se permite obtener una referencia no constante a un objeto temporal, que getx()
función getx()
? Claramente, esto está prohibido por el estándar de C ++, pero estoy interesado en el propósito de dicha restricción, no en una referencia al estándar.
struct X
{
X& ref() { return *this; }
};
X getx() { return X();}
void g(X & x) {}
int f()
{
const X& x = getx(); // OK
X& x = getx(); // error
X& x = getx().ref(); // OK
g(getx()); //error
g(getx().ref()); //OK
return 0;
}
- Está claro que la vida útil del objeto no puede ser la causa, ya que la referencia constante a un objeto no está prohibida por C ++ Standard.
- Está claro que el objeto temporal no es constante en el ejemplo anterior, porque se permiten las llamadas a funciones no constantes. Por ejemplo,
ref()
podría modificar el objeto temporal. - Además,
ref()
te permite engañar al compilador y obtener un enlace a este objeto temporal y eso resuelve nuestro problema.
Adicionalmente:
Dicen que "asignar un objeto temporal a la referencia const prolonga la vida útil de este objeto" y "No se dice nada acerca de las referencias no constantes". Mi pregunta adicional . ¿La siguiente asignación extiende la vida útil del objeto temporal?
X& x = getx().ref(); // OK
¿Por qué querrías alguna vez X& x = getx();
? Solo usa X x = getx();
y confiar en RVO.
De este artículo del blog de Visual C ++ sobre referencias de rvalue :
... C ++ no quiere que modifiques temporariamente accidentalmente, pero llamar directamente a una función miembro no constante en un rvalor modificable es explícito, así que está permitido ...
Básicamente, no debes tratar de modificar los temporales por la misma razón de que son objetos temporales y morirán en cualquier momento. La razón por la que se le permite llamar a métodos no constantes es que, bueno, puede hacer algunas cosas "estúpidas" siempre que sepa lo que está haciendo y sea explícito (como usar reinterpret_cast). Pero si vincula una referencia temporal a una no constante, puede seguir pasándola "para siempre" solo para que desaparezca su manipulación del objeto, porque en algún punto del camino olvidó por completo que esto era temporal.
Si yo fuera tú, repensaría el diseño de mis funciones. ¿Por qué g () acepta la referencia, modifica el parámetro? Si no, haz que sea una referencia constante, si es así, ¿por qué tratas de pasárselo temporalmente? ¿No te importa si estás modificando un temporal? ¿Por qué getx () vuelve temporal de todos modos? Si comparte con nosotros su escenario real y lo que está tratando de lograr, puede obtener algunas buenas sugerencias sobre cómo hacerlo.
Ir en contra del lenguaje y engañar al compilador rara vez resuelve los problemas, generalmente crea problemas.
Edición: abordar las preguntas en el comentario: 1)X& x = getx().ref(); // OK when will x die?
X& x = getx().ref(); // OK when will x die?
- No sé y no me importa, porque esto es exactamente lo que quiero decir con "ir en contra del idioma". El lenguaje dice "los temporales mueren al final de la declaración, a menos que estén obligados a una referencia constante, en cuyo caso mueren cuando la referencia queda fuera del alcance". Aplicando esa regla, parece que x ya está muerto al comienzo de la siguiente declaración, ya que no está vinculado a la referencia const (el compilador no sabe qué devuelve ref ()). Sin embargo, esto es sólo una conjetura. 2) Establecí el propósito claramente: no se te permite modificar los temporales, porque simplemente no tiene sentido (ignorando las referencias de valor de C ++ 0x). La pregunta "entonces, ¿por qué se me permite llamar a los miembros no constantes?" Es una buena respuesta, pero no tengo mejor respuesta que la que ya mencioné anteriormente.
3) Bueno, si tengo razón sobre x en X& x = getx().ref();
Al morir al final de la declaración, los problemas son obvios.
De todos modos, según sus preguntas y comentarios, no creo que estas respuestas adicionales lo satisfagan. Aquí hay un último intento / resumen: el comité de C ++ decidió que no tiene sentido modificar los temporales, por lo tanto, rechazaron el enlace a las referencias no constantes. Puede haber alguna implementación del compilador o problemas históricos que también estuvieron involucrados, no lo sé. Luego, surgió un caso específico y se decidió que, a pesar de todas las probabilidades, todavía permitirían la modificación directa a través de la llamada al método no constante. Pero eso es una excepción: en general, no se les permite modificar los temporales. Sí, C ++ es a menudo así de raro.
El principal problema es que
g(getx()); //error
es un error lógico: g
está modificando el resultado de getx()
pero no tiene ninguna posibilidad de examinar el objeto modificado. Si g
no necesitaba modificar su parámetro, no habría requerido una referencia de valor l, podría haber tomado el parámetro por valor o por referencia constante.
const X& x = getx(); // OK
es válido porque a veces necesitas reutilizar el resultado de una expresión y es bastante claro que estás tratando con un objeto temporal.
Sin embargo no es posible realizar
X& x = getx(); // error
válido sin hacer que g(getx())
válido, que es lo que los diseñadores de idiomas intentaban evitar en primer lugar.
g(getx().ref()); //OK
es válido porque los métodos solo conocen la constancia de this
, no saben si se los llama en un valor de l o en un valor de valor.
Como siempre en C ++, tiene una solución para esta regla, pero debe indicar al compilador que sabe lo que está haciendo al ser explícito:
g(const_cast<x&>(getX()));
En su código, getx()
devuelve un objeto temporal, denominado "rvalue". Puede copiar valores en objetos (también conocido como variables) o enlazarlos a referencias constantes (lo que prolongará su vida útil hasta el final de la vida útil de la referencia). No puede enlazar valores a referencias no constantes.
Esta fue una decisión de diseño deliberada para evitar que los usuarios modifiquen accidentalmente un objeto que va a morir al final de la expresión:
g(getx()); // g() would modify an object without anyone being able to observe
Si desea hacer esto, primero deberá hacer una copia local o del objeto o vincularlo a una referencia constante:
X x1 = getx();
const X& x2 = getx(); // extend lifetime of temporary to lifetime of const reference
g(x1); // fine
g(x2); // can''t bind a const reference to a non-const reference
Tenga en cuenta que el próximo estándar de C ++ incluirá referencias de valor. Lo que usted conoce como referencias, por lo tanto, se va a llamar "referencias de valor l". Se le permitirá vincular rvalues a rvalue referencias y puede sobrecargar las funciones en "rvalue-ness":
void g(X&); // #1, takes an ordinary (lvalue) reference
void g(X&&); // #2, takes an rvalue reference
X x;
g(x); // calls #1
g(getx()); // calls #2
g(X()); // calls #2, too
La idea detrás de las referencias de rvalue es que, dado que estos objetos van a morir de todos modos, puedes aprovechar ese conocimiento e implementar lo que se llama "semántica de movimiento", un cierto tipo de optimización:
class X {
X(X&& rhs)
: pimpl( rhs.pimpl ) // steal rhs'' data...
{
rhs.pimpl = NULL; // ...and leave it empty, but deconstructible
}
data* pimpl; // you would use a smart ptr, of course
};
X x(getx()); // x will steal the rvalue''s data, leaving the temporary object empty
Excelente pregunta, y aquí está mi intento de obtener una respuesta más concisa (ya que una gran cantidad de información útil está en los comentarios y es difícil de descubrir en el ruido).
Cualquier referencia vinculada directamente a un temporal extenderá su vida útil [12.2.5]. Por otro lado, una referencia inicializada con otra referencia no lo hará (incluso si en última instancia es el mismo temporal). Eso tiene sentido (el compilador no sabe a qué se refiere esa referencia en última instancia).
Pero toda esta idea es extremadamente confusa. Por ejemplo, const X &x = X();
hará que la duración temporal sea tan larga como la referencia x
, pero const X &x = X().ref();
NO (quién sabe qué ref()
realmente devolvió). En este último caso, el destructor para X
se llama al final de esta línea. (Esto es observable con un destructor no trivial.)
Por lo general, parece confuso y peligroso (¿por qué complicar las reglas sobre la vida útil de los objetos?), Pero presumiblemente hubo una necesidad al menos de referencias const, por lo que el estándar establece este comportamiento para ellos.
[Del comentario sbi ]: sbi cuenta que el hecho de que vincularlo a una referencia constante mejore la vida útil de un temporero es una excepción que se ha agregado deliberadamente (TTBOMK para permitir optimizaciones manuales). No se agregó una excepción para las referencias no constantes, ya que probablemente la vinculación de una referencia temporal a una no constada sea un error de programador.
Todos los temporales persisten hasta el final de la expresión completa. Para hacer uso de ellos, sin embargo, necesitas un truco como el que tienes con ref()
. Eso es legal. No parece haber una buena razón para que salte el aro adicional, excepto para recordarle al programador que algo inusual está sucediendo (a saber, un parámetro de referencia cuyas modificaciones se perderán rápidamente).
[Otro comentario de sbi ] La razón que Stroustrup da (en D&E) para deshabilitar la vinculación de valores a referencias no constantes es que, si g () de Alexey modificaría el objeto (lo que cabría esperar de una función que toma una no constante). referencia), modificaría un objeto que va a morir, por lo que nadie podría obtener el valor modificado de todos modos. Él dice que esto, muy probablemente, es un error.
La solución maligna implica la palabra clave ''mutable''. En realidad ser malvado se deja como un ejercicio para el lector. O vea aquí: http://www.ddj.com/cpp/184403758
Lo que está mostrando es que está permitido el encadenamiento del operador.
X& x = getx().ref(); // OK
La expresión es ''getx (). Ref ();'' y esto se ejecuta hasta su finalización antes de la asignación a ''x''.
Tenga en cuenta que getx () no devuelve una referencia sino un objeto completamente formado en el contexto local. El objeto es temporal pero no es constante, lo que le permite llamar a otros métodos para calcular un valor o que se produzcan otros efectos secundarios.
// It would allow things like this.
getPipeline().procInstr(1).procInstr(2).procInstr(3);
// or more commonly
std::cout << getManiplator() << 5;
Mira el final de esta respuesta para un mejor ejemplo de esto.
No puede vincular una referencia temporal a una referencia, ya que al hacerlo se generará una referencia a un objeto que se destruirá al final de la expresión, lo que le deja una referencia pendiente (que está desordenada y el estándar no le gusta desordenado).
El valor devuelto por ref () es una referencia válida, pero el método no presta atención a la vida útil del objeto que está devolviendo (porque no puede tener esa información dentro de su contexto). Básicamente has hecho el equivalente de:
x& = const_cast<x&>(getX());
La razón por la que está bien hacer esto con una referencia constante a un objeto temporal es que el estándar extiende la vida útil de lo temporal a la vida útil de la referencia para que la vida útil de los objetos temporales se extienda más allá del final de la declaración.
Entonces, la única pregunta que queda es ¿por qué el estándar no quiere permitir una referencia a los temporales para extender la vida del objeto más allá del final de la declaración?
Creo que es porque hacerlo haría que el compilador fuera muy difícil de corregir para objetos temporales. Se hizo para las constantes referencias a los temporales, ya que esto tiene un uso limitado y, por lo tanto, lo obligó a hacer una copia del objeto para hacer algo útil, pero proporciona alguna funcionalidad limitada.
Piensa en esta situación:
int getI() { return 5;}
int x& = getI();
x++; // Note x is an alias to a variable. What variable are you updating.
Extender la vida útil de este objeto temporal va a ser muy confuso.
Mientras que las siguientes:
int const& y = getI();
Le dará un código que es intuitivo de usar y entender.
Si desea modificar el valor, debe devolver el valor a una variable. Si está intentando evitar el costo de volver a copiar el objeto desde la función (ya que parece que el objeto es una copia reconstruida (técnicamente lo es)). Entonces no molestes, el compilador es muy bueno en ''Optimización del valor de retorno''
Parece que la pregunta original de por qué esto no está permitido ha sido respondida claramente: "porque es muy probable que sea un error".
FWIW, pensé que iba a mostrar cómo se podría hacer, aunque no creo que sea una buena técnica.
La razón por la que a veces quiero pasar un temporal a un método que toma una referencia no constante es desechar intencionalmente un valor devuelto por referencia que al método de llamada no le importa. Algo como esto:
// Assuming: void Person::GetNameAndAddr(std::string &name, std::string &addr);
string name;
person.GetNameAndAddr(name, string()); // don''t care about addr
Como se explicó en las respuestas anteriores, eso no se compila. Pero esto compila y funciona correctamente (con mi compilador):
person.GetNameAndAddr(name,
const_cast<string &>(static_cast<const string &>(string())));
Esto solo demuestra que puedes usar casting para mentir al compilador. Obviamente, sería mucho más limpio declarar y pasar una variable automática no utilizada:
string name;
string unused;
person.GetNameAndAddr(name, unused); // don''t care about addr
Esta técnica introduce una variable local innecesaria en el alcance del método. Si, por algún motivo, desea evitar que se use más adelante en el método, por ejemplo, para evitar confusiones o errores, puede ocultarlo en un bloque local:
string name;
{
string unused;
person.GetNameAndAddr(name, unused); // don''t care about addr
}
- Chris
Tengo un escenario que me gustaría compartir donde me gustaría poder hacer lo que Alexey está preguntando. En un plugin de Maya C ++, tengo que hacer el siguiente shenanigan para obtener un valor en un atributo de nodo:
MFnDoubleArrayData myArrayData;
MObject myArrayObj = myArrayData.create(myArray);
MPlug myPlug = myNode.findPlug(attributeName);
myPlug.setValue(myArrayObj);
Esto es tedioso para escribir, así que escribí las siguientes funciones de ayuda:
MPlug operator | (MFnDependencyNode& node, MObject& attribute){
MStatus status;
MPlug returnValue = node.findPlug(attribute, &status);
return returnValue;
}
void operator << (MPlug& plug, MDoubleArray& doubleArray){
MStatus status;
MFnDoubleArrayData doubleArrayData;
MObject doubleArrayObject = doubleArrayData.create(doubleArray, &status);
status = plug.setValue(doubleArrayObject);
}
Y ahora puedo escribir el código desde el principio del post como:
(myNode | attributeName) << myArray;
El problema es que no se compila fuera de Visual C ++, porque está tratando de enlazar la variable temporal devuelta desde el | Operador a la referencia MPlug del operador <<. Me gustaría que fuera una referencia porque este código se llama muchas veces y prefiero que no se copie tanto el MPlug. Solo necesito el objeto temporal para vivir hasta el final de la segunda función.
Bueno, este es mi escenario. Solo pensé que mostraría un ejemplo donde a uno le gustaría hacer lo que Alexey describe. ¡Doy la bienvenida a todas las críticas y sugerencias!
Gracias.
Por qué se discute en las Preguntas frecuentes de C ++ (en negrita ):
En C ++, las referencias no constantes pueden vincularse a valores de l y las referencias const pueden vincularse a valores de l o rvalues, pero no hay nada que pueda vincularse a un valor no constante. Esto es para proteger a las personas de cambiar los valores de los temporales que se destruyen antes de que se pueda usar su nuevo valor . Por ejemplo:
void incr(int& a) { ++a; }
int i = 0;
incr(i); // i becomes 1
incr(0); // error: 0 is not an lvalue
Si se permitiera ese incremento (0), algún temporal que nadie vio se incrementaría o, mucho peor, el valor de 0 sería 1. El último suena tonto, pero en realidad hubo un error como ese en los compiladores tempranos de Fortran. Aparte de una ubicación de memoria para mantener el valor 0.