45 votes

Toujours déclarer std::mutex comme mutable en C++11 ?

Après avoir regardé la conférence d'Herb Sutter You Don't Know const et mutable je me demande si je dois toujours définir un mutex comme mutable ? Si oui, je suppose qu'il en va de même pour tout conteneur synchronisé (par ex, tbb::concurrent_queue ) ?

Un peu de contexte : Dans son exposé, il a affirmé que const == mutable == thread-safe, et que std::mutex est par définition thread-safe.

Il y a aussi une question connexe sur la conférence, Est-ce que const signifie thread-safe en C++11 ? .

Edita:

Aquí J'ai trouvé une question connexe (peut-être un doublon). Elle a été posée avant C++11, cependant. Cela fait peut-être une différence.

42voto

GManNickG Points 155079

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 .

2 votes

+1, mais je pense qu'il manque une phrase clé, à savoir que la constance est liée à une méthode observable comportement, pas nécessairement un comportement logique.

10voto

Mankarse Points 22800

Je viens de regarder la conférence, et je ne suis pas tout à fait d'accord avec ce que dit Herb Sutter.

Si je comprends bien, son argument est le suivant :

  1. [res.on.data.races]/3 impose une exigence aux types qui sont utilisés avec la bibliothèque standard : les fonctions membres non-const doivent être thread-safe.

  2. Par conséquent, const est équivalent à thread-safe.

  3. Et si const est équivalent à thread-safe, le mutable doit être équivalent à "croyez-moi, même les membres non-const de cette variable sont thread-safe".

À mon avis, les trois parties de cet argument sont défectueuses (et la deuxième partie est gravement défectueuse).

Le problème avec 1 c'est que [res.on.data.races] donne des exigences pour les types de la bibliothèque standard, et non pour les types à utiliser avec la bibliothèque standard. Ceci dit, je pense qu'il est raisonnable (mais pas tout à fait clair) d'interpréter [res.on.data.races] comme donnant également des exigences pour les types à utiliser avec la bibliothèque standard, parce qu'il serait pratiquement impossible pour une implémentation de bibliothèque de respecter l'exigence de ne pas modifier les objets par le biais de const les références si const les fonctions membres étaient capables de modifier les objets.

En critique problème avec 2 est que, bien que ce soit vrai (si nous acceptons 1 ) que const doit impliquer la sécurité des fils, il est no Il est vrai que la sécurité des fils implique const et donc les deux ne sont pas équivalents. const implique toujours "logiquement immuable", c'est juste que la portée de "logiquement immuable" s'est élargie pour exiger la sécurité des fils.

Si nous prenons const et thread-safe pour être équivalents, nous perdons la fonctionnalité intéressante de const qui est qu'il nous permet de raisonner facilement sur le code en voyant où les valeurs peuvent être modifiées :

//`a` is `const` because `const` and thread-safe are equivalent.
//Does this function modify a?
void foo(std::atomic<int> const& a);

En outre, la section pertinente de [res.on.data.races] parle de "modifie", qui peut être raisonnablement interprété dans le sens plus général de "change d'une manière observable de l'extérieur", plutôt que de "change d'une manière non sûre pour les fils".

Le problème avec 3 est simplement qu'il ne peut être vrai que si 2 est vrai, et 2 est gravement défectueux.


Donc pour appliquer cela à votre question -- non, vous ne devriez pas faire en sorte que chaque objet synchronisé en interne mutable .

En C++11, comme en C++03, `const` signifie "logiquement immuable" et `mutable` signifie "peut changer, mais le changement ne sera pas observable de l'extérieur". La seule différence est qu'en C++11, "logiquement immuable" a été étendu pour inclure "thread-safe".

Vous devez réserver mutable pour les variables membres qui n'affectent pas l'état de l'objet visible de l'extérieur. D'un autre côté (et c'est le point clé que Herb Sutter a soulevé dans sa présentation), si vous avez un membre que es mutable pour une raison quelconque, ce membre doit être synchronisés en interne, sinon vous risquez de faire const n'implique pas de sécurité thread, et cela provoquerait un comportement indéfini avec la bibliothèque standard.

0 votes

En C++11, comme en C++03, const signifie "logiquement immuable" et mutable signifie "peut changer, mais le changement ne sera pas observable de l'extérieur". La seule différence est qu'en C++11, "logiquement immuable" a été étendu pour inclure "thread-safe".'' C'est ce que disait Herb.

0 votes

@Miles Rout : Peut-être que c'est ce qu'il voulait dire, mais ce n'est pas ce qu'il a réellement dit. Dans son exposé, il dit très explicitement "const == threadsafe". "const implique threadsafe" n'est pas la même chose que "const est équivalent à threadsafe". L'une est une implication à sens unique, l'autre est une implication à double sens. L'implication à double sens donnée dans l'exposé n'est pas valide, car il existe des opérations threadsafe qui modifient les objets ; et l'utilisation de const = = threadsafe n'est pas valide. const pour ceux qui feraient const moins utile.

0 votes

== ne signifie pas "est équivalent à"

6voto

n.m. Points 30344

Parlons du changement dans const .

void somefunc(Foo&);
void somefunc(const Foo&);

Dans le C++03 et avant, le const par rapport à la version non const l'un, fournit des garanties supplémentaires aux appelants. Il promet de ne pas modifier son argument, où par modification nous voulons dire appeler Foo (y compris l'affectation, etc.), ou en le passant à des fonctions qui attendent une valeur non-const. const ou de faire de même avec ses membres exposés non-mutants. somefunc se limite à const les opérations sur Foo . Et la garantie supplémentaire est totalement unilatérale. Ni l'appelant ni le Foo ne doivent rien faire de spécial pour appeler le fournisseur de l const version. Quiconque est capable d'appeler le non const peut appeler la version const version également.

Dans C++11, cela change. Le site const version offre toujours la même garantie à l'appelant, mais elle a désormais un prix. Le fournisseur de Foo doit s'assurer que tous les const les opérations sont protégées contre les threads . Ou du moins, il doit le faire lorsque somefunc est une fonction de la bibliothèque standard. Pourquoi ? Parce que la bibliothèque standard mai paralléliser ses opérations, et il sera appelez const des opérations sur tout et n'importe quoi sans aucune synchronisation supplémentaire. Vous, l'utilisateur, devez donc vous assurer que cette synchronisation supplémentaire n'est pas nécessaire. Bien sûr, ce n'est pas un problème dans la plupart des cas, puisque la plupart des classes n'ont pas de membres mutables et que la plupart des classes const Les opérations ne touchent pas les données globales.

Alors quoi ? mutable signifie maintenant ? C'est la même chose qu'avant ! A savoir, ces données sont non-const, mais c'est un détail d'implémentation, je vous promets que cela n'affecte pas le comportement observable. Cela signifie que non, vous ne devez pas marquer tout ce qui est en vue mutable tout comme vous ne l'avez pas fait en C++98. Ainsi, lorsque vous devez marquer un membre de données mutable ? Tout comme en C++98, lorsque vous devez appeler son non const les opérations d'un const et vous pouvez être sûr que cela ne cassera rien. Pour réitérer :

  • si l'état physique de votre membre de données n'affecte pas l'état observable de l'objet.
  • et il est thread-safe (synchronisé en interne)
  • alors vous pouvez (si vous en avez besoin !) aller de l'avant et le déclarer mutable .

La première condition est imposée, comme en C++98, parce que d'autres codes, y compris la bibliothèque standard, peuvent appeler votre fichier const et personne ne doit observer les changements résultant de ces appels. La deuxième condition existe, et c'est ce qui est nouveau dans C++11, car de tels appels peuvent être effectués de manière asynchrone.

3voto

BobbyA Points 49

La réponse acceptée couvre la question, mais il convient de mentionner que Sutter a depuis modifié la diapositive qui suggérait à tort que const == mutable == thread-safe. L'article de blog qui a conduit à ce changement de diapositive peut être consulté ici :

Ce que Sutter a fait de mal à propos de Const en C++11

TL:DR Const et Mutable impliquent tous deux la sécurité des fils, mais ont des significations différentes en ce qui concerne ce qui peut et ne peut pas être modifié dans votre programme.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X