从强制转换到基址的指针是否保证是指向派生对象内存区域的指针
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
您的断言应该成立并且代码应该是可移植的。
鉴于此代码:
#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
您的断言应该成立并且代码应该是可移植的。