安全(且无成本)地重新解释大小数据
Safe (and costless) reinterpretation of sized data
我想编写自己的"小向量"类型,第一个障碍是弄清楚如何实现堆栈存储。
我偶然发现了std::aligned_storage
,它似乎是专门为实现任意堆栈存储而设计的,但我非常不清楚什么是安全的,什么是不安全的。 cppreference.com 方便地有一个使用std::aligned_storage
的示例,我将在这里重复:
template<class T, std::size_t N>
class static_vector
{
// properly aligned uninitialized storage for N T's
typename std::aligned_storage<sizeof(T), alignof(T)>::type data[N];
std::size_t m_size = 0;
public:
// Create an object in aligned storage
template<typename ...Args> void emplace_back(Args&&... args)
{
if( m_size >= N ) // possible error handling
throw std::bad_alloc{};
// construct value in memory of aligned storage
// using inplace operator new
new(&data[m_size]) T(std::forward<Args>(args)...);
++m_size;
}
// Access an object in aligned storage
const T& operator[](std::size_t pos) const
{
// note: needs std::launder as of C++17
return *reinterpret_cast<const T*>(&data[pos]);
}
// Delete objects from aligned storage
~static_vector()
{
for(std::size_t pos = 0; pos < m_size; ++pos) {
// note: needs std::launder as of C++17
reinterpret_cast<T*>(&data[pos])->~T();
}
}
};
这里的几乎所有内容对我来说都是有意义的,除了那两条评论说:
注意:自 C++17 日起需要
std::launder
"as of"条款本身相当令人困惑;这是否意味着
此代码不正确或不可移植,便携式版本应使用
std::launder
(在 C++17 中引入),或C++17 对内存混叠/重新解释规则进行了重大更改?
除此之外,从性能的角度来看,std::launder
的使用让我感到担忧。我的理解是,在大多数情况下,编译器可以对内存别名做出非常强烈的假设(特别是指向不同类型的指针不引用相同的内存),以避免冗余内存负载。
我想在编译器方面保持这种级别的混叠确定性(即从我的小向量访问T
与对普通T[]
或T *
的访问相同),尽管从我读到的std::launder
,这听起来像是一个完整的混叠屏障,即编译器必须假设它对洗涤指针的来源一无所知。我担心在每次operator[]
都使用它会干扰通常的负载存储消除。
也许编译器比这更聪明,或者我首先误解了std::launder
的工作方式。无论如何,我真的不知道我用这种级别的C++记忆黑客在做什么。很高兴知道我必须为这个特定的用例做些什么,但如果有人能启发我更一般的规则,那将不胜感激。
更新(进一步探索)
进一步阅读这个问题,我目前的理解是,除非使用std::launder
,否则我粘贴在此处的示例在标准下具有未定义的行为。也就是说,较小的实验证明了我认为是未定义的行为,并没有表明Clang或GCC像标准所允许的那样严格。
让我们从在混叠指针的情况下明显不安全的东西开始:
float definitelyNotSafe(float *y, int *z) {
*y = 5.0;
*z = 7;
return *y;
}
正如人们所期望的那样,Clang和GCC(启用了优化和严格别名)都生成始终返回5.0
的代码;如果传递y
并z
该别名,则该函数将不会具有"期望"的行为:
.LCPI1_0:
.long 1084227584 # float 5
definitelyNotSafe(float*, int*): # @definitelyNotSafe(float*, int*)
mov dword ptr [rdi], 1084227584
mov dword ptr [rsi], 7
movss xmm0, dword ptr [rip + .LCPI1_0] # xmm0 = mem[0],zero,zero,zero
ret
但是,当编译器可以看到别名指针的创建时,事情会变得有点奇怪:
float somehowSafe(float x) {
// Make some aliasing pointers
auto y = &x;
auto z = reinterpret_cast<int *>(y);
*y = 5.0;
*z = 7;
return x;
}
在这种情况下,Clang和GCC(带有-O3
和-fstrict-aliasing
)都生成代码,通过z
观察x
的修改:
.LCPI0_0:
.long 7 # float 9.80908925E-45
somehowSafe(float): # @somehowSafe(float)
movss xmm0, dword ptr [rip + .LCPI0_0] # xmm0 = mem[0],zero,zero,zero
ret
也就是说,编译器并不能保证"利用"未定义的行为;毕竟它是未定义的。在这种情况下,假设*z = 7
没有效果是没有利润的。那么,如果我们"激励"编译器利用严格的别名呢?
int stillSomehowSafe(int x) {
// Make some aliasing pointers
auto y = &x;
auto z = reinterpret_cast<float *>(y);
auto product = float(x) * x * x * x * x * x;
*y = 5;
*z = product;
return *y;
}
假设*z = product
对*y
的值没有影响显然对编译器有利;这样做将允许编译器将这个函数简化为一个总是返回5
的函数。尽管如此,生成的代码没有做出这样的假设:
stillSomehowSafe(int): # @stillSomehowSafe(int)
cvtsi2ss xmm0, edi
movaps xmm1, xmm0
mulss xmm1, xmm0
mulss xmm1, xmm0
mulss xmm1, xmm0
mulss xmm1, xmm0
mulss xmm1, xmm0
movd eax, xmm1
ret
我对这种行为感到相当困惑。我知道我们对编译器在存在未定义行为时会做什么的保证为零,但我也感到惊讶的是,Clang和GCC都没有对这些优化更积极。这让我怀疑我是否误解了标准,或者 Clang 和 GCC 对"严格混叠"的定义是否较弱(和记录在案)。
std::launder
主要是为了处理像std::optional
或你的small_vector
这样的场景,其中相同的存储可能会随着时间的推移被重用于多个对象,并且这些对象可能是const
的,或者可能有const
或引用成员。
它对优化器说"这里有一个T
,但它可能与您之前T
不同,因此const
成员可能已更改,或者引用成员可能引用其他内容"。
在没有const
或引用成员的情况下,std::launder
什么都不做,也是不必要的。见 http://eel.is/c++draft/ptr.launder#5
- 请解释"函数1(p1,p2,p3);"的输出
- C++将浮点指针值舍入为小数位数
- 请解释这句话(cout<<1+int((a<b)^((b-a)&1) )<<endl
- 从给定的 I 和 D 序列中形成最小数
- 如何防止 c++ 在从浮点型转换为双精度型(不适用于 IO)时添加额外的小数?
- 被解释为低级别const的const对象的地址
- 计算每个节点的树高,帮助我解释这个代码解决方案
- MSVC将仅移动结构参数解释为指针
- 内联程序集printf将整数解释为地址
- 有人能解释一下为什么下界是这样工作的吗C++的
- Visual Studio(或任何其他工具)能否将地址解释为调用堆栈(boost上下文)的开头
- 我是c ++的新手,你能解释一下在这种情况下的指针吗
- 有人能为我解释一下C++代码吗
- 你能解释一下什么运行时错误是如何解决它的吗?
- 请解释字谜的代码,我看不懂计数器数组,每个值已经是0
- 使用 std::index_sequence 初始化具有固定大小数组成员的 POD 结构容器
- 计算 PI 最多 42 位小数
- 有人可以向我解释为什么控制台输出 0 吗?
- 向我解释CIN在数量变量的小数价通过时如何工作
- 对指向固定大小数组的指针数组的内存分配的解释