类数组容器实现与严格别名
Array-like container implementation vs strict aliasing
我正在尝试实现一个具有一些特殊要求和 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
返回的指针似乎违反了 strict aliasing 规则。这也是下标运算符、迭代器等的简单实现的情况。
那么,在不违反严格的别名规则的情况下,实现由 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)));
它比重新解释演员有什么好处?对我来说,它没有解决任何问题,而且看起来过于复杂。
but dereferencing a pointer returned by data seems to violate strict aliasing rules
我不同意。
Both char* storage
and a pointer returned by data()
point to the same region of memory.
这无关紧要。指向同一个对象的多个指针不违反别名规则。
Moreover, subscript operator will ... dereference a pointer of incompatible type, which is UB.
但是对象不是不兼容的类型。在 emplace_back
中,你使用 placement new 将 Type
的对象构造到内存中。假设没有代码路径可以避免这个新的放置,因此假设下标运算符 returns 是指向这些对象之一的指针,那么取消引用 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;
Sometimes I see code with two static casts through void*
instead of one reinterpret cast: How is it better than reinterpret cast?
我想不出任何行为会有所不同的情况。
我正在尝试实现一个具有一些特殊要求和 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
返回的指针似乎违反了 strict aliasing 规则。这也是下标运算符、迭代器等的简单实现的情况。
那么,在不违反严格的别名规则的情况下,实现由 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)));
它比重新解释演员有什么好处?对我来说,它没有解决任何问题,而且看起来过于复杂。
but dereferencing a pointer returned by data seems to violate strict aliasing rules
我不同意。
Both
char* storage
and a pointer returned bydata()
point to the same region of memory.
这无关紧要。指向同一个对象的多个指针不违反别名规则。
Moreover, subscript operator will ... dereference a pointer of incompatible type, which is UB.
但是对象不是不兼容的类型。在 emplace_back
中,你使用 placement new 将 Type
的对象构造到内存中。假设没有代码路径可以避免这个新的放置,因此假设下标运算符 returns 是指向这些对象之一的指针,那么取消引用 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;
Sometimes I see code with two static casts through
void*
instead of one reinterpret cast: How is it better than reinterpret cast?
我想不出任何行为会有所不同的情况。