c++ - ¿Por qué mover std:: opcional no restablecer estado
c++17 optional (4)
A menos que se especifique lo contrario, un objeto movido desde el tipo de clase se deja en un estado válido pero no especificado . No necesariamente un "estado de reinicio", y definitivamente no es "invalidado".
Para los tipos primitivos, mover es lo mismo que copiar, es decir, la fuente no se modifica.
El constructor de movimientos predeterminado para un tipo de clase con miembros primitivos moverá cada miembro, es decir, dejará los miembros primitivos sin cambios; un constructor de movimiento definido por el usuario puede o no "restablecerlo".
Un vector movido desde puede o no tener elementos en él. Esperamos que no, ya que es eficiente, pero no se puede confiar en él.
Es posible que una std::string
movida desde todavía tenga elementos, debido a la optimización de cadena pequeña.
move
en std::optional
realidad está especificado por el estándar (C ++ 17 [opcional.ctor] / 7). Se define como hacer move
en el tipo contenido, si está presente. No convierte un opcional valorado en un opcional sin valor.
Por lo tanto, se espera que su código dé como resultado true true
, y que el valor real contenido en foo
se mantenga igual.
Con respecto a la pregunta de por qué el constructor de movimientos de std::optional
se define de esta manera: No puedo decir con seguridad; pero un optional
no es como un vector con un tamaño máximo de 1. Es más como una variable con una marca de validez pegada. En consecuencia, tiene sentido que mover un optional
sea como mover la variable.
Si mover un optional
deja el antiguo "vacío", entonces a = std::move(b);
invocaría el destructor del objeto gestionado de b
, lo cual sería inesperado (al menos para mí).
Me sorprendió bastante saber que el constructor de movimientos (y la asignación correspondiente) de std::optional
no restablece los movimientos opcionales, como se puede ver en [19.6.3.1/7] que dice "bool (rhs) es sin alterar."
Esto también puede ser visto por el siguiente código:
#include <ios>
#include <iostream>
#include <optional>
#include <utility>
int main() {
std::optional<int> foo{ 0 };
std::optional<int> bar{ std::move(foo) };
std::cout << std::boolalpha
<< foo.has_value() << ''/n'' // true
<< bar.has_value() << ''/n''; // true
}
Esto parece contradecir otras instancias de movimiento en la biblioteca estándar, como con std::vector
donde el contenedor movido generalmente se reinicia de alguna manera (en el caso de vector, se garantiza que estará vacío después) para "invalidar" incluso si Los objetos contenidos dentro de ellos mismos han sido movidos desde ya. ¿Hay alguna razón para este o posible caso de uso que se supone que esta decisión debe respaldar, como quizás intentar imitar el comportamiento de una versión no opcional del mismo tipo?
En una palabra: Performance .
Una de las principales razones motivadoras para que exista la semántica de movimiento en primer lugar es el rendimiento . Por lo tanto, las operaciones especiales mueven la construcción y la asignación de movimientos debe ser lo más rápida posible para todos los tipos.
Con el fin de ayudar a este objetivo, es una práctica estándar que los objetos que se mueven se dejen en un estado válido pero no especificado. Por lo tanto, lo mínimo que debe hacer la construcción / asignación de movimiento optional
es moverse desde el argumento fuente. Especificar la configuración de la fuente para que no tenga un valor después del movimiento es equivalente a decir:
Después de que te muevas, haz un trabajo extra, innecesario.
No importa qué tan pequeño sea ese trabajo extra, no es cero. Algunos (y me atrevo a decir que muchos) los clientes no necesitarán ese trabajo extra, y no deberían tener que pagar por ello. Los clientes que lo necesiten pueden agregar fácilmente x.reset()
después de la mudanza, poniendo el movimiento de optional
en un estado bien especificado.
Lo que dice ese paragraph es que si ese opcional tenía un valor, todavía tiene un valor. Dado que ese valor se ha movido de (al objeto recién construido), podría ser un valor diferente al que tenía antes del movimiento. Esto le permite acceder al objeto opcional movido desde de la misma manera que un objeto no movido desde opcional, por lo que el comportamiento de una T
vs. optional<T>
(cuando contiene un objeto) cuando se accede después del movimiento es el mismo.
Además, el efecto general de un movimiento desde un opcional depende de cómo el tipo T
contenido maneja un movimiento. Otras clases (como vector
) no tienen esta dependencia.
Si bien podría ser razonable esperar que std :: optonal se comporte de manera similar a std :: unique_ptr que restablece el estado del objeto movido, hay razones para no exigir tal comportamiento. Creo que uno de ellos es que std :: opcional de un tipo trivial debería ser un tipo de copia trivial. Como tal, no puede tener un constructor de movimiento no predeterminado y no puede restablecer el indicador has_value.
Tener std :: opcional para un tipo no trivial se comporta de manera diferente al opcional para un tipo trivial es una idea bastante mala.