从强制转换到基址的指针是否保证是指向派生对象内存区域的指针

Is the pointer from casting to base gurenteed to be a pointer into the memory region of the derived object

鉴于此代码:

#include <cassert>
#include <cstring>

struct base{
    virtual ~base() = default;
};

class derived: public base{
public:
    int x;
};

using byte = unsigned char;


int main() {
    byte data[sizeof(derived)];
    derived d;
    memcpy(data, &d, sizeof(derived));
    base* p = static_cast<base*>(reinterpret_cast<derived*>(data));
    const auto offset = (long)data - (long)p;
    assert(offset < sizeof(derived)); // <-- Is this defined?
}

正如我的评论所问,这是标准定义的行为吗?即转换为 base 是否保证指向被派生被转换所占据的区域的指针?根据我的测试,它适用于 gcc 和 clang,但我想知道它是否也适用于跨平台(显然这个版本采用 64 位指针)

Is the pointer from casting to base gurenteed to be a pointer into the memory region of the derived object

一般情况下不一定。如果基是虚拟的,并且所讨论的派生对象不是最派生的对象,那么在这种情况下,虚拟基可能在派生对象的内存之外。

但在极端情况之外,例如在示例代码中,基础子对象确实保证在派生对象中。这就是“子对象”的含义。

可能对齐错误

您的 data 数组是一个字符数组,因此它的对齐方式为 1 个字节。
但是,您的 class 包含一个 int 成员,因此它的对齐至少为 4 个字节。

所以你 data 数组没有充分对齐,甚至无法包含派生对象。

您可以通过提供至少 derived 或更高的数据数组对齐方式轻松解决此问题,例如:

alignas(alignof(derived)) byte data[sizeof(derived)];

(godbolt demonstrating the problem)

如果需要,您也可以使用 std::aligned_storage

在 classes 上使用 memcpy 并不总是安全的

在 classes 上使用 memcpy 仅当它们是 trivially copyable 时才有效(因此按字节复制与调用复制构造函数相同)。由于虚拟析构函数,您的 class 不可简单复制,因此 memcpy 不允许复制 class.

您可以使用 std::is_trivially_copyable_v 轻松检查:

std::cout << std::is_trivially_copyable_v<derived> << std::endl;

您可以通过调用复制构造函数而不是使用 memcpy:

轻松解决此问题
alignas(alignof(derived)) char data[sizeof(derived)];
derived d;
derived* derivedInData = new (data) derived(d);

虚拟继承、多重继承和其他恶作剧

classes 在内存中的布局方式是 implementation-defined,因此您基本上无法保证编译器将如何布局您的 class 层次结构。

不过,有几件事是您可以信赖的:

  • sizeof(cls) 将始终 return 大小 cls 需要,包括它的所有基础 classes,即使它使用虚拟和/或多重继承。 (sizeof)

    When applied to a class type, the result is the number of bytes occupied by a complete object of that class, including any additional padding required to place such object in an array.

  • placement new 将构造一个对象和 return 在给定缓冲区内指向它的指针。
  • static_cast<> to baseclass 总是被定义

实际答案

是的,基 class 指针必须始终指向缓冲区内的某处,因为它是派生 class 的一部分。但是它在缓冲区中的确切位置是实现定义的,所以你不应该依赖它。

从新位置 returned 指针也是如此 - 它可能指向数组的开头或其他地方(例如数组分配),但它始终在数据中数组。

只要您坚持其中一种模式:

struct base { int i; }
struct derived : base { int j; };

alignas(alignof(derived)) char data[sizeof(derived)];
derived d;
memcpy(data, &d, sizeof(derived)); // trivially copyable
derived* ptrD = reinterpret_cast<derived*>(data);
base* ptrB = static_cast<base*>(ptrD);

/

struct base { int i; virtual ~base() = default; }
struct derived : base { int j; };

alignas(alignof(derived)) char data[sizeof(derived)];
derived d;
derived* ptrD = new(data) derived(d); // not trivially copyable
base* ptrB = static_cast<base*>(ptrD);
ptrD->~derived(); // remember to call destructor

您的断言应该成立并且代码应该是可移植的。