c++ - objeto - new int en c
(¿Por qué) un constructor de movimientos o un operador de asignaciones debe borrar su argumento? (7)
¿Quién define qué significa un estado indeterminado?
El autor de la clase.
Si mira la documentación de std::unique_ptr
, verá que después de las siguientes líneas:
std::unique_ptr<int> pi = std::make_unique<int>(5);
auto pi2 = std::move(pi);
pi
está realmente en un estado muy definido. Se habrá reset()
y pi.get()
será igual a nullptr
.
Un ejemplo de implementación del constructor de movimiento de un curso de C ++ que estoy tomando se parece a esto:
/// Move constructor
Motorcycle::Motorcycle(Motorcycle&& ori)
: m_wheels(std::move(ori.m_wheels)),
m_speed(ori.m_speed),
m_direction(ori.m_direction)
{
ori.m_wheels = array<Wheel, 2>();
ori.m_speed = 0.0;
ori.m_direction = 0.0;
}
( m_wheels
es un miembro del tipo std::array<Wheel, 2>
, y Wheel solo contiene una double m_speed
y un bool m_rotating
. En la clase Motorcycle , m_speed
y m_direction
también son double
s.)
No entiendo por qué los valores de ori
deben ser aclarados.
Si una Motorcycle
tuviera algún puntero, queríamos "robar", entonces seguro, tendríamos que configurar ori.m_thingy = nullptr
para no, por ejemplo, delete m_thingy
dos veces. ¿Pero importa cuando los campos contienen los objetos?
Le pregunté a un amigo sobre esto, y me señalaron hacia esta página , que dice:
Los constructores de movimiento típicamente "roban" los recursos mantenidos por el argumento (por ejemplo, punteros a objetos asignados dinámicamente, descriptores de archivos, sockets TCP, flujos de E / S, hilos en ejecución, etc.), en lugar de hacer copias de ellos, y dejan el argumento en algún estado válido pero por lo demás indeterminado . Por ejemplo, pasar de
std::string
o destd::vector
puede hacer que el argumento quede vacío. Sin embargo, no se debe confiar en este comportamiento.
¿Quién define qué significa un estado indeterminado ? No veo cómo establecer la velocidad en 0.0
es más indeterminado que dejarlo. Y, como dice la última frase de la cita, el código no debe depender del estado de la motocicleta trasladada de todos modos, ¿por qué molestarse en limpiarlo?
¿Quién define qué significa un estado indeterminado?
El idioma inglés, creo. "Indeterminado" tiene aquí uno de sus significados usuales en inglés: "No determinado, no fijado con precisión en extensión, indefinido, incierto. No establecido. No resuelto o decidido" . El estado de un objeto movido no está restringido por el estándar, salvo que debe ser "válido" para ese tipo de objeto. No tiene que ser lo mismo cada vez que se mueve un objeto.
Si solo estuviéramos hablando de tipos proporcionados por la implementación, entonces el lenguaje apropiado sería un "estado válido pero no especificado". Pero reservamos la palabra "no especificado" para hablar sobre los detalles de la implementación de C ++, no detalles de lo que el código de usuario puede hacer.
"Válido" se define por separado para cada tipo. Tomando los tipos enteros como un ejemplo, las representaciones de trampas no son válidas y cualquier cosa que represente un valor es válida.
Ese estándar aquí no dice que el constructor de movimientos debe hacer que el objeto sea indeterminado, simplemente que no necesita ponerlo en un estado determinado. Entonces, aunque estás en lo cierto de que 0 no es "más indeterminado" que el valor anterior, en cualquier caso es discutible, ya que el constructor de movimientos no necesita hacer que el objeto viejo "sea lo más indeterminado posible".
En este caso, el autor de la clase ha elegido colocar el antiguo objeto en un estado específico. Depende de ellos si documentan cuál es ese estado, y si lo hacen, depende totalmente de los usuarios del código si confían en él.
Por lo general, recomendaría no confiar en él, porque en ciertas circunstancias el código que escribe pensando que es semánticamente un movimiento, realmente hace una copia. Por ejemplo, pones std::move
en el lado derecho de una tarea sin importar si el objeto es const
o no porque funciona de cualquier manera, y luego viene alguien más y piensa "ah, se ha movido desde," debe haber sido borrado a cero ". Nop. Han pasado por alto que la Motorcycle
se borra cuando se trasladó, pero const Motorcycle
por supuesto, no lo es, sin importar lo que la documentación pueda sugerirles :-)
Si vas a establecer un estado, entonces realmente es una moneda tirar qué estado. Puede establecerlo en un estado "claro", tal vez coincida con lo que hace el constructor no-args (si hay uno), sobre la base de que esto representa el valor más neutral que existe. Y supongo que en muchas arquitecturas 0 es el valor (tal vez conjunto) más barato para establecer algo. Alternativamente, podría establecerlo en un valor llamativo, con la esperanza de que cuando alguien escriba un error donde accidentalmente se mueva de un objeto y luego use su valor, ellos piensen "¿Qué? ¿La velocidad de la luz? ¿Calle residencial? Ah, sí, recuerdo, ese es el valor que esta clase establece cuando se trasladó, probablemente hice eso ".
El objeto fuente es un valor r, potencialmente un xvalor. Entonces, lo que debe preocuparnos es la destrucción inminente de ese objeto.
Los identificadores de recursos o punteros son el elemento más significativo que distingue un movimiento de una copia: después de la operación se supone que se transfiere la propiedad de ese identificador.
Entonces, obviamente, como mencionas en la pregunta, durante un movimiento necesitamos afectar el objeto fuente para que ya no se identifique como propietario de los objetos transferidos.
Thing::Thing(Thing&& rhs)
: unqptr_(move(rhs.unqptr_))
, rawptr_(rhs.rawptr_)
, ownsraw_(rhs.ownsraw_)
{
the.ownsraw_ = false;
}
Tenga en cuenta que no rawptr_
.
Aquí se toma una decisión de diseño que, siempre y cuando el indicador de propiedad solo sea cierto para un objeto, no nos importa si hay un puntero colgante.
Sin embargo, otro ingeniero puede decidir que el puntero se borre para que en lugar de ub al azar, el siguiente error genere un acceso nullptr:
void MyClass::f(Thing&& t) {
t_ = move(t);
// ...
cout << t;
}
En el caso de algo tan inocuo como las variables que se muestran en la pregunta, es posible que no necesiten borrarse, pero eso depende del diseño de la clase.
Considerar:
class Motorcycle
{
float speed_ = 0.;
static size_t s_inMotion = 0;
public:
Motorcycle() = default;
Motorcycle(Motorcycle&& rhs);
Motorcycle& operator=(Motorcycle&& rhs);
void setSpeed(float speed)
{
if (speed && !speed_)
s_inMotion++;
else if (!speed && speed_)
s_inMotion--;
speed_ = speed;
}
~Motorcycle()
{
setSpeed(0);
}
};
Esta es una demostración bastante artificial de que la propiedad no es necesariamente un simple puntero o bool, sino que puede ser una cuestión de consistencia interna.
El operador de movimiento podría usar setSpeed
para rellenarse, o simplemente podría hacer
Motorcycle::Motorcycle(Motorcycle&& rhs)
: speed_(rhs.speed_)
{
rhs.speed_ = 0; // without this, s_inMotion gets confused
}
(Disculpas por errores tipográficos o autocorrectos, escritos en mi teléfono)
Hay pocas cosas que deben ser factibles de forma segura con un objeto movido desde:
- Destruirlo
- Asignar / Mover a él.
- Cualquier otra operación que lo diga explícitamente.
Por lo tanto, debe pasar de ellos lo más rápido posible mientras da esas dos garantías, y más solo si son útiles y puede cumplirlos de forma gratuita.
Lo que a menudo significa no borrar la fuente, y otras veces implica hacerlo.
Como una adición, prefiera el incumplimiento explícito de las funciones definidas por el usuario, y las definidas implícitamente sobre ambas, por brevedad y preservar la trivialidad.
No necesitan ser limpiados El diseñador de la clase decidió que sería una buena idea dejar el objeto movido de cero inicializado.
Tenga en cuenta que una situación donde sí tiene sentido es para los objetos que gestionan los recursos que se liberan en el destructor. Por ejemplo, un puntero a la memoria asignada dinámicamente. Dejar el puntero del objeto movido sin modificar significa dos objetos que administran el mismo recurso. Ambos sus destructores intentarían lanzar.
No hay un comportamiento estándar para esto. Al igual que con el puntero, puede usarlos después de eliminarlos. Algunas personas ven que no debería preocuparse y simplemente no reutilizan el objeto, pero el compilador no lo prohibirá.
Aquí hay una entrada de blog (no por mí) sobre esto con una discusión interesante en el comentario:
https://foonathan.github.io/blog/2016/07/23/move-safety.html
y el seguimiento:
https://foonathan.github.io/blog/2016/08/24/move-default-ctor.html
Y aquí con un video chat reciente sobre este tema con argumentos discutiendo esto:
https://www.youtube.com/watch?v=g5RnXRGar1w
Básicamente se trata de tratar un objeto movido como un puntero borrado o hacerlo en una caja fuerte para ser "movido desde el estado"
Probablemente tengas razón en que el autor de esta clase está poniendo operaciones innecesarias en el constructor.
Incluso si m_wheels
fuera un tipo asignado a un montón, como std::vector
, todavía no habría ninguna razón para "borrarlo", ya que ya se está transfiriendo a su propio constructor de movimientos:
: m_wheels(std::move(ori.m_wheels)), // invokes move-constructor of appropriate type
Sin embargo, no ha mostrado lo suficiente de la clase como para permitirnos saber si las operaciones explícitas de "limpieza" son necesarias en este caso particular. Según la respuesta del Deduplicador, se espera que las siguientes funciones se comporten correctamente en el estado "movido desde":
// Destruction
~Motorcycle(); // This is the REALLY important one, of course
// Assignment
operator=(const Motorcycle&);
operator=(Motorcycle&&);
Por lo tanto, debe observar la implementación de cada una de estas funciones para determinar si el constructor de movimientos es correcto.
Si los tres usan la implementación predeterminada (que, por lo que has mostrado, parece razonable), entonces no hay razón para borrar manualmente los objetos que se han movido. Sin embargo, si alguna de estas funciones usa los valores de m_wheels
, m_speed
o m_direction
para determinar cómo comportarse, entonces el move-constructor puede necesitar m_direction
para asegurar el comportamiento correcto.
Tal diseño de clase sería poco elegante y propenso a errores, ya que típicamente no esperaríamos que los valores de las primitivas importen en el estado "movido desde", a menos que las primitivas se utilicen explícitamente como indicadores para indicar si debe producirse limpieza en el destructor
En última instancia, como ejemplo para usar en una clase de C ++, el constructor de movimientos que se muestra no es técnicamente "incorrecto" en el sentido de causar un comportamiento no deseado, pero parece un mal ejemplo porque es probable que cause exactamente la confusión que lo llevó publicar esta pregunta