Qué dura después de usar std :: move c ++ 11

Después de usar std :: move en una variable que podría ser un campo en una clase como:

class A { public: vector&& stealVector() { return std::move(myVector); } void recreateMyVector() { } private: vector myVector; }; 

¿Cómo recrearé el vector, como uno claro? ¿Qué queda en myVector después del std :: move?

El mantra común es que una variable que ha sido “movida desde” está en un estado válido, pero no especificado. Eso significa que es posible destruir y asignar a la variable, pero nada más.

( Stepanov llama a esto “parcialmente formado”, creo, que es un buen término).


Para ser claros, esta no es una regla estricta; más bien, es una guía sobre cómo pensar sobre el movimiento: después de moverse de algo, no debería querer usar el objeto original más. Cualquier bash de hacer algo no trivial con el objeto original (aparte de asignarlo o destruirlo) debe pensarse y justificarse cuidadosamente.

Sin embargo, en cada caso particular, puede haber operaciones adicionales que tengan sentido en un objeto movido, y es posible que desee aprovecharlas. Por ejemplo:

  • Los contenedores de la biblioteca estándar describen las condiciones previas para sus operaciones; las operaciones sin precondiciones están bien. Los únicos útiles que vienen a la mente son clear() , y quizás swap() (pero prefieren la asignación en lugar de intercambiar). Hay otras operaciones sin condiciones previas, como size() , pero siguiendo el razonamiento anterior, no debería haber ningún negocio que pregunte por el tamaño de un objeto que acaba de decir que ya no deseaba.

  • El unique_ptr garantiza que después de ser movido desde, es nulo, que puede explotar en una situación donde la propiedad se toma condicionalmente :

     std::unique_ptr resource(new T); std::vector &)> handlers = /* ... */; for (auto const & f : handlers) { int result = f(resource); if (!resource) { return result; } } 

    Un controlador se ve así:

     int foo_handler(std::unique_ptr & p) { if (some_condition)) { another_container.remember(std::move(p)); return another_container.state(); } return 0; } 

    Hubiera sido posible genéricamente que el manejador devuelva algún otro tipo de estado que indique si tomó posesión del puntero único, pero dado que el estándar en realidad garantiza que el movimiento desde un puntero único lo deja como nulo, podemos explotar eso para transmitir esa información en el puntero único en sí.

Mueva el vector miembro a un vector local, borre el miembro, devuelva el local por valor.

 std::vector stealVector() { auto ret = std::move(myVector); myVector.clear(); return ret; } 

¿Qué queda en myVector después del std :: move?

std::move no se mueve, es solo un yeso. Puede suceder que myVector esté intacto después de la llamada a stealVector() ; vea la salida del primer a.show() en el código de ejemplo a continuación. (Sí, es un código tonto pero válido.)

Si las entrañas de myVector son realmente robadas (ver b = a.stealVector(); en el código de ejemplo), estará en un estado válido pero no especificado. Sin embargo, debe ser asignable y destructible; en el caso de std::vector , también puede llamar a clear() y swap() . Realmente no debería hacer ninguna otra suposición con respecto al estado del vector.

¿Cómo recrearé el vector, como uno claro?

Una opción es simplemente llamar a clear() en él. Entonces sabes su estado seguro.


El código de ejemplo:

 #include  #include  #include  #include  using namespace std; class A { public: A(initializer_list il) : myVector(il) { } void show() { if (myVector.empty()) cout << "(empty)"; for (const string& s : myVector) cout << s << " "; cout << endl; } vector&& stealVector() { return std::move(myVector); } private: vector myVector; }; int main() { A a({"a", "b", "c"}); a.stealVector(); a.show(); vector b{"1", "2", "3"}; b = a.stealVector(); a.show(); } 

Esto imprime los siguientes en mi máquina:

a B C
(vacío)

Como creo que Stepanov ha sido tergiversado en las respuestas hasta ahora, permítanme agregar una breve descripción general de la mía:

Para los tipos estándar (y solo aquellos), el estándar especifica que un objeto movido se deja en el famoso estado “válido, pero no especificado”. En particular, ninguno de los tipos std usa el estado parcialmente formado de Stepanov, que algunos, incluido yo, consideramos un error.

Para sus propios tipos, debe esforzarse tanto por el constructor predeterminado como por el objeto fuente de un movimiento para establecer el estado parcialmente formado, que Stepanov definió en Elementos de progtwigción (2009) como un estado en el que las únicas operaciones válidas son destrucción y asignación de un nuevo valor. En particular, el Estado parcialmente formado no necesita representar un valor válido del objeto ni tampoco debe adherirse a invariantes de clase normales.

Contrariamente a la creencia popular, esto no es nada nuevo. El estado parcialmente formado existe desde los albores de C / C ++:

 int i; // i is Partially-Formed: only going out of scope and // assignment are allowed, and compilers understand this! 

Lo que esto significa prácticamente para el usuario es nunca asumir que puede hacer más con un objeto movido que destruirlo o asignarle un nuevo valor, a menos que, por supuesto, la documentación indique que puede hacer más, lo que generalmente es posible. para contenedores, que a menudo pueden, naturalmente y de manera eficiente, establecer el estado vacío.

Para los autores de la clase, significa que tiene dos opciones:

Primero, evita el estado parcialmente formado como lo hace el STL. Pero para una clase con Remote State, por ejemplo, una clase pimpl’ed, esto significa que para representar un valor válido, o bien acepta nullptr como un valor válido para pImpl , lo que le solicita que defina, en el nivel API público, qué pImpl nullptr medios, incl. verificando nullptr en todas las funciones de miembros.

O necesita asignar un nuevo pImpl para el objeto movido desde (y construido por defecto), que, por supuesto, no es nada que haría ningún progtwigdor C ++ consciente del rendimiento. Sin embargo, a un progtwigdor de C ++ consciente de la performance tampoco le gustaría ensuciar su código con cheques nullptr solo para soportar el uso menor de un uso no trivial de un objeto movido desde.

Lo que nos lleva a la segunda alternativa: adoptar el estado parcialmente formado. Eso significa que acepta nullptr pImpl , pero solo para objetos construidos y movidos por defecto. Un nullptr pImpl representa el estado parcialmente formado, en el que solo se permiten la destrucción y la asignación de otro valor. Esto significa que solo el dtor y los operadores de asignación deben poder tratar con un nullptr pImpl , mientras que todos los demás miembros pueden asumir un pImpl válido. Esto tiene otro beneficio: tanto su ctor predeterminado como los operadores de movimiento pueden ser no noexcept , lo cual es importante para su uso en std::vector (por lo tanto, los movimientos y no las copias se utilizan al reasignarlos).

Ejemplo de clase de Pen :

 class Pen { struct Private; Private *pImpl = nullptr; public: Pen() noexcept = default; Pen(Pen &&other) noexcept : pImpl{std::exchange(other.pImpl, {})} {} Pen(const Pen &other) : pImpl{new Private{*other.pImpl}} {} // assumes valid `other` Pen &operator=(Pen &&other) noexcept { Pen(std::move(other)).swap(*this); return *this; } Pen &operator=(const Pen &other) { Pen(other).swap(*this); return *this; } void swap(Pen &other) noexcept { using std::swap; swap(pImpl, other.pImpl); } int width() const { return pImpl->width; } // ... };