通过 std::unique_ptr 的 LazyArray 模板,这是双重检查习语的正确实现吗?

Posted

技术标签:

【中文标题】通过 std::unique_ptr 的 LazyArray 模板,这是双重检查习语的正确实现吗?【英文标题】:LazyArray template via std::unique_ptr, is this a proper implementation of the double-check idiom? 【发布时间】:2020-03-16 09:56:12 【问题描述】:

此代码是否使用 C++11 atomic 安全地实现了双重检查习惯用法? 我在“C++ 编程语言第 4 版”中看到。一个使用atomic<bool> 的示例,我尽力保持相同,但我不自信。另外,这个可以改进吗?

由于once_flag 的存储开销,我想避免使用call_once

这个“LazyArray”是为了减少内存而编写的,目的是用对客户端代码的最小更改来替换数组(预计只有一小部分元素会被访问)。从多个线程访问数组是不争的事实,并且由于性能的原因,广泛的锁定会出现问题。

/**
 * Lazy creation of array elements.
 * They will only be created when they are referenced.
 *
 * This "array" does not support iteration because it can have holes
 *
 * Array bounds isn't checked to keep it fast.
 * Destruction of the array destroys all the T objects (via the unique_ptr d'tor)
 */

template<class T, size_t size>
class LazyArray

    typedef LazyArray<T, size> mytype;
public:
    // copying is not allowed (unlike regular arrays)
    LazyArray(const LazyArray&) = delete;
    LazyArray& operator=(const LazyArray&) = delete;

    LazyArray()

    T& operator[](size_t i)
    
        return at(i);
    

    const T& operator[](size_t i) const
    
        return const_cast<mytype *>(this)->at(i);
    
private:
    using guard = std::lock_guard<std::mutex>;

    // get T object at index i by reference
    T& at(size_t i) // only non-const variant is implemented, const version will use const_cast
    
        auto &p = m_array[i];
        std::atomic<T*> ap(p.get());

        if(!ap) // object not created yet
        
            guard g(mtx);
            ap = p.get();
            if(!ap)
                p.reset(new T);
        
        return *p;
    

    std::unique_ptr<T> m_array[size];
    std::mutex mtx;
;


【问题讨论】:

请记住,只有当 sizeof(T) 远大于 sizeof(unique_ptr&lt;T&gt;)(通常是指针的大小,例如 8 字节)时,这才有意义。您已经有一个 size unique_ptr 对象数组使用了空间。引入另一个级别的间接也不利于性能,并且如果您遍历数组的顺序元素,可能会导致更糟糕的局部性。 理想情况下,您应该为T 的数组直接分配连续内存,但不要触摸它,因此操作系统的惰性分配机制可以为您发挥作用(保持新鲜来自操作系统的虚拟内存页面延迟为零/COW 映射到操作系统的零页面),直到第一次读取或写入。但是你需要知道何时使用placement-new通过单独的簿记或T中的哨兵字段来构造新元素。即知道T的某些部分在已经构造的对象中不能是0 你应该使用 const 和 const_cast 来实现你的非常量函数,而不是相反。非 const 函数可以抛弃 const。由于通过 const 函数中的 const_cast 进行成员修改,当前代码中的问题是未定义的行为。 @Darhuuk,也许在这种情况下应该删除 const 变体,因为如果 LazyArray 是 const,这意味着每个 unique_ptr 也是 const 并且不能安全地重置对吧?没有 const 调用非 const 变体,两者都调用 at() @CplusPuzzle 我的观点是你的 const 函数修改了类的成员(在这种情况下是互斥锁,在非常量函数 at 中)。通常不会编译。因为const_cast 确实如此,但现在它是未定义的行为。 【参考方案1】:

理论上不会。

支票:

        std::atomic<T*> ap(p.get());
        if(!ap) // object not created yet

应该获取对应这个版本:

                p.reset(new T);

事实上并非如此。

它可能会如何失败:

    对象构造的某些部分 new T 在其他线程看到 p 分配了新值后可能对它可见 分配 p.reset 可能会被破坏,因为它是非原子的

可能在某些平台上。

    如果存储可以在其他存储之后重新排序(在 x86 上不会发生),或者如果编译器决定自行交换这些存储(不太可能),则会发生这种情况 指针大小的变量写入不会被破坏

使用std::atomic 解决这两个问题:

   std::atomic<T*> m_array[size];

当然,您必须手动删除 T*。 我建议基于std::atomic&lt;T*&gt; 创建atomic_unique_ptr 并在那里使用它

【讨论】:

感谢@Alex。是不是保证p只有在T对象完全构造好之后才能得到新的T的地址? @CplusPuzzle,没有。商店可以在其他商店之后订购。在排序较弱的平台(如 ARM)上,可能由于硬件行为而发生。请参阅此处的表格:en.wikipedia.org/wiki/Memory_ordering#Runtime_memory_ordering 可能由于编译器优化而发生,尽管可能性不大,请参阅“as-if 下的优化”en.wikipedia.org/wiki/Memory_ordering#Optimization_under_as-if。 好的,谢谢@Alex。保证每个数组索引只创建一个对象,并且如果数组元素是纯数据,则无法访问无效内存是否正确? 只有一个对象 - 是的。无法访问无效内存 - 不,这可能是由于上述重新排序。简单的数据减少了这种情况发生的实际机会,但并没有消除理论上的可能性。例如,调试new 可能会初始化为某种模式,并且此初始化可能会超出p 的分配。

以上是关于通过 std::unique_ptr 的 LazyArray 模板,这是双重检查习语的正确实现吗?的主要内容,如果未能解决你的问题,请参考以下文章

智能指针unique_ptr用法

[C++11]独占的智能指针unique_ptr的初始化和使用

指向 std::unique_ptr 的内容

与 std::unique_ptr 相关的错误

一些 std::unique_ptr 使用和“陷阱”

为啥 std::unique_lock 改变 std::unique_ptr?