我正在设计一个非常小的偏移指针类。它显示优化级别相关的输出,表明未定义的行为。但为什么会这样,又该如何解决呢?参见示例
#include <cstddef>
#include <iostream>
template<typename T>
class NullableOffsetPtr
{
public:
NullableOffsetPtr(T const* ptr) :
offset_{
ptr == nullptr ? std::ptrdiff_t{1} : reinterpret_cast<std::byte const*>(ptr) - reinterpret_cast<std::byte const*>(this)}
{
}
T* get()
{
return offset_ == 1 ? nullptr : reinterpret_cast<T*>(reinterpret_cast<std::byte*>(this) + offset_);
}
private:
std::ptrdiff_t offset_; // Using units of bytes, as alignments of *this and *ptr may not match.
};
class Sample
{
};
int main(int argc, char* argv[])
{
Sample sample;
NullableOffsetPtr<Sample> ptr{&sample};
std::cout << "(ptr.get() == &sample): " << (ptr.get() == &sample) << std::endl;
std::cout << "ptr.get(): " << ptr.get() << std::endl;
std::cout << "&sample : " << &sample << std::endl;
return 0;
}
使用所有优化级别 -O1 及以上,gcc 编译的可执行文件的输出为
(ptr.get() == &sample): 0
ptr.get(): 0x7ffe503cd42f
&sample : 0x7ffe503cd42f
尽管各个值(每次运行都不同)显示为相同,但指针比较的结果为 false!
当然,
reinterpret_cast
立即敲响了UB的铃声,但根据我的理解,应该在reinterpret_cast中的类型别名的范围内。
以下修改将“修复”此行为,但不是令人满意的解决方案,并且不能解释上面的问题:
另请参阅 godbolt 示例
如果两个对象都是同一顶级对象的子对象(可能是间接的),则只能使用指针算术从另一个对象访问一个对象。 (例如,如果您将两个对象设置为同一类实例的非静态数据成员,GCC 就会开始运行。)
相关措辞如下:
[expr.add]/4.2
表示 +
只能用于在同一数组的元素之间移动指针。同样,-
只能减去同一数组元素之间的指针。[basic.types.general]/4
表示您可以将对象视为 unsigned char
的数组。[basic.lval]/11.3
祝福char
、unsigned char
和std::byte
,允许通过指针/引用访问任何对象,忽略严格的别名。大家普遍认为,你不能在
uintptr_t
中进行指针运算来解决[expr.add]/4.2
,但我现在找不到相关的措辞。
关于生命周期和别名的措辞总体来说是一团糟,如果你挖掘得太深,很多事情在技术上都是 UB (或暗示如此),而在实践中却没有得到执行。例如,有一个可以通过指针访问字节的概念。它用在 std::launder
的描述中,据我所知,没有在其他地方使用过(该标准实际上并没有在任何地方说访问“无法访问”的字节实际上是 UB)。
Sample
是一个空类,由于这在您的代码中没有明显的影响,因此它可以与
NullableOffsetPtr
共享一个地址。 因此,打印时
&sample
和
ptr.get()
可以提供相同的输出。但是,
NullableOffsetPtr
的构造函数中的指针减法是未定义的行为:
当两个指针表达式 P 和 Q 相减时,结果的类型是实现定义的有符号整型;该类型应与标头中定义为 std::ptrdiff_t 的类型相同([support.types.layout])。
如果 P 和 Q 的计算结果均为空指针值,则结果为 0。
- 否则,如果 P 和 Q 分别指向同一数组对象 x 的数组元素 i 和 j,则表达式 P - Q 的值为 i−j。
- 否则,行为未定义。