¿Cómo es que una referencia no constante no puede unirse a un objeto temporal?

226

¿Por qué no está permitido obtener referencias no constantes a un objeto temporal, que función getx()devuelve? Claramente, esto está prohibido por C ++ Standard 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;
}
  1. Está claro que la vida útil del objeto no puede ser la causa, porque C ++ Standard no prohíbe la referencia constante a un objeto .
  2. Está claro que el objeto temporal no es constante en la muestra anterior, porque se permiten llamadas a funciones no constantes. Por ejemplo, ref()podría modificar el objeto temporal.
  3. Además, le ref()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 constante extiende la vida útil de este objeto" y "Sin embargo, no se dice nada sobre las referencias no constantes". Mi pregunta adicional . ¿La siguiente asignación extiende la vida útil del objeto temporal?

X& x = getx().ref(); // OK
Alexey Malistov
fuente
44
No estoy de acuerdo con la parte "la vida útil del objeto no puede ser la causa", solo porque se establece en el estándar, que asignar un objeto temporal a la referencia constante extiende la vida útil de este objeto a la duración de la referencia constante. Nada se dice acerca de las referencias no constante aunque ...
SadSido
3
Bueno, cuál es la causa de eso "Sin embargo, no se dice nada sobre las referencias no constantes ...". Es parte de mi pregunta. ¿Hay algún sentido en esto? Puede que los autores de Standard se hayan olvidado de las referencias no constantes y pronto veremos el próximo número principal.
Alexey Malistov
2
GotW # 88: un candidato para el "Const más importante". hierbasutter.spaces.live.com/blog/cns!2D4327CC297151BB!378.entry
fnieto - Fernando Nieto
44
@Michael: VC enlaza los valores con referencias no constantes. Llaman a esto una característica, pero realmente es un error. (Tenga en cuenta que no es un error porque es inherentemente ilógico, sino porque se descartó explícitamente para evitar errores tontos.)
sbi

Respuestas:

97

De este artículo del blog de Visual C ++ sobre las referencias de rvalue :

... C ++ no quiere que modifiques accidentalmente los temporales, pero llamar directamente a una función miembro no constante en un valor variable modificable es explícito, por lo que está permitido ...

Básicamente, no deberías intentar modificar los temporales por el solo hecho de que son objetos temporales y morirán en cualquier momento. La razón por la que puede llamar a métodos no constantes es que, bueno, puede hacer algunas cosas "estúpidas" siempre y cuando sepa lo que está haciendo y sea explícito al respecto (por ejemplo, usando reinterpret_cast). Pero si vincula una referencia temporal a una referencia no constante, puede seguir pasándola "para siempre" solo para que su manipulación del objeto desaparezca, porque en algún momento del camino olvidó por completo que esto era temporal.

Si yo fuera usted, repensaría el diseño de mis funciones. ¿Por qué g () acepta referencia, modifica el parámetro? Si no, haga referencia constante, en caso afirmativo, ¿por qué intenta pasarle temporalmente? ¿No le importa que sea una modificación temporal? ¿Por qué getx () regresa temporalmente 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 problemas, por lo general crea problemas.


Editar: Abordar preguntas en el comentario: 1) X& x = getx().ref(); // OK when will x die?- No lo sé y no me importa, porque esto es exactamente lo que quiero decir con "ir en contra del idioma". El lenguaje dice que "los temporales mueren al final de la declaración, a menos que estén obligados a hacer referencia constante, en cuyo caso mueren cuando la referencia queda fuera de alcance". Aplicando esa regla, parece que x ya está muerto al comienzo de la siguiente declaración, ya que no está obligado a const referencia (el compilador no sabe qué devuelve () devuelve). Sin embargo, esto es solo una suposición.

2) Indiqué claramente el propósito: no está permitido modificar temporarios, porque simplemente no tiene sentido (ignorando las referencias de valor de C ++ 0x). La pregunta "entonces, ¿por qué se me permite llamar a miembros que no son constantes?" es buena, pero no tengo una mejor respuesta que la que dije anteriormente.

3) Bueno, si estoy en lo cierto al X& x = getx().ref();morir x al final de la declaración, los problemas son obvios.

De todos modos, según su pregunta y comentarios, no creo que incluso 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 la vinculación a referencias no constantes. Puede haber alguna implementación del compilador o también hubo problemas históricos, no lo sé. Luego, surgió un caso específico, y se decidió que, contra todo pronóstico, todavía permitirán la modificación directa mediante la llamada al método no constante. Pero eso es una excepción: generalmente no se le permite modificar los temporales. Sí, C ++ es a menudo así de raro.

sbk
fuente
2
@sbk: 1) En realidad, la frase correcta es: "... al final de la expresión completa ...". Una "expresión completa", creo, se define como una que no es una subexpresión de alguna otra expresión. Si esto siempre es lo mismo que "el final de la declaración", no estoy seguro.
sbi
55
@sbk: 2) En realidad, usted está permitido modificar rvalues (Temporaries). Está prohibido para los tipos integrados ( intetc.), pero se permite para los tipos definidos por el usuario: (std::string("A")+"B").append("C").
sbi
66
@sbk: 3) La razón que Stroustrup da (en D&E) para no permitir la vinculación de valores a referencias no constantes es que, si Alexey g()modifica el objeto (lo que cabría esperar de una función que toma una referencia no constante), modificaría un objeto que va a morir, para que nadie pueda obtener el valor modificado de todos modos. Él dice que esto, muy probablemente, es un error.
sbi
2
@sbk: Lo siento si te he ofendido, pero no creo que 2) sea una trampa. Los valores simplemente no son constantes a menos que los haga así y pueda cambiarlos, a menos que sean incorporados. Me tomó un tiempo entender si, por ejemplo, con el ejemplo de cadena, hago algo mal (JFTR: no lo hago), por lo que tiendo a tomar en serio esta distinción.
sbi
55
Estoy de acuerdo con sbi: este asunto no es nada quisquilloso. Toda la base de la semántica de movimiento es que los valores de tipo de clase se mantienen mejor sin constante.
Johannes Schaub - litb
38

En su código getx()devuelve un objeto temporal, un llamado "rvalue". Puede copiar valores en objetos (también conocidos como variables) o vincularlos a referencias constantes (lo que extenderá su vida útil hasta el final de la vida de la referencia). No puede vincular 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 C ++ incluirá referencias rvalue. Por lo tanto, lo que conoce como referencias se está convirtiendo en "referencias de valor". Se le permitirá vincular rvalues ​​a referencias de rvalue y puede sobrecargar 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 valor es que, dado que estos objetos van a morir de todos modos, puede 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
sbi
fuente
2
Hola, esta es una respuesta asombrosa. Necesito saber una cosa, el g(getx())no funciona porque su firma es g(X& x)y get(x)devuelve un objeto temporal, por lo que no podemos vincular un objeto temporal ( rvalue ) a una referencia no constante, ¿correcto? Y en su primer código, creo que será en const X& x2 = getx();lugar de const X& x1 = getx();...
SexyBeast
1
¡Gracias por señalar este error en mi respuesta 5 años después de que lo escribí! :-/Sí, su razonamiento es correcto, aunque un poco al revés: no podemos vincular temporales a constreferencias que no sean (lvalue), y por lo tanto, el temporal devuelto por getx()(y no get(x)) no puede vincularse a la referencia de lvalue a la que se refiere el argumento g().
sbi
Umm, ¿qué quisiste decir con getx()(y no get(x)) ?
SexyBeast
Cuando escribo "... getx () (y no get (x)) ..." , quiero decir que el nombre de la función es getx(), y no get(x)(como usted escribió).
sbi
Esta respuesta mezcla terminología. Un rvalue es una categoría de expresión. Un objeto temporal es un objeto. Un valor r puede o no denotar un objeto temporal; y un objeto temporal puede o no ser denotado por un valor r.
MM
16

Lo que está mostrando es que se permite 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 es no constante, lo que le permite llamar a otros métodos para calcular un valor o tener otros efectos secundarios ocurren.

// It would allow things like this.
getPipeline().procInstr(1).procInstr(2).procInstr(3);

// or more commonly
std::cout << getManiplator() << 5;

Mire al final de esta respuesta para un mejor ejemplo de esto

Puede que no se unen a un temporal a una referencia ya que al hacerlo va a generar una referencia a un objeto que será destruido al final de la expresión que lo que deja colgando con una referencia (que es desordenado 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, por lo que la vida útil de los objetos temporales se extiende más allá del final de la declaración.

Entonces, la única pregunta que queda es ¿por qué el estándar no quiere permitir referencias a 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 sea muy difícil de corregir para objetos temporales. Se realizó para referencias constantes 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 será muy confuso.
Mientras que lo siguiente:

int const& y = getI();

Le dará un código que es intuitivo de usar y comprender.

Si desea modificar el valor, debe devolver el valor a una variable. Si está tratando de evitar el costo de copiar el objeto desde la función (ya que parece que el objeto es una copia reconstruida (técnicamente lo es)). Entonces no te molestes, el compilador es muy bueno en 'Optimización del valor de retorno'

Martin York
fuente
3
"Entonces, la única pregunta que queda es por qué el estándar no quiere permitir referencias a temporales para extender la vida del objeto más allá del final de la declaración". ¡Eso es! Usted entiende mi pregunta. Pero no estoy de acuerdo con tu opinión. Dices "haz que el compilador sea muy difícil", pero fue hecho para referencia constante. Usted dice en su muestra "Nota x es un alias de una variable. ¿Qué variable está actualizando?" No hay problema. Hay una variable única (temporal). Algún objeto temporal (igual a 5) debe ser cambiado.
Alexey Malistov
@ Martin: Las referencias colgantes no solo son desordenadas. ¡Podrían provocar errores graves cuando se acceda más adelante en el método!
mmmmmmmm
1
@Alexey: Tenga en cuenta que el hecho de que vincularlo a una referencia constante mejora la vida útil de un temporal es una excepción que se ha agregado deliberadamente (TTBOMK para permitir optimizaciones manuales). No se agregó una excepción para las referencias que no son constantes, ya que se consideró que vincular una referencia temporal a una no constante era un error del programador.
sbi
1
@alexy: ¡Una referencia a una variable invisible! No es tan intuitivo.
Martin York
const_cast<x&>(getX());no tiene sentido
curiousguy
10

¿Por qué se discute en las preguntas frecuentes de C ++ ( negrita mía):

En C ++, las referencias no constantes se pueden unir a valores y las referencias constantes se pueden unir a valores o valores, pero no hay nada que pueda unirse a un valor no constante. Eso 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 ese incr (0) se permitiera, se incrementaría algo temporal que nadie vio o, lo que es peor, el valor de 0 se convertiría en 1. Esto último suena tonto, pero en realidad hubo un error como ese en los primeros compiladores de Fortran que establecieron aparte una ubicación de memoria para mantener el valor 0.

Tony Delroy
fuente
¡Habría sido divertido ver la cara del programador que fue mordido por ese "error cero" de Fortran! x * 0 da x? ¿Qué? ¿¿Qué??
John D
Ese último argumento es particularmente débil. Ningún compilador que valga la pena mencionar cambiaría el valor de 0 a 1, ni siquiera lo interpretaría incr(0);de esa manera. Obviamente, si esto se permitiera, se interpretaría como la creación de un número entero temporal y pasarlo aincr()
user3204459 el
6

El problema principal es que

g(getx()); //error

es un error lógico: gestá modificando el resultado getx()pero no tiene ninguna posibilidad de examinar el objeto modificado. Si gno fuera necesario modificar su parámetro, entonces no habría requerido una referencia de valor, podría haber tomado el parámetro por valor o por referencia constante.

const X& x = getx(); // OK

es válido porque a veces necesita reutilizar el resultado de una expresión, y está bastante claro que está tratando con un objeto temporal.

Sin embargo, no es posible hacer

X& x = getx(); // error

válido sin hacerlo 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 saben acerca de la consistencia del this, no saben si se les llama en un valor o en un valor.

Como siempre en C ++, tiene una solución para esta regla, pero debe indicarle al compilador que sabe lo que está haciendo al ser explícito:

g(const_cast<x&>(getX()));
Dan Berindei
fuente
5

Parece que la pregunta original de por qué esto no está permitido se ha respondido claramente: "porque es muy probable que sea un error".

FWIW, pensé que mostraría cómo hacerlo, aunque no creo que sea una buena técnica.

La razón por la que a veces quiero pasar un método temporal a un método que toma una referencia no constante es descartar 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 muestra que puedes usar el casting para mentirle 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 alguna razón 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

Chris Pearson
fuente
4

¿Por qué querrías alguna vez X& x = getx();? Solo use X x = getx();y confíe en RVO.

DevFred
fuente
3
Porque quiero llamar en g(getx())lugar deg(getx().ref())
Alexey Malistov
44
@Alexey, esa no es una razón real. Si haces eso, entonces tienes un error lógico en algún lugar, porque gva a modificar algo que ya no puedes tener en tus manos.
Johannes Schaub - litb
3
@ JohannesSchaub-litb tal vez no le importa.
curioso
" confiar en RVO ", excepto que no se llama "RVO".
curioso
2
@ curiousguy: Ese es un término muy aceptado para ello. No hay absolutamente nada de malo en referirse a él como "RVO".
Puppy
3

Excelente pregunta, y aquí está mi intento de una respuesta más concisa (dado que hay mucha información útil en los comentarios y difícil de excavar en el ruido).

Cualquier referencia vinculada directamente a un temporal extenderá su vida [12.2.5]. Por otro lado, una referencia inicializada con otra referencia no lo hará (incluso si finalmente 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 lo temporal dure tanto como la xreferencia, pero const X &x = X().ref();NO lo hará (quién sabe lo que ref()realmente regresó). En el último caso, el destructor para Xse llama al final de esta línea. (Esto es observable con un destructor no trivial).

Por lo tanto, en general parece confuso y peligroso (¿por qué complicar las reglas sobre la vida útil de los objetos?), Pero presumiblemente era necesario al menos referencias constantes, por lo que el estándar establece este comportamiento para ellos.

[Del comentario de sbi ]: Tenga en cuenta que el hecho de vincularlo a una referencia constante mejora la vida útil de un temporal es una excepción que se ha agregado deliberadamente (TTBOMK para permitir optimizaciones manuales). No se agregó una excepción para las referencias que no son constantes, ya que se consideró que vincular una referencia temporal a una no constante era un error del programador.

Todos los temporales persisten hasta el final de la expresión completa. Sin embargo, para utilizarlos, necesitas un truco como el que tienes ref(). Eso es legal No parece haber una buena razón para que salte el aro adicional, excepto para recordarle al programador que está sucediendo algo inusual (es decir, un parámetro de referencia cuyas modificaciones se perderán rápidamente).

[Otro comentario de sbi ] La razón que da Stroustrup (en D&E) para no permitir la vinculación de valores a referencias no constantes es que, si la g () de Alexey modificaría el objeto (lo que cabría esperar de una función que realiza una función no constante) referencia), modificaría un objeto que va a morir, para que nadie pueda obtener el valor modificado de todos modos. Él dice que esto, muy probablemente, es un error.

DS.
fuente
2

"Está claro que el objeto temporal no es constante en la muestra anterior, porque se permiten llamadas a funciones no constantes. Por ejemplo, ref () podría modificar el objeto temporal".

En su ejemplo, getX () no devuelve una constante 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 sus tres puntos sean incorrectos.

Patricio
fuente
1

Tengo un escenario que me gustaría compartir donde desearía poder hacer lo que Alexey me pide. En un complemento 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 de escribir, así que escribí las siguientes funciones auxiliares:

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 de la publicación como:

(myNode | attributeName) << myArray;

El problema es que no se compila fuera de Visual C ++, porque está tratando de vincular la variable temporal devuelta por | 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 MPlug no se copie tanto. Solo necesito el objeto temporal para vivir hasta el final de la segunda función.

Bueno, este es mi escenario. Solo pensé en mostrar un ejemplo en el que a uno le gustaría hacer lo que Alexey describe. Agradezco todas las críticas y sugerencias!

Gracias.

Robert Trussardi
fuente