Je ne sais pas si c'est une question de style ou quelque chose qui a une règle stricte ...
Si je veux garder l'interface de méthode publique aussi constante que possible, mais sécuriser le thread objet, dois-je utiliser des mutex mutables? En général, est-ce un bon style, ou une interface de méthode non const devrait-elle être préférée? Veuillez justifier votre point de vue.
[Réponse modifiée]
Fondamentalement, l'utilisation de méthodes const avec des mutex mutables est une bonne idée (ne renvoyez pas les références en passant, assurez-vous de renvoyer par valeur), au moins pour indiquer qu'elles ne modifient pas l'objet. Les mutex ne devraient pas être const, ce serait un mensonge éhonté de définir des méthodes de verrouillage/déverrouillage comme const ...
En fait, cela (et la mémorisation) sont les seules utilisations loyales que je vois du mot-clé mutable
.
Vous pouvez également utiliser un mutex externe à votre objet: faites en sorte que toutes vos méthodes soient réentrantes et demandez à l'utilisateur de gérer lui-même le verrou: { lock locker(the_mutex); obj.foo(); }
n'est pas si difficile à taper, et
{
lock locker(the_mutex);
obj.foo();
obj.bar(42);
...
}
a l'avantage de ne pas nécessiter deux verrous mutex (et vous êtes assuré que l'état de l'objet n'a pas changé).
La question cachée est: où placez-vous le mutex protégeant votre classe?
En résumé, disons que vous voulez lire le contenu d'un objet protégé par un mutex.
La méthode "read" doit être sémantiquement "const" car elle ne modifie pas l'objet lui-même. Mais pour lire la valeur, vous devez verrouiller un mutex, extraire la valeur, puis déverrouiller le mutex, ce qui signifie que le mutex lui-même doit être modifié, ce qui signifie que le mutex lui-même ne peut pas être "const".
Alors tout va bien. L'objet peut être "const", et le mutex n'a pas besoin d'être:
Mutex mutex ;
int foo(const Object & object)
{
Lock<Mutex> lock(mutex) ;
return object.read() ;
}
À mon humble avis, c'est une mauvaise solution, car n'importe qui pourrait réutiliser le mutex pour protéger autre chose. En t'incluant. En fait, vous vous trahirez parce que, si votre code est suffisamment complexe, vous serez simplement confus quant à ce que tel ou tel mutex protège exactement.
Je sais: j'ai été victime de ce problème.
À des fins d'encapsulation, vous devez placer le mutex aussi près que possible de l'objet qu'il protège.
Habituellement, vous écrivez une classe avec un mutex à l'intérieur. Mais tôt ou tard, vous devrez protéger une structure STL complexe, ou toute autre chose écrite par un autre sans mutex à l'intérieur (ce qui est une bonne chose).
Une bonne façon de le faire est de dériver l'objet d'origine avec un modèle héritant en ajoutant la fonction mutex:
template <typename T>
class Mutexed : public T
{
public :
Mutexed() : T() {}
// etc.
void lock() { this->m_mutex.lock() ; }
void unlock() { this->m_mutex.unlock() ; } ;
private :
Mutex m_mutex ;
}
De cette façon, vous pouvez écrire:
int foo(const Mutexed<Object> & object)
{
Lock<Mutexed<Object> > lock(object) ;
return object.read() ;
}
Le problème est qu'il ne fonctionnera pas car object
est const, et l'objet verrou appelle les méthodes non const lock
et unlock
.
Si vous pensez que const
est limité aux objets const au niveau du bit, alors vous êtes foutu et devez revenir à la "solution de mutex externe".
La solution consiste à admettre que const
est davantage un qualificatif sémantique (tout comme volatile
lorsqu'il est utilisé comme qualificateur de méthode de classes). Vous cachez le fait que la classe n'est pas entièrement const
mais assurez-vous toujours de fournir une implémentation qui tient la promesse que les parties significatives de la classe ne seront pas modifiées lors de l'appel d'une méthode const
.
Vous devez ensuite déclarer votre mutex mutable, et les méthodes de verrouillage/déverrouillage const
:
template <typename T>
class Mutexed : public T
{
public :
Mutexed() : T() {}
// etc.
void lock() const { this->m_mutex.lock() ; }
void unlock() const { this->m_mutex.unlock() ; } ;
private :
mutable Mutex m_mutex ;
}
La solution mutex interne est une bonne solution à mon humble avis: avoir des objets déclarés l'un près de l'autre dans une main et les avoir agrégés dans un wrapper dans l'autre main, c'est la même chose à la fin.
Mais l'agrégation a les avantages suivants:
Donc, gardez votre mutex aussi près que possible de l'objet mutex (par exemple en utilisant la construction Mutexed ci-dessus), et optez pour le qualificatif mutable
pour le mutex.
Apparemment, Herb Sutter a le même point de vue: sa présentation sur les "nouvelles" significations de const
et mutable
en C++ 11 est très éclairante:
http://herbsutter.com/2013/01/01/video-you-dont-know-const-and-mutable/