45

看完 Herb Sutter 的演讲You Don't Know const and mutable之后,我想知道我是否应该始终将互斥锁定义为 mutable?如果是,我猜任何同步容器(例如,tbb::concurrent_queue)都一样?

一些背景知识:在他的演讲中,他说 const == mutable == 线程安全,并且std::mutex根据定义是线程安全的。

关于谈话还有一个相关的问题,Does const mean thread-safe in C++11

编辑:

在这里,我发现了一个相关的问题(可能是重复的)。不过,它是在 C++11 之前被问到的。也许这会有所作为。

4

4 回答 4

42

不,但是,大多数时候他们会。

虽然将其const视为“线程安全”和mutable“(已经)线程安全”const是有帮助的,但从根本上仍然与承诺“我不会改变这个值”的概念相关联。它总是会的。

我的思路很长,所以请耐心等待。

在我自己的编程中,我const到处放。如果我有一个值,除非我说我想改变它,否则改变它是一件坏事。如果你试图有目的地修改一个 const 对象,你会得到一个编译时错误(易于修复并且没有可交付的结果!)。如果您不小心修改了非常量对象,您会遇到运行时编程错误、编译应用程序中的错误以及令人头疼的问题。所以最好是在前边犯错并保留东西const

例如:

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

为什么参数类型为is_evenis_prime标记const?因为从实现的角度来看,更改我正在测试的数字将是一个错误!为什么const auto& x?因为我不打算改变那个值,如果我这样做了,我希望编译器对我大喊大叫。与isEvenand相同isPrime:此测试的结果不应更改,因此请强制执行。

当然,const成员函数只是一种给出this形式类型的方法const T*。它说“如果我要改变我的一些成员,那将是一个实施错误”。

mutable说“除了我”。这就是“逻辑上的 const”的“旧”概念的来源。考虑他给出的常见用例:互斥体成员。您需要锁定此互斥锁以确保您的程序正确,因此您需要对其进行修改。但是,您不希望该函数是非常量的,因为修改任何其他成员都是错误的。所以你做了它const并将互斥锁标记为mutable.

这些都与线程安全无关。

我认为说新定义取代了上面给出的旧想法有点过分了。它们只是从另一个角度补充它,即线程安全。

现在 Herb 的观点是,如果你有const函数,它们需要是线程安全的,才能被标准库安全地使用。作为这样的推论,您真正应该标记的唯一成员mutable是那些已经是线程安全的成员,因为它们可以从const函数中修改:

struct foo
{
    void act() const
    {
        mNotThreadSafe = "oh crap! const meant I would be thread-safe!";
    }

    mutable std::string mNotThreadSafe;
};

好的,所以我们知道线程安全的东西可以标记为mutable,你问:应该这样吗?

我认为我们必须同时考虑这两种观点。从 Herb 的新观点来看,是的。它们是线程安全的,因此不需要受函数的 const 约束。但仅仅因为他们可以安全地摆脱限制const并不意味着他们必须这样做。我仍然需要考虑:如果我修改了那个成员,会不会是一个错误的实现?如果是这样,它不需要mutable

这里有一个粒度问题:一些函数可能需要修改可能的mutable成员,而另一些则不需要。这就像只希望某些功能具有类似朋友的访问权限,但我们只能为整个班级加好友。(这是一个语言设计问题。)

在这种情况下,您应该在mutable.

const_castHerb 举了一个例子并宣布它是安全的,他说得有点太松散了。考虑:

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

    unsigned counter;
};

在大多数情况下这是安全的,除非foo对象本身是const

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

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

这在 SO 的其他地方有所介绍,但是const foo,意味着counter成员也是const,并且修改const对象是未定义的行为。

这就是为什么你应该在mutable:方面犯错的原因const_cast并没有给你同样的保证。已counter被标记mutable,它不会是一个const对象。

好的,所以如果我们mutable在一个地方需要它,我们在任何地方都需要它,我们只需要在不需要的情况下小心。当然这意味着所有线程安全成员都应该被标记mutable

不,因为并非所有线程安全成员都用于内部同步。最简单的例子是某种包装类(并不总是最佳实践,但它们存在):

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

在这里,我们包装threadsafe_container并提供了另一个我们想要的成员函数(在实践中作为一个自由函数会更好)。在这里不需要mutable,从旧的角度来看,正确性完全胜过:在一个函数中,我正在修改容器,这没关系,因为我没有说我不会(省略const),而在另一个函数中我不是修改容器并确保我信守承诺(省略mutable)。

我认为 Herb 在争论我们使用的大多数情况下,我们mutable也在使用某种内部(线程安全)同步对象,我同意。因此,他的观点大部分时间都有效。但是在某些情况下,我只是碰巧有一个线程安全的对象,只是把它当作另一个成员;在这种情况下,我们依靠const.

于 2013-01-03T04:34:15.493 回答
10

我刚刚看了演讲,我并不完全同意赫伯·萨特所说的。

如果我理解正确,他的论点如下:

  1. [res.on.data.races]/3对与标准库一起使用的类型提出了要求——非常量成员函数必须是线程安全的。

  2. 因此const相当于线程安全。

  3. 如果const相当于线程安全,则mutable必须相当于“相信我,即使这个变量的非常量成员也是线程安全的”。

在我看来,这个论点的所有三个部分都存在缺陷(第二部分存在严重缺陷)。

问题1在于,它[res.on.data.races]给出了标准库中类型的要求,而不是标准库中使用的类型。也就是说,我认为将其解释为也对标准库使用的类型提出要求是合理的(但并不完全明确)[res.on.data.races],因为库实现实际上不可能支持不修改对象的要求如果成员函数能够修改对象,则通过const引用。const

关键问题2是,虽然它是真的(如果我们接受1const必须暗示线程安全,但线程安全暗示不是真的,const因此两者不等价。const仍然意味着“逻辑上不可变”,只是“逻辑上不可变”的范围已经扩大到需要线程安全。

如果我们将const线程安全视为等价的,那么我们就失去了一个很好的特性,const即它允许我们通过查看可以修改值的位置来轻松地推理代码:

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

此外,有关[res.on.data.races]“修改”的讨论部分可以合理地解释为“以外部可观察方式发生的变化”,而不仅仅是“以线程不安全方式发生的变化”。

问题3很简单,只有当它是真的时它才可能2是真的,并且2存在严重的缺陷。


因此,将此应用于您的问题 - 不,您不应该让每个内部同步对象mutable

在 C++11 中,与在 C++03 中一样,`const` 表示“逻辑上不可变”,而 `mutable` 表示“可以更改,但更改不会被外部观察到”。唯一的区别是在 C++11 中,“逻辑上不可变”已扩展为包括“线程安全”。

您应该保留mutable不影响对象外部可见状态的成员变量。另一方面(这是 Herb Sutter 在他的演讲中提出的关键点),如果您有一个成员由于某种原因是可变的,则该成员必须在内部同步,否则您可能会导致const不暗示线程安全,并且这将导致标准库出现未定义的行为。

于 2013-01-03T04:10:38.170 回答
6

来说说中的变化const

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

在 C++03 及之前的const版本中,与非版本相比,该版本const为调用者提供了额外的保证。它承诺不修改它的参数,这里的修改是指调用Foo's 的非常量成员函数(包括赋值等),或者将它传递给期望非const参数的函数,或者对其暴露的非可变数据成员做同样的事情. somefunc将自身限制为对 的const操作Foo。而额外的保证完全是片面的。调用者和Foo提供者都不需要做任何特殊的事情来调用const版本。任何能够调用非const版本的人也可以调用const版本。

在 C++11 中,这种情况发生了变化。该const版本仍然为调用者提供相同的保证,但现在它带有一个价格。的提供者Foo必须确保所有const操作都是线程安全的。或者至少它必须在somefunc标准库函数时这样做。为什么?因为标准库可能会并行化它的操作,并且它const在没有任何额外同步的情况下对任何事物调用操作。因此,您(用户)必须确保不需要这种额外的同步。当然,在大多数情况下这不是问题,因为大多数类没有可变成员,并且大多数const操作不涉及全局数据。

那么现在是什么mutable意思呢?和以前一样!也就是说,这个数据是非常量的,但它是一个实现细节,我保证它不会影响可观察的行为。这意味着不,您不必在视线范围内标记所有内容mutable,就像您在 C++98 中没有这样做一样。那么什么时候应该标记一个数据成员mutable呢?就像在 C++98 中一样,当你需要const从一个方法中调用它的非操作时const,你可以保证它不会破坏任何东西。重申:

  • 如果您的数据成员的物理状态不影响对象的可观察状态
  • 是线程安全的(内部同步的)
  • 那么你可以(如果你需要!)继续声明它mutable

第一个条件是强加的,就像在 C++98 中一样,因为包括标准库在内的其他代码可能会调用您的const方法,并且没有人应该观察到此类调用导致的任何更改。存在第二个条件,这是 C++11 中的新功能,因为可以异步进行此类调用。

于 2013-01-03T11:18:49.507 回答
3

接受的答案涵盖了这个问题,但值得一提的是,Sutter 已经更改了错误地建议 const == mutable == 线程安全的幻灯片。可以在此处找到导致该幻灯片更改的博客文章:

Sutter 对 C++11 中的 Const 有什么误解

TL:DR Const 和 Mutable 都暗示线程安全,但在程序中可以更改和不能更改的内容方面具有不同的含义。

于 2013-08-26T18:17:45.887 回答