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

Après avoir regardé le discours D'Herb Sutter vous ne connaissez pas const et mutable , je me demande si je devrais toujours définir un mutex comme mutable? Si oui, je suppose que la même chose vaut pour tout conteneur synchronisé(par exemple, tbb::concurrent_queue)?

Quelques antécédents: dans son discours, il a déclaré que const = = mutable = = thread-safe, et std::mutex est par définition thread-safe.

Il y a aussi une question connexe sur le discours, const signifie-t-il thread-safe dans C++11 .

Modifier:

Ici, j'ai trouvé une question connexe (peut-être un doublon). Il a été demandé avant C++11, cependant. Peut-être que cela fait une différence.

35
demandé sur Community 2013-01-03 04:49:32

4 réponses

Non. Cependant, la plupart du temps ils seront.

Bien qu'il soit utile de considérer const comme "thread-safe" et mutable comme "(déjà) thread-safe", const est toujours fondamentalement lié à la notion de promesse "Je ne changerai pas cette valeur". Il le sera toujours.

J'ai une longue pensée, alors supportez - moi.

Dans ma propre programmation, je mets const partout. Si j'ai une valeur, c'est une mauvaise chose de la changer à moins que je dise que je le veux. Si vous essayez de modifier délibérément un objet const, vous obtenez une erreur de compilation (facile à réparer et aucun résultat expédiable!). Si vous modifiez accidentellement un objet non-const, vous obtenez une erreur de programmation d'exécution, un bug dans une application compilée et un mal de tête. Il est donc préférable de se tromper sur le premier côté 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 sont les types de paramètres pour is_even et is_prime marqué const? Parce que d'un point de vue d'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 me crie dessus 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 ne sont qu'un moyen de donner à this un type de la forme const T*. Il dit: "ce serait une erreur dans la 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 "const logique". Envisager l' cas d'usage courant qu'il a donné: un membre mutex. Vous avez besoin de pour verrouiller ce mutex pour vous assurer que votre programme est correct, vous devez donc le modifier. Vous ne voulez pas que la fonction soit non-const, car ce serait une erreur de modifier un autre membre. Donc, vous le faites const et marquez le mutex comme mutable.

rien de tout cela n'a à voir avec la sécurité des fils.

Je pense que c'est un pas de trop pour dire que les nouvelles définitions remplacent les anciennes idées données ci-dessus; elles ne font que compléter d'un autre point de vue, celui de thread-sécurité.

Maintenant le point de vue d'Herbe donne que si vous avez const fonctions, ils doivent être thread-safe pour être utilisables en toute sécurité par la bibliothèque standard. En corollaire de ceci, les seuls membres que vous devriez vraiment marquer comme mutable sont ceux qui sont déjà thread-safe, car ils sont modifiables à partir d'une fonction const:

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 thread-safe peuvent être marquées comme mutable, vous demandez: devraient-elles l'être?

Je pense que nous devons considérer les deux vues simultanément. Du nouveau point de vue D'Herb, Oui. Ils sont thread safe donc n'ont pas besoin d'être liés par la const-ness de la fonction. Mais ce n'est pas parce qu'ils peuvent être excusés en toute sécurité des contraintes de const qu'ils doivent l'être. Je dois encore considérer: serait - ce une erreur dans la mise en œuvre si je modifiais ce membre? Si oui, il ne doit pas être mutable!

Il y a un problème de granularité ici: certaines fonctions peuvent besoin de modifier le membre mutable alors que d'autres ne le font pas. C'est comme vouloir seulement que certaines fonctions aient un accès semblable à un ami, mais nous ne pouvons qu'ami toute la classe. (C'est un problème de conception de langage.)

Dans ce cas, vous devriez vous tromper du côté de mutable.

Herb a parlé un peu trop lâchement quand il a donné un exemple const_cast un déclaré sûr. Considérez:

struct foo
{
    void act() const
    {
        const_cast<unsigned&>(counter)++;
    }

    unsigned counter;
};

Ceci est sûr dans la plupart des circonstances, sauf lorsque l'objet foo lui-même est const:

foo x;
x.act(); // okay

const foo y;
y.act(); // UB!

C'est couvertes ailleurs, mais const foo, implique l' counter membre est également const, et la modification d'une const objet est un comportement indéfini.

C'est pourquoi vous devriez vous tromper du côté de mutable: const_cast ne vous donne pas tout à fait les mêmes garanties. Si counter avait été marqué mutable, il n'aurait pas été un objet const.

Ok, donc si nous en avons besoin mutable en un seul endroit, nous en avons besoin partout, et nous devons juste faire attention dans les cas où nous ne le faisons pas. signifie que tous les membres thread-safe doivent ê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 wrapper (pas toujours la meilleure pratique mais ils existent):

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;
};

Ici, nous enveloppons threadsafe_container et fournissons une autre fonction membre que nous voulons (ce serait mieux en tant que 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 correct 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 je m'assure que je respecte cette promesse (en omettant mutable).

Je pense Que Herb argumente le plus de 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ù j'arrive simplement à avoir un objet thread-safe et simplement le traiter comme un autre membre; dans ce cas, nous nous rabattons sur l'utilisation Ancienne et fondamentale de const.

35
répondu GManNickG 2013-08-26 18:23:24

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 sur les 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", la mutable doit être équivalent à "fais-moi confiance, même les membres non const de cette variable sont thread-safe".

À mon avis, les trois parties de cet argument sont imparfaites (et la deuxième partie est gravement imparfaite).

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

Le critique problème avec 2, c'est que s'il est vrai (si nous acceptons 1) que const doit impliquer thread-safe, c'est pas vrai que thread-safe implique const, de sorte que les deux ne sont pas équivalents. const implique toujours "logiquement immuable", c'est juste que la portée de "l'immuabilité logique" s'est élargie pour exiger la sécurité des threads.

Si nous prenons const et thread-safe pour être équivalents, nous perdons la fonctionnalité intéressante de const qui 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 "changements d'une manière observable de l'extérieur", plutôt que de simplement " changements d'une manière thread-dangereuse".

Le problème avec 3 est simplement que cela ne peut être vrai que si 2 est vrai, et 2 est gravement viciée.


Donc, pour appliquer cela à votre question-non, vous ne devriez pas faire tous les objets synchronisés 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 extérieurement observables". La seule différence est qu'en C++11, "logiquement immuable" a été élargi pour inclure "thread-safe".

Vous devez réserver mutable pour les variables membres qui n'affectent pas l'état visible de l'objet. D'un autre côté (et c'est le point clé Que Herb Sutter fait dans son discours), si vous avez un membre qui est mutable pour une raison quelconque, ce membre doit être synchronisé en interne, sinon vous risquez de faire const pas impliquer thread-safe, et cela provoquerait un comportement indéfini avec la bibliothèque standard.

10
répondu Mankarse 2013-01-03 04:16:45

Parlons du changement de const.

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

En C++03 et avant, la version const, par rapport à la version non - const, fournit des garanties supplémentaires aux appelants. Il promet de ne pas modifier son argument, où par modification nous entendons appeler les fonctions membres non const de Foo (y compris l'affectation, etc.), ou le transmettre à des fonctions qui attendent un argument non-const, ou faire de même à ses membres de données non mutables exposés. somefunc se limite aux opérations const sur Foo. Et la garantie supplémentaire est totalement unilatérale. Ni l'appelant ni le fournisseur Foo n'ont à faire quelque chose de spécial pour appeler la version const. Toute personne capable d'appeler la version non-const peut également appeler la version const.

En C++11 cela change. La version const fournit toujours la même garantie à l'appelant, mais maintenant elle vient avec un prix. le fournisseur de Foo doit s'assurer que toutes les opérations const sont thread safe . Ou au moins il doit le faire quand somefunc est une fonction de bibliothèque standard. Pourquoi? Parce que la bibliothèque standard peut paralléliser ses opérations, et elle appellera les opérations const sur n'importe quoi et n'importe quoi sans 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, car la plupart des classes n'ont pas de membres mutables et la plupart des opérations const ne touchent pas les données globales.

Alors, qu'est-ce que mutable veut dire maintenant? C'est l' même qu'avant! A savoir, ces données sont non-const, mais c'est un détail d'implémentation, je promets que cela n'affecte pas le comportement observable. Cela signifie que non, vous n'avez pas à marquer tout en vue mutable, tout comme vous ne l'avez pas fait en C++98. Alors, quand devriez-vous marquer un membre de données mutable? Tout comme dans c++98, lorsque vous devez appeler ses opérations non - const à partir d'une méthode const, et vous pouvez garantir qu'elle 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 avez besoin!) allez-y et déclarez-le mutable.

La première condition est imposée, comme dans C++98, car d'autres codes, y compris la bibliothèque standard, peuvent appeler vos méthodes const et personne ne doit observer les changements résultant de tels appels. La deuxième condition est là, et c'est ce qui est nouveau dans C++11, car de tels appels peuvent être fait de manière asynchrone.

6
répondu n.m. 2013-01-03 11:18:49

La réponse acceptée couvre la question, mais il convient de mentionner que Sutter a depuis changé la diapositive qui suggérait incorrectement que const = = mutable = = thread-safe. Le billet de blog qui a conduit à ce changement de diapositive peut être trouvé ici:

Qu'est ce que Sutter s'est trompé à propos de Const in C++11

TL: Dr Const et Mutable impliquent tous deux Thread-safe, mais ont des significations différentes en ce qui concerne ce qui peut et ne peut pas être changé dans votre programme.

3
répondu BobbyA 2013-08-26 18:17:45