通过 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<T>)
(通常是指针的大小,例如 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<T*>
创建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 模板,这是双重检查习语的正确实现吗?的主要内容,如果未能解决你的问题,请参考以下文章