类似数组的容器实现与严格的别名



我正在尝试实现一个类似数组的容器,它有一些特殊的需求和std::vector接口的子集。以下是代码摘录:

template<typename Type>
class MyArray
{
public:
explicit MyArray(const uint32_t size) : storage(new char[size * sizeof(Type)]), maxElements(size) {}
MyArray(const MyArray&) = delete;
MyArray& operator=(const MyArray&) = delete;
MyArray(MyArray&& op) { /* some code */ }
MyArray& operator=(MyArray&& op) { /* some code */ }
~MyArray() { if (storage != nullptr) delete[] storage; /* No explicit destructors. Let it go. */  }
Type* data() { return reinterpret_cast<Type*>(storage); }
const Type* data() const { return reinterpret_cast<const Type*>(storage); }
template<typename... Args>
void emplace_back(Args&&... args)
{
assert(current < maxElements);
new (storage + current * sizeof(Type)) Type(std::forward<Args>(args)...);
++current;
}
private:
char* storage = nullptr;
uint32_t maxElements = 0;
uint32_t current = 0;
};

它在我的系统上运行得很好,但取消引用data返回的指针似乎违反了严格的别名规则。这也是下标运算符、迭代器等的天真实现的一种情况。

那么,在不违反严格的别名规则的情况下,实现由char数组支持的容器的正确方法是什么呢?据我所知,使用std::aligned_storage只会提供正确的对齐,但不会避免代码被依赖于严格混叠的编译器优化破坏。此外,出于性能考虑,我不想使用-fno-strict-aliasing和类似的标志。

例如,考虑下标运算符(为简洁起见,它是C++中关于UB的文章中的经典代码片段:

Type& operator[](const uint32_t idx)
{
Type* ptr = reinterpret_cast<Type*>(storage + idx * sizeof(ptr)); // Cast is OK.
return *ptr; // Dereference is UB.
}

在没有发现我的程序被破坏的风险的情况下,实现它的正确方法是什么?标准容器是如何实现的?在所有编译器中,是否存在未记录的编译器内部函数的欺骗行为?

有时,我看到代码通过void*进行两次静态强制转换,而不是一次重新解释强制转换:

Type* ptr = static_cast<Type*>(static_cast<void*>(storage + idx * sizeof(ptr)));

怎么比重新诠释演员阵容更好呢?对我来说,它没有解决任何问题,但看起来过于复杂。

但取消引用数据返回的指针似乎违反了严格的别名规则

我不同意。

char* storagedata()返回的指针都指向同一内存区域。

这无关紧要。指向同一对象的多个指针不会违反混叠规则。

此外,下标运算符将。。。取消引用不兼容类型的指针,即UB。

但是对象不是不兼容的类型。在emplace_back中,使用placement new将Type的对象构造到内存中。假设没有代码路径可以避免这种新的放置,因此假设下标运算符返回一个指向这些对象之一的指针,那么取消引用Type*的指针是很好的定义,因为它指向兼容的Type的对象。

这就是与指针别名相关的内容:内存中对象的类型,以及被取消引用的指针的类型。从中转换去引用指针的任何中间指针都与别名无关。


请注意,您的析构函数不会调用在storage内构造的对象的析构器,因此,如果Type不是平凡的可析构函数,那么行为是未定义的。


Type* ptr = reinterpret_cast<Type*>(storage + idx * sizeof(ptr));

sizeof错误。您需要的是sizeof(Type)sizeof *ptr。或者更简单地说

auto ptr = reinterpret_cast<Type*>(storage) + idx;

有时我看到通过void*使用两个静态强制转换而不是一个重新解释强制转换的代码:它怎么比重新解释强制执行更好?

我想不出任何情况下的行为会有所不同。

最新更新