std::launder 和严格的别名规则
std::launder and strict aliasing rule
考虑这段代码:
void f(char * ptr)
{
auto int_ptr = reinterpret_cast<int*>(ptr); // <---- line of interest
// use int_ptr ...
}
void example_1()
{
int i = 10;
f(reinterpret_cast<char*>(&i));
}
void example_2()
{
alignas(alignof(int)) char storage[sizeof(int)];
new (&storage) int;
f(storage);
}
来自 example_1
的电话的兴趣行:
Q1: 在调用方,char
指针为我们的整数指针设置了别名。这是有效的。但是将它转换回 int
是否也有效?我们知道 int
在其生命周期内存在,但考虑到函数是 在另一个翻译单元 中定义的(未启用链接时优化)并且上下文未知。然后所有编译器看到的是:int
指针想要别名 char
指针,这违反了严格的别名规则。那么允许吗?
Q2:考虑不允许。我们在 C++17 中得到了 std::launder
。它是一种指针优化屏障,主要用于访问放置 new
到其他类型对象的存储中的对象,或者当涉及 const
成员时。我们可以用它来给编译器一个提示并防止未定义的行为吗?
来自 example_2 的电话的兴趣行:
Q3:这里应该需要std::launder
,因为这是Q2中描述的std::launder
用例,对吧?
auto int_ptr = std::launder(reinterpret_cast<int*>(ptr));
但再考虑一下f
是在另一个翻译单元中定义的。编译器如何知道我们在调用端发生的放置 new
?编译器(只看到函数f
)如何区分example_1
和example_2
?或者以上只是假设,因为严格的别名规则会排除一切(记住,char*
到 int*
是不允许的)并且编译器可以做它想做的事?
后续问题:
Q4: 如果上面的所有代码由于别名规则都是错误的,请考虑更改函数 f
以采用 void 指针:
void f(void* ptr)
{
auto int_ptr = reinterpret_cast<int*>(ptr);
// use int_ptr ...
}
然后我们没有别名问题,但仍然存在 example_2
的 std::launder
情况。我们是否更改了调用方并将我们的 example_2
函数重写为:
void example_2()
{
alignas(alignof(int)) char storage[sizeof(int)];
new (&storage) int;
f(std::launder(storage));
}
或者函数 f
中的 std::launder
是否足够?
严格的别名规则是对实际用于访问对象的左值类型的限制。对于该规则而言,重要的是 a) 对象的实际类型,以及 b) 用于访问的泛左值的类型。
指针经过的中间转换是无关紧要的,只要它们保留指针值即可。 (这是双向的;就此而言,再多的巧妙转换或洗钱也无法解决严格的混叠违规问题。)
只要 ptr
实际上指向 int
类型的对象,f
就有效,假设它通过 int_ptr
访问该对象而无需进一步转换。
example_1
写的有效; reinterpret_cast
s 不改变指针值。
example_2
是无效的,因为它给 f
一个实际上并不指向 int
对象的指针(它指向storage
数组)。参见
您不需要 std::launder 在函数 f() 中。尽可能通用,函数 f() 可以与任何指针一起使用:脏(需要清洗)或不脏。它只在调用端已知,所以你必须使用这样的东西:
void example_2()
{
alignas(alignof(int)) char storage[sizeof(int)];
new (&storage) int; // char[] has gone, now int there
f(std::launder(reinterpret_cast<int*>(storage))); // assiming f(void* p)
}
而 f() 本身是另一回事。正如您提到的,考虑将 f() 放在共享库中,因此根本没有任何上下文假设。
考虑这段代码:
void f(char * ptr)
{
auto int_ptr = reinterpret_cast<int*>(ptr); // <---- line of interest
// use int_ptr ...
}
void example_1()
{
int i = 10;
f(reinterpret_cast<char*>(&i));
}
void example_2()
{
alignas(alignof(int)) char storage[sizeof(int)];
new (&storage) int;
f(storage);
}
来自 example_1
的电话的兴趣行:
Q1: 在调用方,char
指针为我们的整数指针设置了别名。这是有效的。但是将它转换回 int
是否也有效?我们知道 int
在其生命周期内存在,但考虑到函数是 在另一个翻译单元 中定义的(未启用链接时优化)并且上下文未知。然后所有编译器看到的是:int
指针想要别名 char
指针,这违反了严格的别名规则。那么允许吗?
Q2:考虑不允许。我们在 C++17 中得到了 std::launder
。它是一种指针优化屏障,主要用于访问放置 new
到其他类型对象的存储中的对象,或者当涉及 const
成员时。我们可以用它来给编译器一个提示并防止未定义的行为吗?
来自 example_2 的电话的兴趣行:
Q3:这里应该需要std::launder
,因为这是Q2中描述的std::launder
用例,对吧?
auto int_ptr = std::launder(reinterpret_cast<int*>(ptr));
但再考虑一下f
是在另一个翻译单元中定义的。编译器如何知道我们在调用端发生的放置 new
?编译器(只看到函数f
)如何区分example_1
和example_2
?或者以上只是假设,因为严格的别名规则会排除一切(记住,char*
到 int*
是不允许的)并且编译器可以做它想做的事?
后续问题:
Q4: 如果上面的所有代码由于别名规则都是错误的,请考虑更改函数 f
以采用 void 指针:
void f(void* ptr)
{
auto int_ptr = reinterpret_cast<int*>(ptr);
// use int_ptr ...
}
然后我们没有别名问题,但仍然存在 example_2
的 std::launder
情况。我们是否更改了调用方并将我们的 example_2
函数重写为:
void example_2()
{
alignas(alignof(int)) char storage[sizeof(int)];
new (&storage) int;
f(std::launder(storage));
}
或者函数 f
中的 std::launder
是否足够?
严格的别名规则是对实际用于访问对象的左值类型的限制。对于该规则而言,重要的是 a) 对象的实际类型,以及 b) 用于访问的泛左值的类型。
指针经过的中间转换是无关紧要的,只要它们保留指针值即可。 (这是双向的;就此而言,再多的巧妙转换或洗钱也无法解决严格的混叠违规问题。)
只要ptr
实际上指向 int
类型的对象,f
就有效,假设它通过 int_ptr
访问该对象而无需进一步转换。
example_1
写的有效; reinterpret_cast
s 不改变指针值。
example_2
是无效的,因为它给 f
一个实际上并不指向 int
对象的指针(它指向storage
数组)。参见
您不需要 std::launder 在函数 f() 中。尽可能通用,函数 f() 可以与任何指针一起使用:脏(需要清洗)或不脏。它只在调用端已知,所以你必须使用这样的东西:
void example_2()
{
alignas(alignof(int)) char storage[sizeof(int)];
new (&storage) int; // char[] has gone, now int there
f(std::launder(reinterpret_cast<int*>(storage))); // assiming f(void* p)
}
而 f() 本身是另一回事。正如您提到的,考虑将 f() 放在共享库中,因此根本没有任何上下文假设。