c++ cv qualifier
¿Declarar siempre std:: mutex como mutable en C++ 11? (4)
Después de ver la charla de Herb Sutter You Do not Know const y mutable , me pregunto si siempre debería definir un mutex como mutable. En caso afirmativo, supongo que lo mismo vale para cualquier contenedor sincronizado (por ejemplo, tbb::concurrent_queue
)?
Algunos antecedentes: en su charla, afirmó que const == mutable == thread-safe, y std::mutex
es por definición thread-safe.
También hay una pregunta relacionada con la charla: ¿Const significa hilo seguro en C ++ 11 ?
Editar:
Here , encontré una pregunta relacionada (posiblemente un duplicado). Sin embargo, se preguntó antes de C ++ 11. Tal vez eso haga una diferencia.
Acabo de ver la charla, y no estoy totalmente de acuerdo con lo que dice Herb Sutter.
Si entiendo correctamente, su argumento es el siguiente:
[res.on.data.races]/3
impone un requisito sobre los tipos que se utilizan con la biblioteca estándar; las funciones de miembro no constantes deben ser seguras para subprocesos.Por lo tanto,
const
es equivalente a thread-safe.Y si
const
es equivalente a thread-safe, elmutable
debe ser equivalente a "confía en mí, incluso los miembros no const de esta variable son seguros para subprocesos".
En mi opinión, las tres partes de este argumento son defectuosas (y la segunda parte está críticamente defectuosa).
El problema con 1
es que [res.on.data.races]
proporciona los requisitos para los tipos en la biblioteca estándar, no los tipos que se utilizarán con la biblioteca estándar. Dicho esto, creo que es razonable (pero no del todo claro) interpretar [res.on.data.races]
como también los requisitos para los tipos que se utilizarán con la biblioteca estándar, porque sería prácticamente imposible para una biblioteca Implementación para mantener el requisito de no modificar objetos a través de las referencias const
si las funciones miembro de const
fueran capaces de modificar objetos.
El problema crítico con 2
es que si bien es cierto (si aceptamos 1
) que const
debe implicar seguro para subprocesos, no es cierto que thread-safe implica const
, por lo que los dos no son equivalentes. const
todavía implica "lógicamente inmutables", es solo que el alcance de la "inmutabilidad lógica" se ha expandido para requerir seguridad de hilo.
Si consideramos que const
y thread-safe son equivalentes, perdemos la buena característica de const
que es que nos permite razonar fácilmente sobre el código al ver dónde se pueden modificar los valores:
//`a` is `const` because `const` and thread-safe are equivalent.
//Does this function modify a?
void foo(std::atomic<int> const& a);
Además, la sección relevante de [res.on.data.races]
habla sobre "modificaciones", que pueden interpretarse razonablemente en el sentido más general de "cambios de una manera externamente observable", en lugar de solo "cambios en un hilo". manera insegura ".
El problema con 3
es simplemente que solo puede ser cierto si 2
es verdadero, y 2
es críticamente defectuoso.
Entonces, para aplicar esto a su pregunta, no, no debe hacer que todos los objetos sincronizados internamente sean mutable
.
En C ++ 11, como en C ++ 03, `const` significa" lógicamente inmutable "y" mutable "significa" puede cambiar, pero el cambio no será observable externamente ". La única diferencia es que en C ++ 11, "lógicamente inmutable" se ha expandido para incluir "thread-safe".
Debe reservar mutable
para variables miembro que no afecten el estado externo visible del objeto. Por otro lado (y este es el punto clave que Herb Sutter hace en su charla), si usted tiene un miembro que es mutable por alguna razón, ese miembro debe estar sincronizado internamente, de lo contrario corre el riesgo de que const
no implique un hilo de seguridad, y esto causaría un comportamiento indefinido con la biblioteca estándar.
Hablemos del cambio en const
.
void somefunc(Foo&);
void somefunc(const Foo&);
En C ++ 03 y anteriores, la versión const
, en comparación con la no- const
, proporciona garantías adicionales para los llamantes. Promete no modificar su argumento, donde mediante modificación nos referimos a llamar a las funciones de miembros no conformes de Foo
(incluida la asignación, etc.), o pasarlo a funciones que esperan un argumento no const
, o hacer lo mismo a su expuesto no mutable miembros de datos. somefunc
restringe a las operaciones de const
en Foo
. Y la garantía adicional es totalmente unilateral. Ni la persona que llama ni el proveedor de Foo
no tienen que hacer nada especial para llamar a la versión de const
. Cualquiera que sea capaz de llamar a la versión no const
puede llamar también a la versión de const
.
En C ++ 11 esto cambia. La versión const
aún proporciona la misma garantía a la persona que llama, pero ahora viene con un precio. El proveedor de Foo
debe asegurarse de que todas las operaciones de const
sean seguras para hilos . O al menos debe hacerlo cuando somefunc
es una función de biblioteca estándar. ¿Por qué? Debido a que la biblioteca estándar puede paralelizar sus operaciones, y llamará a las operaciones const
en cualquier cosa y todo sin ninguna sincronización adicional. Por lo tanto, usted, el usuario, debe asegurarse de que esta sincronización adicional no sea necesaria. Por supuesto, esto no es un problema en la mayoría de los casos, ya que la mayoría de las clases no tienen miembros mutables y la mayoría de las operaciones de const
no tocan los datos globales.
Entonces, ¿qué mutable
significa ahora? ¡Es lo mismo que antes! A saber, estos datos no son const, pero es un detalle de implementación, prometo que no afecta el comportamiento observable. Esto significa que no, no tiene que marcar todo a la vista mutable
, del mismo modo que no lo hizo en C ++ 98. Entonces, ¿cuándo debería marcar un miembro de datos mutable
? Al igual que en C ++ 98, cuando necesita llamar a sus operaciones no const
desde un método const
, y puede garantizar que no se romperá nada. Reiterar:
- si el estado físico de su miembro de datos no afecta el estado observable del objeto
- y es seguro para subprocesos (sincronizado internamente)
- entonces puedes (si es necesario) seguir adelante y declararlo
mutable
.
La primera condición se impone, como en C ++ 98, porque otro código, incluida la biblioteca estándar, puede llamar a sus métodos const
y nadie debe observar ningún cambio resultante de dichas llamadas. La segunda condición está ahí, y esto es lo nuevo en C ++ 11, porque tales llamadas se pueden realizar de forma asíncrona.
La respuesta aceptada cubre la pregunta, pero vale la pena mencionar que Sutter cambió la diapositiva que sugiere incorrectamente que const == mutable == thread-safe. La publicación del blog que condujo a ese cambio de diapositiva se puede encontrar aquí:
Lo que Sutter se equivocó sobre Const en C ++ 11
TL: DR Const y Mutable implican Thread-safe, pero tienen diferentes significados con respecto a lo que se puede y no se puede cambiar en su programa.
No. Sin embargo, la mayoría de las veces lo serán.
Si bien es útil pensar en const
como "thread-safe" y mutable
como "(ya) thread-safe", const
aún está fundamentalmente vinculado a la noción de prometer "No cambiaré este valor". Siempre lo será.
Tengo una larga lista de pensamiento así que tengan paciencia conmigo.
En mi propia programación, puse const
todas partes. Si tengo un valor, es una mala idea cambiarlo a menos que yo diga que lo deseo. Si intenta modificar a propósito un objeto const, se obtiene un error en tiempo de compilación (¡fácil de arreglar y no se puede enviar!). Si modifica accidentalmente un objeto no const, obtiene un error de programación de tiempo de ejecución, un error en una aplicación compilada y un dolor de cabeza. Así que es mejor equivocarse en el lado anterior y mantener las cosas en const
.
Por ejemplo:
bool is_even(const unsigned x)
{
return (x % 2) == 0;
}
bool is_prime(const unsigned x)
{
return /* left as an exercise for the reader */;
}
template <typename Iterator>
void print_special_numbers(const Iterator first, const Iterator last)
{
for (auto iter = first; iter != last; ++iter)
{
const auto& x = *iter;
const bool isEven = is_even(x);
const bool isPrime = is_prime(x);
if (isEven && isPrime)
std::cout << "Special number! " << x << std::endl;
}
}
¿Por qué los tipos de parámetros para is_even
y is_prime
marcados const
? Porque desde el punto de vista de la implementación, ¡cambiar el número que estoy probando sería un error! ¿Por qué const auto& x
? Porque no tengo la intención de cambiar ese valor, y quiero que el compilador me grite si lo hago. Lo mismo con isEven
e isPrime
: el resultado de esta prueba no debe cambiar, por lo que debe cumplirlo.
Por supuesto, las funciones miembro de const
son simplemente una forma de dar a this
un tipo de la forma const T*
. Dice "sería un error en la implementación si tuviera que cambiar algunos de mis miembros".
mutable
dice "excepto yo". Aquí es de donde viene la "vieja" noción de "const lógicamente". Considere el caso de uso común que dio: un miembro mutex. Debe bloquear este mutex para asegurarse de que su programa sea correcto, por lo que debe modificarlo. Sin embargo, no desea que la función sea no const, porque sería un error modificar cualquier otro miembro. Entonces lo haces const
y marcas el mutex como mutable
.
Nada de esto tiene que ver con la seguridad de hilos.
Creo que es un paso demasiado para decir que las nuevas definiciones reemplazan las viejas ideas dadas anteriormente; simplemente lo complementan desde otro punto de vista, el de seguridad de hilos.
Ahora, el punto de vista de Herb es que si tienes funciones const
, deben ser seguras para la ejecución de subprocesos para que la biblioteca estándar pueda utilizarlas de forma segura. Como corolario de esto, los únicos miembros que realmente debería marcar como mutable
son aquellos que ya son seguros para subprocesos, porque son modificables desde una función const
:
struct foo
{
void act() const
{
mNotThreadSafe = "oh crap! const meant I would be thread-safe!";
}
mutable std::string mNotThreadSafe;
};
De acuerdo, entonces sabemos que las cosas seguras para hilos pueden marcarse como mutable
, usted pregunta: ¿deberían estarlo?
Creo que debemos considerar ambos puntos de vista simultáneamente. Desde el nuevo punto de vista de Herb, sí. Son seguros para subprocesos, por lo que no necesitan estar vinculados por la const-ness de la función. Pero solo porque puedan ser excusados de las restricciones de const
no significa que tengan que serlo. Todavía tengo que considerar: ¿sería un error en la implementación si modificara ese miembro? Si es así, ¡no debe ser mutable
!
Aquí hay un problema de granularidad: algunas funciones pueden necesitar modificar el miembro mutable
mientras que otras no. Esto es como querer que solo algunas funciones tengan un acceso similar a un amigo, pero solo podemos hacer amistad con toda la clase. (Es un problema de diseño del lenguaje).
En este caso, debe equivocarse en el lado de mutable
.
Herb habló un poco demasiado flojo cuando dio un ejemplo de const_cast
y lo declaró seguro. Considerar:
struct foo
{
void act() const
{
const_cast<unsigned&>(counter)++;
}
unsigned counter;
};
Esto es seguro en la mayoría de las circunstancias, excepto cuando el objeto foo
sí es const
:
foo x;
x.act(); // okay
const foo y;
y.act(); // UB!
Esto está cubierto en otros lugares en SO, pero const foo
, implica que el miembro counter
también es const
, y la modificación de un objeto const
es un comportamiento indefinido.
Esta es la razón por la cual debes equivocarte al lado de mutable
: const_cast
no te da las mismas garantías. Si el counter
hubiera sido marcado como mutable
, no habría sido un objeto const
.
Está bien, entonces si lo necesitamos mutable
en un lugar lo necesitamos en todas partes, y solo tenemos que tener cuidado en los casos en que no lo hagamos. ¿Seguro que esto significa que todos los miembros seguros para subprocesos deberían marcarse como mutable
?
Bueno, no, porque no todos los miembros seguros para subprocesos están ahí para la sincronización interna. El ejemplo más trivial es algún tipo de clase contenedora (no siempre son las mejores prácticas pero existen):
struct threadsafe_container_wrapper
{
void missing_function_I_really_want()
{
container.do_this();
container.do_that();
}
const_container_view other_missing_function_I_really_want() const
{
return container.const_view();
}
threadsafe_container container;
};
Aquí estamos envolviendo threadsafe_container
y proporcionando otra función de miembro que queremos (sería mejor como una función gratuita en la práctica). No hay necesidad de mutable
aquí, la corrección desde el antiguo punto de vista triunfa por completo: en una función estoy modificando el contenedor y está bien porque no dije que no lo haría (omitiendo const
), y en el otro yo '' No modifico el contenedor y me aseguro de mantener esa promesa (omitiendo mutable
).
Creo que Herb argumenta que la mayoría de los casos en los que usaríamos mutable
también estamos utilizando algún tipo de objeto de sincronización interno (thread-safe), y estoy de acuerdo. Ergo su punto de vista funciona la mayor parte del tiempo. Pero existen casos en los que simplemente tengo un objeto seguro para subprocesos y simplemente lo trato como un miembro más; en este caso recurrimos al antiguo y fundamental uso de const
.