c++ - programacion - minecraft splash
¿Cuándo hacer un tipo no movible en C++ 11? (4)
De hecho, cuando busco, descubrí que algunos tipos en C ++ 11 no son móviles:
- todos los tipos
mutex
(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
atomic
tiposatomic
-
once_flag
Aparentemente hay un debate sobre Clang: https://groups.google.com/forum/?fromgroups=#!topic/comp.std.c++/pCO1Qqb3Xa4
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 movimientos en C ++ 11:
¿Cuándo tengo que (o es una buena idea para mí) hacer una clase no móvil en C ++ 11?
(Razones distintas de los problemas de compatibilidad con el código existente, eso es).
La respuesta de Herb (antes de que fuera editada) de hecho dio un buen ejemplo de un tipo que no debería ser movible: std::mutex
.
El tipo mutex nativo del SO (p. Ej. pthread_mutex_t
en plataformas POSIX) puede 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::mutex
contenía un tipo 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 std::mutex
tendría que almacenar el tipo mutex nativo en el montón para que permanezca en el mismo lugar cuando se mueve entre los objetos std::mutex
o el std::mutex
no debe moverse. No es posible almacenarlo en el montón porque un std::mutex
tiene un constructor constexpr
y debe ser elegible para la inicialización constante (es decir, inicialización estática) para garantizar que se construya un std::mutex
global antes de que comience la ejecución del programa , por lo que su constructor no puede usar new
. Entonces, la única opción que queda es que std::mutex
sea 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 mover std::mutex
que es que sería muy difícil hacerlo de manera segura, porque necesitaría saber que nadie está intentando bloquear el mutex en el momento en que se mueve. Como los mutexes son uno de los componentes básicos que puede utilizar para evitar las carreras de datos, sería desafortunado que no estuvieran seguros contra las carreras mismas. Con un std::mutex
inamovible, usted sabe que las únicas cosas que cualquiera puede hacerle una vez que se ha construido y antes de que se destruya es bloquearlo y desbloquearlo, y esas operaciones están garantizadas explícitamente que son seguras para hilos y no introducen datos carreras Este mismo argumento se aplica a std::atomic<T>
objetos std::atomic<T>
: a menos que puedan moverse de forma atómica, no sería posible moverlos con seguridad, otro hilo podría estar intentando llamar compare_exchange_strong
en el objeto en el momento en que se mueve. Entonces, otro caso en el que los tipos no deberían ser móviles es cuando se trata de bloques de construcción de bajo nivel de código concurrente seguro y debe garantizar la atomicidad de todas las operaciones sobre 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 saber si es seguro usarlo o si se movió ... 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 simplemente una pieza pura de memoria, no un tipo que actúa como un soporte para un valor o abstracción de un valor, no tiene sentido moverlo. Los tipos fundamentales como int
no se pueden mover: moverlos es solo una copia. No puedes extraer las agallas de un int
, puedes copiar su valor y luego establecerlo en cero, pero sigue siendo un int
con un valor, solo son bytes de memoria. Pero una int
todavía se puede mover en los términos del idioma porque una copia es una operación de movimiento válida. Sin embargo, para los tipos que no pueden copiarse, si no desea mover la pieza de memoria y tampoco puede copiar su valor, entonces no se puede mover. Una 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 se puede copiar, por lo que no se puede mover.
Otra razón que he encontrado - 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 devolver un objeto ''guardián de alcance'' de ''a'' que restablece el valor en su destructor, así:
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 que change_value_guard se moviera, tendría que agregar un ''si'' a su destructor que verificaría si se ha movido el protector - eso es un extra si, y un impacto en el rendimiento.
Sí, claro, probablemente pueda ser optimizado por cualquier optimizador sano, pero aún así es bueno que el lenguaje (esto requiere C ++ 17, sin embargo, para poder devolver un tipo no movible requiera elisión de copia garantizada) no nos requiera para pagar eso si, de todos modos, no vamos a mover el guardia de otra forma que devolverlo desde la función de creación (el principio de no pagar por lo que no se usa).
Respuesta corta: si se puede copiar un tipo, también debe ser movible. Sin embargo, lo contrario no es cierto: algunos tipos como std::unique_ptr
son movibles, pero no tiene sentido copiarlos; estos son naturalmente tipos solo de movimiento.
Le sigue una respuesta un poco más larga ...
Hay dos tipos principales de tipos (entre otros más especiales como los rasgos):
Tipos de valor, como
int
ovector<widget>
. Estos representan valores, y naturalmente deben ser copiables. En C ++ 11, en general, debería pensar en mover como una optimización de copia, por lo que todos los tipos de copiado deberían ser movibles de forma natural ... el movimiento es solo una forma eficiente de hacer una copia en el caso frecuente de que no lo haga Necesito el objeto original más y lo voy a destruir de todos modos.Tipos similares a los de referencia que existen en las jerarquías de herencia, como las clases base y las clases con funciones de miembro virtual o protegido. Normalmente se mantienen por puntero o referencia, a menudo una
base*
obase&
, y por lo tanto no proporcionan una construcción de copia para evitar cortar; si desea obtener otro objeto igual que uno existente, generalmente llama a una función virtual comoclone
. No es necesario mover la construcción o la asignación por dos motivos: no se pueden copiar, 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 lo hace tiene que moverse a una nueva ubicación de memoria.
La mayoría de los tipos pertenecen a una de esas dos categorías, pero también existen otros tipos de tipos que también son útiles, solo que más raros. En particular, aquí, los tipos que expresan propiedad exclusiva de un recurso, como std::unique_ptr
, son tipos de solo movimiento natural, porque no tienen valor (no tiene sentido copiarlos) pero los usa directamente (no siempre por puntero o referencia) y por eso quiere mover objetos de este tipo de un lugar a otro.