¿Es esta una trampa conocida de C++11 for loops?


Imaginemos que tenemos una estructura para sostener 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 ingenioso para la simplicidad, pero estoy seguro de que está de acuerdo en que hay 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 nuevo estilo de bucle for, por ejemplo, para hacer un bucle sobre las 3 coordenadas x, y y z (sin duda, puede construir más " útil" ejemplos reemplazando Vector con, por ejemplo, Cadena):

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) 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 ser ¿evitada?
  • Sería el idioma mejorarse cambiando la definición de la gama basada en bucle for, que temporales construidos en la expresión existe para la duración del bucle?
Author: iammilind, 2012-05-15

3 answers

¿Es esto correcto y ampliamente apreciado?

Sí, tu entendimiento de las cosas es correcto.

¿Qué parte de lo anterior es la parte "mala" que debe evitarse?

La parte mala es tomar una referencia de valor l a un valor 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();

El tiempo de vida temporal de Vector{1., 2., 3.} no se puede extender porque el compilador no tiene idea de que return value from normalize hace referencia a él.

Sería el idioma mejorarse cambiando la definición de la gama basada en bucle for, que temporales construidos en la expresión existe para la duración del bucle?

Eso sería muy inconsistente con cómo funciona C++.

¿Evitaría ciertas trampas hechas por personas que usan expresiones encadenadas en temporales o varios métodos de evaluación perezosa para expresiones? Sí. Pero también sería requiere código de compilador de caso especial, así como ser confuso en cuanto a por qué no funciona con otras construcciones de expresiones.

Una solución mucho más razonable sería alguna forma de informar al compilador que el valor de retorno de una función es siempre 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ía el temporal correcto. Sin embargo, esa es una solución a nivel de idioma.

Actualmente (si el compilador soporta it), usted puede hacerlo para que normalize no puede ser llamado 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 hará que Vector{1., 2., 3.}.normalize() dé un error de compilación, mientras que v.normalize() funcionará bien. Obviamente usted no será capaz de 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 real recursos para mover, podría usar Vector 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.

 64
Author: Nicol Bolas,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-09-23 03:29:17

Para (doble x: Vector{1., 2., 3.}.normalizar()) { ... }

Eso no es una limitación del lenguaje, sino un problema con su código. La expresión Vector{1., 2., 3.} crea un temporal, pero la función normalizedevuelve un lvalue-reference. Debido a que la expresión es un lvalue , 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 se queda con un colgando referencia.

Ahora, si cambia su diseño para devolver un nuevo objeto por valor en lugar de una referencia al objeto actual, entonces no habrá ningún problema y el código funcionará como se espera.

 25
Author: David Rodríguez - dribeas,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-05-15 03:24:48

En mi humilde opinión, el segundo ejemplo ya es defectuoso. Que los operadores modificadores devuelvan *this es conveniente en la forma que mencionaste: permite encadenar los modificadores. puede ser utilizado para simplemente transmitir el resultado de la modificación, pero hacer esto es propenso a errores porque puede ser fácilmente pasado por alto. Si veo algo 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, ellospodrían , pero sería confuso. Así que si tuviera que escribir algo como esto, me aseguraría de que v permanece constante. Para su ejemplo, añadiría funciones libres

auto normalized(Vector v) -> Vector {return v.normalize();}
auto negated(Vector v) -> Vector {return v.negate();}

Y luego escribir 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 a montones, esto probablemente se podría hacer en una operación de movimiento de C++11 barata.

 4
Author: leftaroundabout,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-05-16 05:01:12