Imaginemos que tenemos una estructura para contener 3 dobles con algunas funciones miembro:
struct Vector {
double x, y, z;
// ...
Vector &negate() {
x = -x; y = -y; z = -z;
return *this;
}
Vector &normalize() {
double s = 1./sqrt(x*x+y*y+z*z);
x *= s; y *= s; z *= s;
return *this;
}
// ...
};
Esto es un poco artificial para simplificar, pero estoy seguro de que está de acuerdo en que existe un código similar. Los métodos le permiten encadenar convenientemente, por ejemplo:
Vector v = ...;
v.normalize().negate();
O incluso:
Vector v = Vector{1., 2., 3.}.normalize().negate();
Ahora, si proporcionamos las funciones begin () y end (), podríamos usar nuestro Vector en un bucle for de nuevo estilo, digamos para recorrer las 3 coordenadas x, y y z (sin duda puede construir más ejemplos "útiles" reemplazando Vector con, por ejemplo, String):
Vector v = ...;
for (double x : v) { ... }
Incluso podemos hacer:
Vector v = ...;
for (double x : v.normalize().negate()) { ... }
y también:
for (double x : Vector{1., 2., 3.}) { ... }
Sin embargo, lo siguiente (me parece a mí) está roto:
for (double x : Vector{1., 2., 3.}.normalize()) { ... }
Si bien parece una combinación lógica de los dos usos anteriores, creo que este último uso crea una referencia colgante, mientras que los dos anteriores están completamente bien.
- ¿Es esto correcto y ampliamente apreciado?
- ¿Qué parte de lo anterior es la parte "mala" que debe evitarse?
- ¿Se mejoraría el lenguaje cambiando la definición del bucle for basado en el rango de modo que los temporales construidos en la expresión for existan durante la duración del bucle?
Respuestas:
Sí, tu comprensión de las cosas es correcta.
La parte mala es tomar una referencia de valor l a un valor temporal devuelto por una función y vincularla a una referencia de valor r. Es tan malo como esto:
El
Vector{1., 2., 3.}
tiempo de vida del temporal no se puede extender porque el compilador no tiene idea de que el valor de retorno de lo hacenormalize
referencia.Eso sería muy inconsistente con el funcionamiento de C ++.
¿Evitaría ciertos errores cometidos por personas que usan expresiones encadenadas en temporales o varios métodos de evaluación perezosa para expresiones? Si. Pero también requeriría un código de compilador de casos especiales, además de ser confuso en cuanto a por qué no funciona con otras construcciones de expresión.
Una solución mucho más razonable sería alguna forma de informar al compilador que el valor de retorno de una función siempre es una referencia a
this
, y por lo tanto, si el valor de retorno está vinculado a una construcción de extensión temporal, entonces extenderá el temporal correcto. Sin embargo, esa es una solución a nivel de idioma.Actualmente (si el compilador lo admite), puede hacer que
normalize
no se pueda llamar de forma temporal:Esto provocará
Vector{1., 2., 3.}.normalize()
un error de compilación, mientrasv.normalize()
que funcionará bien. Obviamente, no podrá hacer cosas correctas como esta:Pero tampoco podrá hacer cosas incorrectas.
Alternativamente, como se sugiere en los comentarios, puede hacer que la versión de referencia rvalue devuelva un valor en lugar de una referencia:
Si
Vector
fuera un tipo con recursos reales para moverse, podría usarVector ret = std::move(*this);
en su lugar. La optimización del valor de retorno con nombre hace que esto sea razonablemente óptimo en términos de rendimiento.fuente
delete
proporcionar una operación alternativa que devuelva un rvalue:Vector normalize() && { normalize(); return std::move(*this); }
(creo que la llamada alnormalize
interior de la función se enviará a la sobrecarga de lvalue, pero alguien debería verificarlo :)&
/&&
calificación de métodos. ¿Es esto de C ++ 11 o es una extensión de compilador propietaria (tal vez generalizada)? Da interesantes posibilidades.Eso no es una limitación del idioma, sino un problema con su código. La expresión
Vector{1., 2., 3.}
crea un temporal, pero lanormalize
función devuelve un lvalue-reference . Debido a que la expresión es un valor l , el compilador asume que el objeto estará vivo, pero debido a que es una referencia a un temporal, el objeto muere después de que se evalúa la expresión completa, por lo que queda una referencia pendiente.Ahora, si cambia su diseño para devolver un nuevo objeto por valor en lugar de una referencia al objeto actual, entonces no habría ningún problema y el código funcionaría como se esperaba.
fuente
const
referencia ampliaría la vida útil del objeto en este caso?normalize()
una función mutante en un objeto existente. De ahí la pregunta. Que un temporal tenga una "vida útil prolongada" cuando se usa para el propósito específico de una iteración, y no de otra manera, es un error confuso.const&
) tiene su vida extendida.Vector & r = Vector{1.,2.,3.}.normalize();
. Su diseño tiene esa limitación, y eso significa que está dispuesto a devolver por valor (lo que podría tener sentido en muchas circunstancias, y más aún con rvalue-reference y move ), o bien debe manejar el problema en el lugar de llamada: crea una variable adecuada, luego úsala en el ciclo for. También tenga en cuenta que la expresiónVector v = Vector{1., 2., 3.}.normalize().negate();
crea dos objetos ...T const& f(T const&);
está completamente bien.T const& t = f(T());
está completamente bien. Y luego, en otra TU descubres esoT const& f(T const& t) { return t; }
y lloras ... Sioperator+
opera sobre valores, es más seguro ; entonces el compilador puede optimizar la copia (¿Quiere velocidad? Pasar por valores), pero eso es una ventaja. El único enlace de temporales que permitiría es el enlace a referencias de valores r, pero las funciones deberían devolver valores por seguridad y depender de Copiar elisión / Movimiento de semántica.En mi humilde opinión, el segundo ejemplo ya es defectuoso. Que los operadores modificadores regresen
*this
es conveniente en la forma que mencionaste: permite el encadenamiento de modificadores. Se puede usar simplemente para transmitir el resultado de la modificación, pero hacer esto es propenso a errores porque puede pasarse por alto fácilmente. Si veo algo comoNo sospecharía automáticamente que las funciones se modifican
v
como efecto secundario. Por supuesto que podrían , pero sería confuso. Entonces, si tuviera que escribir algo como esto, me aseguraría de que sev
mantenga constante. Para su ejemplo, agregaría funciones gratuitasy luego escribe los bucles
y
Eso es IMO mejor legible y es más seguro. Por supuesto, requiere una copia adicional, sin embargo, para los datos asignados al montón, esto probablemente podría hacerse en una operación de movimiento de C ++ 11 barata.
fuente