Non. Cependant, la plupart du temps, ils le seront.
Bien qu'il soit utile de penser à const
comme "thread-safe" et mutable
comme "(déjà) thread-safe", const
est toujours fondamentalement liée à la notion de promesse "Je ne changerai pas cette valeur". Il en sera toujours ainsi.
J'ai un long train de pensées, alors soyez indulgent avec moi.
Dans ma propre programmation, j'ai mis const
partout. Si j'ai une valeur, c'est une mauvaise chose de la changer, sauf si je dis que je le veux. Si vous essayez de modifier intentionnellement un objet const, vous obtenez une erreur de compilation (facile à corriger et aucun résultat livrable !). Si vous modifiez accidentellement un objet non-const, vous obtenez une erreur de programmation à l'exécution, un bogue dans une application compilée et un mal de tête. Il est donc préférable d'opter pour le premier choix et de garder les choses const
.
Par exemple :
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;
}
}
Pourquoi les types de paramètres pour is_even
et is_prime
marqué const
? Parce que du point de vue de l'implémentation, changer le nombre que je teste serait une erreur ! Pourquoi const auto& x
? Parce que je n'ai pas l'intention de changer cette valeur, et je veux que le compilateur m'engueule si je le fais. Idem avec isEven
et isPrime
le résultat de ce test ne devrait pas changer, alors appliquez-le.
Bien sûr. const
Les fonctions membres sont simplement un moyen de donner this
un type de la forme const T*
. Il est dit que "ce serait une erreur de mise en œuvre si je devais changer certains de mes membres".
mutable
dit "sauf moi". C'est de là que vient la "vieille" notion de "logiquement constant". Considérez le cas d'utilisation commun qu'il a donné : un membre mutex. Vous necesita pour verrouiller ce mutex afin de s'assurer que votre programme est correct, vous devez donc le modifier. Cependant, vous ne voulez pas que la fonction soit non-const, car la modification de tout autre membre serait une erreur. Il faut donc la rendre const
et marquer le mutex comme mutable
.
Tout cela n'a rien à voir avec la sécurité des fils.
Je pense que c'est aller trop loin que de dire que les nouvelles définitions remplacent les anciennes idées données ci-dessus ; elles les complètent simplement d'un autre point de vue, celui de la sécurité des fils.
Maintenant, le point de vue de Herb donne que si vous avez const
elles doivent être thread-safe pour pouvoir être utilisées en toute sécurité par la bibliothèque standard. Comme corollaire, les seuls membres que vous devriez vraiment marquer comme étant mutable
sont celles qui sont déjà sûres pour les fils, parce qu'elles sont modifiables à partir d'un système d'exploitation. const
fonction :
struct foo
{
void act() const
{
mNotThreadSafe = "oh crap! const meant I would be thread-safe!";
}
mutable std::string mNotThreadSafe;
};
Ok, donc nous savons que les choses sûres pour les fils de discussion peut être marqué comme mutable
Vous vous demandez s'ils doivent l'être.
Je pense que nous devons considérer les deux points de vue simultanément. Du nouveau point de vue d'Herb, oui. Ils sont thread safe et n'ont donc pas besoin d'être liés par la constance de la fonction. Mais juste parce qu'ils peut peuvent être dispensés des contraintes de la const
ne signifie pas qu'ils doivent l'être. Je dois encore réfléchir : est-ce que ce serait une erreur d'implémentation si je modifiais ce membre ? Si c'est le cas, il faut que ce ne soit pas mutable
!
Il s'agit d'un problème de granularité : certaines fonctions peuvent avoir besoin de modifier l'adresse de l'aspirateur. mutable
tandis que d'autres ne le font pas. C'est comme si l'on voulait que seules certaines fonctions aient un accès de type ami, mais que l'on ne pouvait être ami qu'avec la classe entière. (C'est un problème de conception du langage).
Dans ce cas, il faut privilégier le côté mutable
.
Herb a parlé un peu trop librement lorsqu'il a donné un const_cast
exemple et l'a déclaré sûr. Réfléchissez :
struct foo
{
void act() const
{
const_cast<unsigned&>(counter)++;
}
unsigned counter;
};
Ceci est sûr dans la plupart des circonstances, sauf lorsque le foo
L'objet lui-même est const
:
foo x;
x.act(); // okay
const foo y;
y.act(); // UB!
Ce sujet est traité ailleurs sur SO, mais const foo
, implique la counter
Le membre est également const
et la modification d'un const
est un comportement non défini.
C'est pour cela que vous devriez vous tromper de côté mutable
: const_cast
ne vous donne pas tout à fait les mêmes garanties. Avait counter
a été marqué mutable
ça n'aurait pas été un const
objet.
Ok, donc si nous en avons besoin mutable
dans un endroit, nous en avons besoin partout, et nous devons juste faire attention dans les cas où nous n'en avons pas besoin. Cela signifie sûrement que tous les membres thread-safe devraient être marqués mutable
alors ?
Eh bien non, car tous les membres thread-safe ne sont pas là pour la synchronisation interne. L'exemple le plus trivial est une sorte de classe enveloppe (ce n'est pas toujours la meilleure pratique, mais elle existe) :
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;
};
Nous voilà en train d'emballer threadsafe_container
et en fournissant une autre fonction membre que nous voulons (ce serait mieux comme une fonction libre dans la pratique). Pas besoin de mutable
ici, l'exactitude de l'ancien point de vue l'emporte complètement : dans une fonction, je modifie le conteneur et c'est bon parce que je n'ai pas dit que je ne le ferais pas. (en omettant const
), et dans l'autre je ne modifie pas le conteneur et m'assurer que je tiens cette promesse (en omettant mutable
).
Je pense que Herb soutient que la plupart des cas où nous utiliserions mutable
nous utilisons également une sorte d'objet de synchronisation interne (thread-safe), et je suis d'accord. Ergo son point de vue fonctionne la plupart du temps. Mais il existe des cas où je dois simplement se produire d'avoir un objet thread-safe et de le traiter simplement comme un autre membre ; dans ce cas, nous nous rabattons sur l'utilisation ancienne et fondamentale de l'expression const
.