c++ - sentencia - programacion ats while
¿Es esta una trampa conocida de C++ 11 para bucles? (3)
¿Es esto correcto y ampliamente apreciado?
Sí, tu comprensión de las cosas es correcta.
¿Qué parte de lo anterior es la parte "mala" que debería evitarse?
La parte mala es tomar una referencia de l valor a un temporal devuelto por una función y vincularlo a una referencia de valor r. Es tan malo como esto:
auto &&t = Vector{1., 2., 3.}.normalize();
La vida útil temporal de Vector{1., 2., 3.}
no se puede extender porque el compilador no tiene idea de que el valor de retorno de normalize
referencia.
¿Se mejoraría el lenguaje al cambiar la definición del bucle for para que las temporarias construidas en for-expression existieran durante la duración del bucle?
Eso sería altamente inconsistente con el funcionamiento de C ++.
¿Impediría ciertos intentos de gente que usa expresiones encadenadas en temporarios o varios métodos de evaluación diferida para expresiones? Sí. Pero también requeriría un código de compilación de casos especiales, y sería confuso en cuanto a por qué no funciona con otras construcciones de expresión.
Una solución mucho más razonable sería de alguna manera 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á ligado a una construcción de extensión temporal, entonces se extendería la temporal correcta. Sin embargo, es una solución de nivel de idioma.
Actualmente (si el compilador lo admite), puede hacerlo para que no se pueda normalize
en un temporal:
struct Vector {
double x, y, z;
// ...
Vector &normalize() & {
double s = 1./sqrt(x*x+y*y+z*z);
x *= s; y *= s; z *= s;
return *this;
}
Vector &normalize() && = delete;
};
Esto causará Vector{1., 2., 3.}.normalize()
para dar un error de compilación, mientras que v.normalize()
funcionará bien. Obviamente, no podrás hacer cosas correctas como esta:
Vector t = Vector{1., 2., 3.}.normalize();
Pero tampoco podrás 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:
struct Vector {
double x, y, z;
// ...
Vector &normalize() & {
double s = 1./sqrt(x*x+y*y+z*z);
x *= s; y *= s; z *= s;
return *this;
}
Vector normalize() && {
Vector ret = *this;
ret.normalize();
return ret;
}
};
Si Vector
era un tipo con recursos reales para mover, puede usar Vector ret = std::move(*this);
en lugar. La optimización de valor de retorno hace que esto sea razonablemente óptimo en términos de rendimiento.
Imaginemos que tenemos una estructura para mantener 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 con que hay un código similar disponible. Los métodos le permiten encadenar cómodamente, por ejemplo:
Vector v = ...;
v.normalize().negate();
O incluso:
Vector v = Vector{1., 2., 3.}.normalize().negate();
Ahora bien, si proporcionáramos las funciones de inicio () y fin (), podríamos usar nuestro Vector en un nuevo ciclo for, digamos recorrer las 3 coordenadas x, y, y z (sin duda puede construir ejemplos más "útiles") reemplazando Vector 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, el siguiente (me parece) 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 oscilante, mientras que los dos anteriores son completamente buenos.
- ¿Es esto correcto y ampliamente apreciado?
- ¿Qué parte de lo anterior es la parte "mala" que debería evitarse?
- ¿Se mejoraría el lenguaje al cambiar la definición del bucle for para que las temporarias construidas en for-expression existieran durante la duración del bucle?
para (doble x: Vector {1., 2., 3.}. normalize ()) {...}
Eso no es una limitación del lenguaje, sino un problema con tu código. La expresión Vector{1., 2., 3.}
crea una función temporal, pero la función normalize
devuelve una referencia-valor . Como la expresión es un valor l , el compilador supone que el objeto estará activo, pero dado que es una referencia a un elemento temporal, el objeto muere una vez que se evalúa la expresión completa, por lo que se queda con una referencia colgante.
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 problemas y el código funcionaría como se esperaba.
En mi humilde opinión, el segundo ejemplo ya está defectuoso. Que los operadores modificadores devuelven *this
es conveniente en la forma en que usted lo mencionó: permite el encadenamiento de modificadores. Se puede usar simplemente para entregar el resultado de la modificación, pero hacerlo es propenso a errores porque puede pasarse por alto. Si veo algo así como
Vector v{1., 2., 3.};
auto foo = somefunction1(v, 17);
auto bar = somefunction2(true, v, 2, foo);
auto baz = somefunction3(bar.quun(v), 93.2, v.qwarv(foo));
No sospecharía automáticamente que las funciones modifican v
como un efecto secundario. Por supuesto, podrían , pero sería confuso. Entonces, si tuviera que escribir algo como esto, me aseguraría de que v
permanezca constante. Para su ejemplo, agregaría funciones gratuitas
auto normalized(Vector v) -> Vector {return v.normalize();}
auto negated(Vector v) -> Vector {return v.negate();}
y luego escribe los bucles
for( double x : negated(normalized(v)) ) { ... }
y
for( double x : normalized(Vector{1., 2., 3}) ) { ... }
Eso es IMO mejor legible, y es más seguro. Por supuesto, requiere una copia adicional, sin embargo, para los datos asignados en el montón esto podría hacerse en una operación de movimiento de C ++ 11 barata.