连续原始字节块的内存对齐和严格别名

Memory alignment and strict aliasing for continuous block of raw bytes

从 C++ 标准规则的角度来看,我有一个关于使用相同的连续原始字节块作为各种类型对象的存储的问题。 考虑我们创建连续的原始字节块,f.e.

void *data = ::operator new(100); // 100 bytes of raw data - not typed

然后我们可以像这样使用这个内存:

template<class T>
T* get(std::size_t bshift)
{
    return static_cast<T*>(
        reinterpret_cast<void*>(
            reinterpret_cast<unsigned char*>(data) + bshift
        )
    );
}

  1. 安全还是UB?为什么?
float &fvalue2 = *get<float>(sizeof(float));
fvalue2 = 0.02f;

  1. 安全还是UB?为什么?
float &fvalue_reserve = *get<float>(sizeof(float)*2 + 1);
fvalue = 0.03f;

  1. 安全还是UB?为什么?
double &dvalue = *get<double>(sizeof(float)*3 + 1);
dvalue = 0.04;

  1. 安全还是UB?为什么?
// assume that somehow there is no unused internal padding in struct
struct POD_struct{
    float fvalue1;                   //[0..sizeof(float)]
    float fvalue2;                   //[sizeof(float)..sizeof(float)*2]
    char reserved[sizeof(float) + 1];//[sizeof(float)*2..sizeof(float)*3+1]
    double dvalue;                   //[sizeof(float)*3+1..sizeof(float)*3+1+sizeof(double)]
};

POD_struct pod_struct;
std::memcpy(&pod_struct, get<void*>(0), sizeof(POD_struct));
pod_struct.fvalue2 == 0.02f; //true ??
pod_struct.dvalue == 0.04f; //true ??

  1. 安全还是UB?为什么?和№4有什么不同吗?
// assume that somehow there is no unused internal padding in struct
struct POD_struct{
    float fvalue1;                   //[0..sizeof(float)]
    float fvalue2;                   //[sizeof(float)..sizeof(float)*2]
    char reserved[sizeof(float) + 1];//[sizeof(float)*2..sizeof(float)*3+1]
    double dvalue;                   //[sizeof(float)*3+1..sizeof(float)*3+1+sizeof(double)]
};

POD_struct pod_struct;
std::memcpy(&pod_struct, get<POD_struct*>(0), sizeof(POD_struct));
pod_struct.fvalue2 == 0.02f; //true ??
pod_struct.dvalue == 0.04f; //true ??

  1. 如果我们“填充”由 data 指针指向的内存而不是从零偏移量 (f.e. 1, 2, 3?) 然后 memcpy 是否会有显着差异从该偏移量到 POD_struct 对象?
  2. 是否可以安全地假设如果我们管理所需大小的连续原始内存缓冲区块以适合所有没有填充的 POD 成员(按 1 字节对齐),那么可以将其解释为任何 POD 类型?重用原始内存并将其解释为另一种 POD 类型存储是否安全?

首先,您的问题并不清楚,但我假设您显示的各个片段之间没有其他代码。

代码段 1. 具有未定义的行为,因为指针 get 将 return 实际上不能指向 float 对象。 ::operator new 确实隐式创建对象和 return 指向合适创建对象的指针,但该对象必须是 unsigned char 数组的 unsigned char 对象部分才能给出reinterpret_cast<unsigned char*>(data) + bshift 中的指针算法定义了行为。

但是,get<float>(sizeof(float)); 的 return 值也将是指向 unsigned char 对象的指针。通过 float 泛左值写入 unsigned char 违反了别名规则。

这可以通过在 return 从 get 获取指针之前使用 std::launder 或通过显式创建对象更好地解决:

template<class T>
T* get(std::size_t bshift)
{
    return new(reinterpret_cast<unsigned char*>(data) + bshift) T;
}

虽然这会在每次调用时创建一个具有不确定值的新对象。

std::launder 在这里不需要创建新对象就足够了,因为 ::operator new 可以隐式创建一个 unsigned char 数组,该数组为 float 对象提供存储,该对象也是 implicitly-created。 (假设以这种方式使用的所有对象都适合存储,正确对齐(见下文),不重叠并且 implicit-lifetime types):

template<class T>
T* get(std::size_t bshift)
{
    return std::launder(reinterpret_cast<T*>(reinterpret_cast<unsigned char*>(data) + bshift));
}

但是,如果 alignof(float) != 1(这很可能是真的),即使进行了此修改,2. 和 3. 仍具有未定义的行为。您不能隐式或显式地创建和启动对齐错误的对象的生命周期。 (尽管在技术上可能在不开始其生命周期的情况下显式创建对齐错误的对象。)

对于 4. 和 5.,假设以上不是由于未对齐或 out-of-bounds 访问而导致的未定义行为,并且考虑到问题中的假设,我认为这些片段应该具有定义的行为。但请注意,极不可能满足这些要求。

对于 6.,如果您抵消了您需要的所有内容,请再次注意不要进行 out-of-bounds 分配并且不要违反任何相关类型的对齐方式。 (包括POD_struct 5 的对齐。)

对于7,表述很模糊,所以我不太清楚你的意思。但是您通常不能明确地 内存解释为与实际不同的类型。在您的示例 4. 和 5. 中,您正在 复制 对象表示,这是不同的。


再次明确:实际上,由于对齐违规,您的代码具有 UB。这甚至可能扩展到允许未对齐访问的平台,因为编译器可能会根据指针对齐的假设来优化代码。编译器可能会提供类型注释来指示指针可能未对齐。如果您想在实践中实现未对齐访问,则需要使用这些或其他工具。