Memory 安全(且无成本)重新解释大小数据

Memory 安全(且无成本)重新解释大小数据,memory,c++17,reinterpret-cast,stdlaunder,Memory,C++17,Reinterpret Cast,Stdlaunder,我想写我自己的“小向量”类型,第一个障碍是如何实现栈上存储 我偶然发现了std::aligned_storage,它似乎是为实现任意堆栈存储而设计的,但我不清楚什么是安全的,什么是不安全的。方便地使用std::aligned_storage,我将在这里重复: 模板 类静态向量 { //为N T正确对齐的未初始化存储 typename std::aligned_storage::type data[N]; std::size\u t m\u size=0; 公众: //在对齐存储中创建对象 模板空

我想写我自己的“小向量”类型,第一个障碍是如何实现栈上存储

我偶然发现了
std::aligned_storage
,它似乎是为实现任意堆栈存储而设计的,但我不清楚什么是安全的,什么是不安全的。方便地使用
std::aligned_storage
,我将在这里重复:

模板
类静态向量
{
//为N T正确对齐的未初始化存储
typename std::aligned_storage::type data[N];
std::size\u t m\u size=0;
公众:
//在对齐存储中创建对象
模板空位置返回(Args&…Args)
{
if(m_size>=N)//可能的错误处理
抛出std::bad_alloc{};
//在对齐存储的内存中构造值
//使用inplace操作符new
新建(&data[m_size])T(std::forward关于这个问题,我目前的理解是,我在这里粘贴的示例在标准下有未定义的行为,除非使用
std::launder
。也就是说,证明我认为未定义行为的较小实验,并没有显示Clang或GCC像标准那样严格这似乎是允许的

让我们从一些在别名指针的情况下显然不安全的东西开始:

float definitelyNotSafe(float*y,int*z){
*y=5.0;
*z=7;
返回*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){
//制作一些别名指针
自动y=&x;
自动z=重新解释铸件(y);
*y=5.0;
*z=7;
返回x;
}
在这种情况下,Clang和GCC(带有
-O3
-fstrict别名
)都会生成代码,通过
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
没有效果是没有好处的。那么如果我们“激励”编译器利用严格的别名呢

intStillsomeHowSafe(intx){
//制作一些别名指针
自动y=&x;
自动z=重新解释铸件(y);
汽车产品=浮动(x)*x*x*x*x*x*x;
*y=5;
*z=产品;
返回*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
什么都不做,也没有必要。请参见

,可悲的事实是,野生的许多代码都依赖于严格的别名冲突所导致的技术上未定义的行为。只需查看“经典”快速平方根求逆即可获得一个具体示例。