【问题标题】:Is the pointer from casting to base gurenteed to be a pointer into the memory region of the derived object从转换到基址的指针是否保证是指向派生对象内存区域的指针
【发布时间】:2022-01-01 05:51:38
【问题描述】:

鉴于此代码:

#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?
}

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

【问题讨论】:

  • 您不想放置新位置而不是memcpystatic_cast&lt;base*&gt; 应该不是问题,但reinterpret_cast&lt;derived*&gt; 应该是强制转换(缓冲区中没有对象)。
  • @Jarod42 在我的实际代码中,我使用placement new 并手动调用dtor 和placement copy 构造等,这只是为了举例。此外,memcpy 应该是有效的 afaik,该 int 的值将是垃圾,因为它未初始化但它的数据仍被分配(在堆栈上)
  • 读取未初始化x 是另一个问题(作为对齐的潜在问题);我的观点是,在缓冲区data 中没有对象(任何derived 实例的生命周期都没有开始)。不过,placement new 是正确的,这只是您对“有问题”的问题的简化。

标签: c++ pointers standards


【解决方案1】:

从转换到基类的指针是否被确定为指向派生对象的内存区域的指针

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

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

【讨论】:

  • 假设树中没有虚拟继承,定义了吗?
【解决方案2】:

可能错误的对齐方式

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

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

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

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

(godbolt demonstrating the problem)

如果你愿意,你也可以使用std::aligned_storage

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

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

您可以通过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);

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

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

但是,您可以依靠以下几点:

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

    当应用于类类型时,结果是该类的完整对象占用的字节数,包括将此类对象放入数组所需的任何额外填充。

  • placement new 将构造一个对象并返回一个指向它的指针,该指针位于给定缓冲区内。
  • static_cast&lt;&gt; 始终定义为基类

实际答案

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

从placement new 返回的指针也是如此——它可能指向数组的开头或其他地方(例如数组分配),但它总是在数据数组中。

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-11-04
    • 1970-01-01
    • 1970-01-01
    • 2021-05-20
    • 1970-01-01
    相关资源
    最近更新 更多