Pregunta ¿Por qué al mover una variable de puntero no se establece en nulo?


Al implementar constructores de movimiento y mover operadores de asignación, a menudo se escribe un código como este:

p = other.p;
other.p = 0;

Las operaciones de movimiento implícitamente definidas se implementarían con un código como este:

p = std::move(other.p);

Lo cual sería incorrecto, porque mover una variable de puntero no no establecerlo en nulo. ¿Porqué es eso? ¿Hay algún caso en el que quisiéramos que las operaciones de movimiento dejen intacta la variable del puntero original?

Nota: Al "mover", lo hago no solo quiero decir la subexpresión std::move(other.p)Me refiero a toda la expresión p = std::move(other.p). Entonces, ¿por qué no hay una regla de lenguaje especial que diga "Si el lado derecho de una asignación es un valor x del puntero, se establece en nulo después de que se haya realizado la asignación"?


32
2018-02-26 10:55


origen


Respuestas:


Establecer un puntero sin formato a nulo después de moverlo implica que el puntero representa la propiedad. Sin embargo, muchos punteros se usan para representar relaciones. Además, durante mucho tiempo, se recomienda que las relaciones de propiedad se representen de forma diferente a utilizar un puntero sin formato. Por ejemplo, la relación de propiedad a la que se refiere está representada por std::unique_ptr<T>. Si desea que las operaciones de movimiento generadas implícitamente se ocupen de su propiedad, todo lo que necesita hacer es usar miembros que realmente representen (e implementen) el comportamiento de propiedad deseado.

Además, el comportamiento de las operaciones de movimientos generados es coherente con lo que se hizo con las operaciones de copia: tampoco hacen suposiciones de propiedad y no lo hacen, p. una copia profunda si se copia un puntero. Si desea que esto suceda, también necesita crear una clase adecuada que codifique la semántica pertinente.


28
2018-02-26 13:08



Mover hace que el objeto movido sea "inválido". Lo hace no establecerlo automáticamente en un estado seguro "vacío". De acuerdo con el principio de larga data de C ++ de "no pague por lo que no usa", ese es su trabajo si lo desea.


5
2018-02-26 14:46



Creo que la respuesta es: la implementación de un comportamiento así es bastante trivial y, por lo tanto, el estándar no sintió ninguna necesidad de imponer ninguna regla sobre el compilador. El lenguaje C ++ es enorme y no todo puede imaginarse antes de su uso. Tomemos, por ejemplo, la plantilla de C ++. No fue diseñado por primera vez para ser utilizado tal como se lo utiliza hoy en día (es decir, su capacidad de metaprogramación). Entonces, creo que el estándar simplemente da libertad y no hizo ninguna regla específica para std::move(other.p), siguiendo uno de ellos es el principio de diseño: "No pagas por lo que no usas".

A pesar de que, std::unique_ptr es movible, aunque no se puede copiar. Entonces, si quiere puntero-semántico que sea movible y que pueda copiarse, entonces aquí hay una implementación trivial:

template<typename T>
struct movable_ptr
{
    T *pointer;
    movable_ptr(T *ptr=0) : pointer(ptr) {} 
    movable_ptr<T>& operator=(T *ptr) { pointer = ptr; return *this; }
    movable_ptr(movable_ptr<T> && other) 
    {
        pointer = other.pointer;
        other.pointer = 0;
    }
    movable_ptr<T>& operator=(movable_ptr<T> && other) 
    {
        pointer = other.pointer;
        other.pointer = 0;
        return *this;
    } 
    T* operator->() const { return pointer; }
    T& operator*() const { return *pointer; }

    movable_ptr(movable_ptr<T> const & other) = default;
    movable_ptr<T> & operator=(movable_ptr<T> const & other) = default;
};

Ahora puede escribir clases, sin escribir su propia semántica de movimientos:

struct T
{
   movable_ptr<A> aptr;
   movable_ptr<B> bptr;
   //...

   //and now you could simply say
   T(T&&) = default; 
   T& operator=(T&&) = default; 
};

Tenga en cuenta que todavía tiene que escribir la semántica de copia y el destructor, como movable_ptr es no puntero inteligente.


4
2018-02-26 11:01



Por ejemplo, si tiene un puntero a un objeto compartido. Recuerde que, después de mover un objeto, debe permanecer en un estado interno consistente, por lo que establecer un puntero que no debe ser nulo a un valor nulo no es correcto.

Es decir.:

struct foo
{
  bar*  shared_factory;  // This can be the same for several 'foo's
                         // and must never null.
};

Editar

Aquí hay una cita sobre MoveConstructibe del estándar:

T u = rv;
...
rv’s state is unspecified [ Note:rv must still meet the requirements
of the library component that is using it. The operations listed in
those requirements must work as specified whether rv has been moved
from or not.

0
2018-02-26 11:02