¿Cuándo hacer un tipo no movible en C ++ 11?


127

Me sorprendió que esto no apareciera en mis resultados de búsqueda, pensé que alguien habría preguntado esto antes, dada la utilidad de la semántica de movimiento en C ++ 11:

¿Cuándo debo (o es una buena idea para mí) hacer una clase no movible en C ++ 11?

(Razones otros de los problemas de compatibilidad con el código existente, que es).


2
boost siempre está un paso por delante: "tipos caros de mover" ( boost.org/doc/libs/1_48_0/doc/html/container/move_emplace.html )
SChepurin

1
Creo que esta es una pregunta muy buena y útil ( +1de mí) con una respuesta muy completa de Herb (o su gemelo, como parece ), así que lo hice una entrada de preguntas frecuentes. Si alguien se opone, solo hágame ping en la sala de estar , entonces esto puede discutirse allí.
sbi

1
Las clases móviles AFAIK todavía pueden estar sujetas a cortes, por lo que tiene sentido prohibir el movimiento (y la copia) de todas las clases base polimórficas (es decir, todas las clases base con funciones virtuales).
Philipp

1
@Mehrdad: Solo digo que "T tiene un constructor de movimientos" y " T x = std::move(anotherT);ser legal" no son equivalentes. La última es una solicitud de movimiento que puede recurrir al copiador en caso de que T no tenga movimiento. Entonces, ¿qué significa exactamente "móvil"?
sellibitze

1
@Mehrdad: Consulte la sección de la biblioteca estándar de C ++ sobre lo que significa "MoveConstructible". Es posible que algunos iteradores no tengan un constructor de movimiento, pero aún así es MoveConstructible. Tenga cuidado con las diferentes definiciones de personas "móviles" que tiene en mente.
sellibitze

Respuestas:


110

La respuesta de hierba (antes de que fuera editado) en realidad le dio un buen ejemplo de un tipo que no debe ser móvil: std::mutex.

El tipo de mutex nativo del sistema operativo (por ejemplo, pthread_mutex_ten plataformas POSIX) podría no ser "invariante de ubicación", lo que significa que la dirección del objeto es parte de su valor. Por ejemplo, el sistema operativo puede mantener una lista de punteros a todos los objetos mutex inicializados. Si std::mutexcontiene un tipo de mutex del sistema operativo nativo como miembro de datos y la dirección del tipo nativo debe permanecer fija (porque el sistema operativo mantiene una lista de punteros a sus mutex), entonces cualquiera de std::mutexlos dos tendría que almacenar el tipo de mutex nativo en el montón para que permaneciera en la misma ubicación cuando se mueve entre std::mutexobjetos o std::mutexno debe moverse. No es posible almacenarlo en el montón, porque a std::mutextiene un constexprconstructor y debe ser elegible para una inicialización constante (es decir, una inicialización estática) para questd::mutexse garantiza que se construirá antes de que comience la ejecución del programa, por lo que su constructor no puede usarlo new. Entonces, la única opción que queda es std::mutexser inamovible.

El mismo razonamiento se aplica a otros tipos que contienen algo que requiere una dirección fija. Si la dirección del recurso debe permanecer fija, ¡no la mueva!

Hay otro argumento para no moverse, std::mutexque es que sería muy difícil hacerlo de manera segura, porque necesitaría saber que nadie está tratando de bloquear el mutex en el momento en que se está moviendo. Dado que los mutexes son uno de los bloques de construcción que puedes usar para prevenir las carreras de datos, ¡sería desafortunado si no estuvieran a salvo contra las propias carreras! Con un inmueble, std::mutexusted sabe que lo único que cualquiera puede hacerle una vez que se ha construido y antes de que se haya destruido es bloquearlo y desbloquearlo, y esas operaciones están explícitamente garantizadas para ser seguras y no introducir carreras de datos. Este mismo argumento se aplica a los std::atomic<T>objetos: a menos que se puedan mover atómicamente, no sería posible moverlos con seguridad, otro hilo podría estar intentando llamarcompare_exchange_strongen el objeto justo en el momento en que se mueve. Por lo tanto, otro caso en el que los tipos no deberían ser móviles es donde son bloques de construcción de bajo nivel de código concurrente seguro y deben garantizar la atomicidad de todas las operaciones en ellos. Si el valor del objeto se pudiera mover a un nuevo objeto en cualquier momento, necesitaría usar una variable atómica para proteger cada variable atómica para que sepa si es seguro usarlo o si se ha movido ... y una variable atómica para proteger esa variable atómica, y así sucesivamente ...

Creo que generalizaría para decir que cuando un objeto es solo un recuerdo puro, no un tipo que actúa como titular de un valor o abstracción de un valor, no tiene sentido moverlo. Tipos fundamentales como intno poder mover: moverlos es solo una copia. No puede extraer las agallas de un int, puede copiar su valor y luego establecerlo en cero, pero sigue intsiendo un valor, solo son bytes de memoria. Pero unint todavía es móvilen los términos del lenguaje porque una copia es una operación de movimiento válida. Sin embargo, para los tipos que no se pueden copiar, si no quiere o no puede mover la pieza de memoria y tampoco puede copiar su valor, entonces no es móvil. Un mutex o una variable atómica es una ubicación específica de la memoria (tratada con propiedades especiales), por lo que no tiene sentido moverse, y tampoco es copiable, por lo que no se puede mover.


17
+1 un ejemplo menos exótico de algo que no se puede mover porque tiene una dirección especial es un nodo en una estructura gráfica dirigida.
Potatoswatter

3
Si el mutex no se puede copiar ni mover, ¿cómo puedo copiar o mover un objeto que contiene un mutex? (Como una clase segura para subprocesos con su propio mutex para sincronización ...)
tr3w

44
@ tr3w, no puede, a menos que cree el mutex en el montón y lo mantenga a través de un unique_ptr o similar
Jonathan Wakely

2
@ tr3w: ¿No moverías toda la clase excepto la parte mutex?
user541686

3
@BenVoigt, pero el nuevo objeto tendrá su propio mutex. Creo que quiere decir que tiene operaciones de movimiento definidas por el usuario que mueven todos los miembros excepto el miembro mutex. Entonces, ¿qué pasa si el viejo objeto está caducando? Su mutex expira con él.
Jonathan Wakely

57

Respuesta corta: si un tipo es copiable, también debe ser movible. Sin embargo, lo contrario no es cierto: algunos tipos como std::unique_ptrson movibles, pero no tiene sentido copiarlos; estos son, naturalmente, tipos de solo movimiento.

La respuesta un poco más larga sigue ...

Hay dos tipos principales de tipos (entre otros más especiales, como los rasgos):

  1. Tipos de valor, como into vector<widget>. Estos representan valores y, naturalmente, deberían ser copiables. En C ++ 11, por lo general, debe pensar en mover como una optimización de copia, por lo que todos los tipos copiables deberían ser naturalmente movibles ... mover es solo una forma eficiente de hacer una copia en el caso a menudo común que no hace ' Ya no necesito el objeto original y de todos modos lo destruiremos.

  2. Tipos de referencia que existen en las jerarquías de herencia, como las clases base y las clases con funciones miembro virtuales o protegidas. Normalmente se mantienen con puntero o referencia, a menudo una base*o base&, por lo que no proporcionan una construcción de copia para evitar el corte; si desea obtener otro objeto como uno existente, generalmente llama a una función virtual como clone. Estos no necesitan construcción o asignación de movimiento por dos razones: no son copiables y ya tienen una operación de "movimiento" natural aún más eficiente: simplemente copie / mueva el puntero al objeto y el objeto en sí no tiene que moverse a una nueva ubicación de memoria en absoluto.

La mayoría de los tipos se dividen en una de esas dos categorías, pero también hay otros tipos de tipos que también son útiles, pero más raros. En particular, aquí, los tipos que expresan la propiedad exclusiva de un recurso, como std::unique_ptr, son tipos de solo movimiento natural, porque no tienen un valor similar (no tiene sentido copiarlos) pero los usa directamente (no siempre por puntero o referencia) y, por lo tanto, desea mover objetos de este tipo de un lugar a otro.


61
¿Podría el verdadero Herb Sutter ponerse de pie? :)
fredoverflow

66
Sí, cambié de usar una cuenta de Google OAuth a otra y no puedo molestarme en buscar una forma de fusionar los dos inicios de sesión que me da aquí. (Sin embargo, otro argumento contra OAuth entre los mucho más convincentes). Probablemente no volveré a usar el otro, así que esto es lo que usaré por ahora para la publicación SO ocasional.
Herb Sutter

77
Pensé que std::mutexera inamovible, ya que la dirección usa mutexes POSIX.
Cachorro

9
@SChepurin: En realidad, eso se llama HerbOverflow, entonces.
sbi

26
Esto está recibiendo muchos votos a favor, nadie se ha dado cuenta de que dice cuándo un tipo debe ser solo de movimiento, ¿cuál no es la pregunta? :)
Jonathan Wakely

18

En realidad, cuando busco, descubrí que algunos tipos en C ++ 11 no son móviles:

  • todos los mutextipos ( recursive_mutex, timed_mutex, recursive_timed_mutex,
  • condition_variable
  • type_info
  • error_category
  • locale::facet
  • random_device
  • seed_seq
  • ios_base
  • basic_istream<charT,traits>::sentry
  • basic_ostream<charT,traits>::sentry
  • todos los atomictipos
  • once_flag

Aparentemente hay una discusión sobre Clang: https://groups.google.com/forum/?fromgroups=#!topic/comp.std.c++/pCO1Qqb3Xa4


1
... los iteradores no deben ser móviles? ¿Qué? Por qué?
user541686

sí, creo que iterators / iterator adaptorsdebería ser editado ya que C ++ 11 tiene move_iterator?
billz

Bien, ahora estoy confundido. ¿Estás hablando de iteradores que mueven sus objetivos , o de mover los propios iteradores ?
user541686

1
Así es std::reference_wrapper. Ok, los otros parecen ser inmóviles.
Christian Rau

1
Estos parecen caer en tres categorías: 1. Bajo nivel de tipos relacionados de concurrencia-(Atomics, exclusiones mutuas), 2. Clases de base polimórfica ( ios_base, type_info, facet), 3. una variedad de cosas raras ( sentry). Probablemente las únicas clases inamovibles que escribirá un programador promedio están en la segunda categoría.
Philipp

0

Otra razón que he encontrado es el rendimiento. Digamos que tiene una clase 'a' que tiene un valor. Desea generar una interfaz que permita a un usuario cambiar el valor por un tiempo limitado (para un alcance).

Una forma de lograr esto es devolviendo un objeto 'guardián de alcance' desde 'a' que establece el valor nuevamente en su destructor, de esta manera:

class a 
{ 
    int value = 0;

  public:

    struct change_value_guard 
    { 
        friend a;
      private:
        change_value_guard(a& owner, int value) 
            : owner{ owner } 
        { 
            owner.value = value;
        }
        change_value_guard(change_value_guard&&) = delete;
        change_value_guard(const change_value_guard&) = delete;
      public:
        ~change_value_guard()
        {
            owner.value = 0;
        }
      private:
        a& owner;
    };

    change_value_guard changeValue(int newValue)
    { 
        return{ *this, newValue };
    }
};

int main()
{
    a a;
    {
        auto guard = a.changeValue(2);
    }
}

Si hiciera change_value_guard movible, tendría que agregar un 'if' a su destructor que verificaría si el protector se ha movido, eso es un if adicional y un impacto en el rendimiento.

Sí, claro, probablemente puede ser optimizado por cualquier optimizador sensato, pero aún así es bueno que el lenguaje (sin embargo, esto requiere C ++ 17, para poder devolver un tipo no móvil requiere una elisión de copia garantizada) pagar eso si no vamos a mover el guardia de cualquier otra forma que no sea devolverlo de la función de creación (el principio de no pagar por lo que no usa).

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.