繁体   English   中英

在 C++11 中总是将 std::mutex 声明为可变的?

[英]Always declare std::mutex as mutable in C++11?

看了 Herb Sutter 的演讲你不知道 const 和 mutable ,我想知道我是否应该总是将互斥体定义为可变的? 如果是,我猜对于任何同步容器(例如, tbb::concurrent_queue )也是如此?

一些背景:在他的演讲中,他指出 const == mutable == 线程安全,而std::mutex是每个定义线程安全的。

也有关于谈话的相关问题, 在 C++11 中 const 是否意味着线程安全

编辑:

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

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

虽然将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 因为我不打算更改该值,并且我希望编译器在我更改时对我大喊大叫。 同样的, isEvenisPrime :本次测试的结果应该不会改变,所以强制执行。

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

mutable说“除了我”。 这就是“逻辑常量”的“旧”概念的来源。 考虑他给出的常见用例:互斥锁成员。 需要锁定这个互斥锁以确保你的程序是正确的,所以你需要修改它。 但是,您不希望该函数是非常量的,因为修改任何其他成员都是错误的。 因此,您将其设为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的约束并不意味着它们必须如此。 我还需要考虑:如果我确实修改了那个成员,它会不会在实现中出错? 如果是这样,它不需要是mutable

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

在这种情况下,您应该站在mutable一边犯错。

当 Herb 给出一个const_cast示例并声明它是安全的时,他说得有点过于松散了。 考虑:

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 ),而在另一个const中我'没有修改容器并确保我遵守承诺(省略mutable )。

我认为 Herb 是在争论我们使用mutable的大多数情况,我们也在使用某种内部(线程安全)同步对象,我同意。 因此,他的观点大部分时间都有效。 但是存在这样的情况,我只是碰巧有一个线程安全对象,而只是将它视为另一个成员; 在这种情况下,我们回到const的旧和基本用法。

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

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

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

  2. 因此const等效于线程安全。

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

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

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

关键问题2是,虽然这是事实(如果我们接受1 )该const必须意味着线程安全的,这是正确的线程安全意味着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不意味着线程安全,这会导致标准库出现未定义的行为。

让我们谈谈const的变化。

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

在 C++03 及const版本中,与非const版本相比, const版本为调用者提供了额外的保证。 它承诺不修改其参数,在这里通过修改我们的意思是叫Foo的非const成员函数(包括分配等),或者将它传递给那些期望非功能const参数,或做同样其裸露的非易变数据成员。 somefunc将自身限制为Foo上的const操作。 而额外的保证完全是片面的。 调用者和Foo提供者都不需要做任何特殊的事情来调用const版本。 任何能够调用非const版本的人也可以调用const版本。

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

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

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

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

接受的答案涵盖了这个问题,但值得一提的是,萨特后来更改了错误地建议 const == mutable == thread-safe 的幻灯片。 可以在此处找到导致该幻灯片更改的博客文章:

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM