¿Qué constituye un estado válido para un objeto "movido desde" en C++ 11?
c++11 move-semantics (2)
He estado tratando de entender cómo se supone que la semántica de movimiento en C ++ 11 funciona, y estoy teniendo problemas para entender qué condiciones debe satisfacer un objeto que se haya movido. Ver la respuesta aquí en realidad no resuelve mi pregunta, porque no puedo ver cómo aplicarla a los objetos pimpl de una manera sensata, a pesar de los argumentos que mueven la semántica son perfectos para los chulos .
La ilustración más fácil de mi problema involucra el modismo pimpl, así:
class Foo {
std::unique_ptr<FooImpl> impl_;
public:
// Inlining FooImpl''s constructors for brevity''s sake; otherwise it
// defeats the point.
Foo() : impl_(new FooImpl()) {}
Foo(const Foo & rhs) : impl_(new FooImpl(*rhs.impl_)) {}
Foo(Foo && rhs) : impl_(std::move(rhs.impl_)) {}
Foo & operator=(Foo rhs)
{
std::swap(impl_, rhs.impl_);
return *this;
}
void do_stuff ()
{
impl_->do_stuff;
}
};
Ahora, ¿qué puedo hacer una vez que me haya mudado de un Foo
? Puedo destruir el objeto movido de forma segura, y puedo asignarlo, los cuales son absolutamente cruciales. Sin embargo, si trato de do_stuff
con mi Foo
, explotará. Antes de agregar la semántica de movimiento para mi definición de Foo
, cada Foo
satisfacía la invariante que podía do_stuff
, y ese ya no es el caso. Tampoco parece haber muchas alternativas excelentes, ya que (por ejemplo) colocar el Foo
movido implicaría una nueva asignación dinámica, que en parte derrota el propósito de la semántica del movimiento. Podría verificar si impl_
en do_stuff
e inicializarlo a un FooImpl
predeterminado, si es así, pero eso agrega una verificación (generalmente espuria), y si tengo muchos métodos, significaría recordar hacer el control en cada uno.
¿Debería renunciar a la idea de que ser capaz de hacer un do_stuff
es una invariante razonable?
Sin embargo, si trato de hacer_stuff con mi Foo, explotará.
Sí. Entonces esto:
vector<int> first = {3, 5, 6};
vector<int> second = std::move(first);
first.size(); //Value returned is undefined. May be 0, may not
La regla que usa el estándar es dejar el objeto en un valor válido (lo que significa que el objeto funciona) pero no especificado . Esto significa que las únicas funciones que puede llamar son aquellas que no tienen condiciones en el estado actual del objeto. Para vector
, puede usar sus operadores de asignación copiar / mover, así como también clear
y empty
, y varias otras operaciones. Entonces puedes hacer esto:
vector<int> first = {3, 5, 6};
vector<int> second = std::move(first);
first.clear(); //Cause the vector to become empty.
first.size(); //Now the value is guaranteed to be 0.
Para su caso, la tarea copiar / mover (desde cualquier lado) debería seguir funcionando, al igual que el destructor. Pero todas las demás funciones tuyas tienen una precondición basada en el estado de no haberse movido.
Entonces no veo tu problema.
Si quería asegurarse de que ninguna instancia de una clase Pimpl''d pudiera estar vacía, entonces implementaría una semántica de copia adecuada y prohibiría el movimiento. El movimiento requiere la posibilidad de que un objeto esté en un estado vacío.
Define y documenta para sus tipos qué estado ''válido'' es y qué operación se puede realizar en objetos movidos desde sus tipos.
Mover un objeto de un tipo de biblioteca estándar coloca el objeto en un estado no especificado, que se puede consultar de forma normal para determinar operaciones válidas.
17.6.5.15 Estado movido desde los tipos de biblioteca [lib.types.movedfrom]
Los objetos de tipos definidos en la biblioteca estándar de C ++ se pueden mover desde (12.8). Las operaciones de movimiento pueden especificarse explícitamente o generarse implícitamente. A menos que se especifique lo contrario, dichos objetos movidos se colocarán en un estado válido pero no especificado.
El objeto está en estado ''válido'' y significa que todos los requisitos que el estándar especifica para el tipo aún son verdaderos. Eso significa que puede usar cualquier operación en un tipo de biblioteca estándar movido, para el cual las condiciones previas son ciertas.
Normalmente, se conoce el estado de un objeto, por lo que no es necesario verificar si cumple con las condiciones previas para cada operación que desee realizar. La única diferencia con los objetos movidos es que usted no conoce el estado, por lo que debe verificarlo. Por ejemplo, no debe pop_back () en una cadena movida desde hasta que haya consultado el estado de la cadena para determinar que se cumplan las condiciones previas de pop_back ().
std::string s = "foo";
std::string t(std::move(s));
if (!s.empty()) // empty has no preconditions, so it''s safe to call on moved-from objects
s.pop_back(); // after verifying that the preconditions are met, pop_back is safe to call on moved-from objects
El estado probablemente no se especifique porque sería oneroso crear un solo conjunto útil de requisitos para todas las diferentes implementaciones de la biblioteca estándar.
Como usted es responsable no solo de las especificaciones sino también de la implementación de sus tipos, puede simplemente especificar el estado y evitar la necesidad de realizar consultas. Por ejemplo, sería perfectamente razonable especificar que al pasar de su objeto tipo pimpl provoca que do_stuff se convierta en una operación no válida con un comportamiento indefinido (a través de la desreferenciación de un puntero nulo). El lenguaje está diseñado de tal forma que el movimiento solo ocurre cuando no es posible hacer algo con el objeto movido, o cuando el usuario ha indicado una operación de movimiento muy obviamente y muy explícitamente, por lo que el usuario nunca debería sorprenderse con un movimiento. del objeto.
También tenga en cuenta que los ''conceptos'' definidos por la biblioteca estándar no hacen ningún permiso para los objetos que se han movido. Esto significa que para cumplir con los requisitos de cualquiera de los conceptos definidos por la biblioteca estándar, los objetos movidos desde sus tipos aún deben cumplir con los requisitos del concepto. Esto significa que si los objetos de su tipo no permanecen en un estado válido (según lo define el concepto relevante), entonces no puede usarlo con la biblioteca estándar (o el resultado es un comportamiento indefinido).