Phi*_*ßen 36 c++ const mutable thread-safety c++11
在看了Herb Sutter的演讲" 你不知道const和mutable"之后,我想知道我是否应该总是将互斥量定义为可变的?如果是的话,我想同样适用于任何同步容器(例如tbb::concurrent_queue)?
一些背景:在他的演讲中,他说const == mutable == thread-safe,并且std::mutex每个定义都是线程安全的.
关于这个话题也有相关的问题,const在C++ 11中是否意味着线程安全.
编辑:
在这里,我发现了一个相关的问题(可能是重复的).不过,它在C++ 11之前被问过.也许这会产生影响.
GMa*_*ckG 37
不会.但是,大部分时间他们都会.
虽然将其const视为"线程安全"和mutable"(已经)线程安全" const是有帮助的,但仍然基本上与承诺"我不会改变这个值"的概念联系在一起.它永远都是.
我有一个很长的思路,所以请耐心等待.
在我自己的编程中,我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_even并is_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,你问:它们应该是吗?
我认为我们必须同时考虑这两种观点.从赫伯的新观点来看,是的.它们是线程安全的,因此不需要受函数常量的约束.但仅仅因为他们可以安全地摆脱限制const并不意味着他们必须这样做.我仍然需要考虑:如果我修改了该成员,那么它是否会在实现中出错?如果是这样,它不需要mutable!
这里有一个粒度问题:某些功能可能需要修改潜在mutable会员,而其他功能则不需要.这就像只想要一些函数来进行类似朋友的访问,但我们只能对整个类进行交友.(这是一个语言设计问题.)
在这种情况下,你应该站在一边mutable.
当他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),而在另一个我不是修改容器并确保我保持这个承诺(省略mutable).
我认为Herb正在争论我们使用的大多数情况,我们mutable也使用某种内部(线程安全)同步对象,我同意.因此,他的观点大部分时间都有效.但是存在这样的情况:我只是碰巧有一个线程安全的对象而只是将它视为另一个成员; 在这种情况下,我们回到旧的和基本的用途const.
Man*_*rse 10
我只是看了谈话,我并不完全同意Herb Sutter所说的话.
如果我理解正确,他的论点如下:
[res.on.data.races]/3 对标准库使用的类型强加了一个要求 - 非const成员函数必须是线程安全的.
因此const相当于线程安全.
如果const等同于线程安全,则mutable必须等同于"相信我,即使这个变量的非const成员都是线程安全的".
在我看来,这个论点的所有三个部分都是有缺陷的(第二部分是严重缺陷的).
问题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.
您应该保留mutable不影响对象外部可见状态的成员变量.另一方面(这是Herb Sutter在他的演讲中提出的关键点),如果你的某个成员由于某种原因是可变的,那么该成员必须在内部同步,否则你冒险const不会暗示线程安全,并且这将导致标准库的未定义行为.
让我们谈谈改变const.
void somefunc(Foo&);
void somefunc(const Foo&);
在C++ 03及之前的const版本中,与非版本相比,版本const为调用者提供了额外的保证.它承诺不修改它的参数,其中通过修改我们的意思是调用Foo非const成员函数(包括赋值等),或者将它传递给期望非const参数的函数,或者对其暴露的非可变数据成员执行相同的操作. .somefunc限制自己的const操作Foo.额外的保证是完全片面的.呼叫者和Foo提供者都不需要做任何特殊的事情来调用该const版本.任何能够调用非const版本的人都可以调用该const版本.
在C++ 11中,这会发生变化.该const版本仍然为呼叫者提供相同的保证,但现在它有一个价格.提供者Foo必须确保所有const操作都是线程安全的.或者至少它必须在somefunc标准库函数时才这样做.为什么?由于标准库可以并行它的操作,它会调用const任何东西,一切操作无需任何额外的同步.因此,您(用户)必须确保不需要此额外同步.当然,在大多数情况下这不是问题,因为大多数类没有可变成员,大多数const操作都不接触全局数据.
那现在mutable意味着什么?它和以前一样!也就是说,这个数据是非const的,但它是一个实现细节,我保证它不会影响可观察的行为.这意味着不,你不必在视线中标记所有内容mutable,就像你在C++ 98中没有这样做一样.那么什么时候应该标记数据成员mutable?就像在C++ 98中一样,当你需要const从一个const方法调用它的非操作时,你可以保证它不会破坏任何东西.重申:
mutable.第一个条件是强加的,就像在C++ 98中一样,因为其他代码(包括标准库)可能会调用您的const方法,并且没有人应该观察由此类调用引起的任何更改.第二个条件是,这是C++ 11中的新功能,因为这样的调用可以异步进行.
| 归档时间: | 
 | 
| 查看次数: | 10770 次 | 
| 最近记录: |