Mover semántica en C ++ - Mover-devolver variables locales

10

Entiendo que en C ++ 11, cuando devuelve una variable local de una función por valor, el compilador puede tratar esa variable como una referencia de valor r y 'moverla' fuera de la función para devolverla (si RVO / NRVO no sucede en su lugar, por supuesto).

Mi pregunta es, ¿no puede esto romper el código existente?

Considere el siguiente código:

#include <iostream>
#include <string>

struct bar
{
  bar(const std::string& str) : _str(str) {}
  bar(const bar&) = delete;
  bar(bar&& other) : _str(std::move(other._str)) {other._str = "Stolen";}
  void print() {std::cout << _str << std::endl;}

  std::string _str;
};

struct foo
{
  foo(bar& b) : _b(b) {}
  ~foo() {_b.print();}

  bar& _b;
};

bar foobar()
{
  bar b("Hello, World!");
  foo f(b);

  return std::move(b);
}

int main()
{
  foobar();
  return EXIT_SUCCESS;
}

Pensé que sería posible que un destructor de un objeto local haga referencia al objeto que se mueve implícitamente y, por lo tanto, vea inesperadamente un objeto 'vacío'. He intentado poner a prueba esta (ver http://ideone.com/ZURoeT ), pero me dio el resultado 'correcto' sin la explícita std::moveen foobar(). Supongo que se debió a NRVO, pero no intenté reorganizar el código para deshabilitarlo.

¿Estoy en lo cierto en que esta transformación (que causa un movimiento fuera de la función) ocurre implícitamente y podría romper el código existente?

ACTUALIZACIÓN Aquí hay un ejemplo que ilustra de lo que estoy hablando. Los siguientes dos enlaces son para el mismo código. http://ideone.com/4GFIRu - C ++ 03 http://ideone.com/FcL2Xj - C ++ 11

Si nos fijamos en la salida, es diferente.

Entonces, supongo que esta pregunta ahora es: ¿se consideró esto al agregar un movimiento implícito al estándar, y se decidió que estaba bien agregar este cambio importante ya que este tipo de código es bastante raro? También me pregunto si algún compilador advertirá en casos como este ...

Bwmat
fuente
Un movimiento siempre debe dejar el objeto en un estado destructible.
Zan Lynx
Sí, pero esa no es la pregunta. El código anterior a C ++ 11 podría suponer que el valor de una variable local no cambiaría simplemente por devolverlo, por lo que este movimiento implícito podría romper esa suposición.
Bwmat
Eso es lo que traté de dilucidar en mi ejemplo; a través de destructores, puede inspeccionar el estado de (un subconjunto de) las variables locales de una función 'después' de que se ejecute la instrucción return, pero antes de que la función realmente regrese.
Bwmat
Esta es una excelente pregunta con el ejemplo que agregó. Espero que esto obtenga más respuestas de profesionales que puedan dilucidar esto. La única retroalimentación real que puedo dar es: esta es la razón por la cual los objetos generalmente no deberían tener vistas no propietarias de los datos. En realidad, hay muchas formas de escribir código de aspecto inocente que se daña por defecto cuando le da a los objetos vistas que no son de su propiedad (punteros en bruto o referencias). Puedo dar más detalles sobre esto en una respuesta adecuada si lo desea, pero supongo que eso no es lo que realmente quiere escuchar. Y por cierto, ya se sabe que 11 puede romper el código existente, por ejemplo, al definir nuevas palabras clave.
Nir Friedman
Sí, sé que C ++ 11 nunca afirmó no romper ningún código antiguo, pero esto es bastante sutil y sería muy fácil pasarlo por alto (sin errores del compilador, advertencias, segfaults)
Bwmat

Respuestas:

8

Scott Meyers publicó en comp.lang.c ++ (agosto de 2010) sobre un problema en el que la generación implícita de constructores de movimientos podría romper invariantes de clase C ++ 03:

struct X
{
  // invariant: v.size() == 5
  X() : v(5) {}

  ~X() { std::cout << v[0] << std::endl; }

private:    
  std::vector<int> v;
};

int main()
{
    std::vector<X> y;
    y.push_back(X()); // X() rvalue: copied in C++03, moved in C++0x
}

Aquí el problema es que en C ++ 03, Xtenía un invariante que su vmiembro siempre tenía 5 elementos. X::~X()contaba con ese invariante, pero el constructor de movimiento recién introducido se movió v, estableciendo así su longitud en cero.

Esto está relacionado con su ejemplo, ya que el invariante roto solo se detecta en el Xdestructor del s (como usted dice, es posible que un destructor de un objeto local haga referencia al objeto que se mueve implícitamente y, por lo tanto, ve inesperadamente un objeto vacío ).

C ++ 11 intenta lograr un equilibrio entre romper parte del código existente y proporcionar optimizaciones útiles basadas en constructores de movimientos.

El comité decidió inicialmente que los compiladores de movimientos y los operadores de asignación de movimientos deberían ser generados por el compilador cuando el usuario no los proporciona.

Luego decidió que esto era realmente motivo de alarma y restringió la generación automática de constructores de movimiento y operadores de asignación de movimiento de tal manera que es mucho menos probable, aunque no imposible, que se rompa el código existente (por ejemplo, destructor definido explícitamente).

Es tentador pensar que evitar la generación de constructores de movimiento implícito cuando está presente un destructor definido por el usuario es suficiente, pero no es cierto ( N3153 - El movimiento implícito debe ir para más detalles).

En N3174 - Moverse o no moverse Stroupstrup dice:

Considero que es un problema de diseño de lenguaje, en lugar de un simple problema de compatibilidad con versiones anteriores. Es fácil evitar romper el código antiguo (por ejemplo, solo eliminar las operaciones de movimiento de C ++ 0x), pero veo que hacer de C ++ 0x un mejor lenguaje al hacer que las operaciones de movimiento sean un objetivo importante para el que puede valer la pena romper algunos C + +98 código.

manlio
fuente