基本思想是创建一个可变大小的数组,在构造时固定,在单个分配单元中创建另一个类,以减少开销并提高效率。分配缓冲区以适合数组,另一个对象和placement new用于构造它们。为了访问数组的元素和另一个对象,使用指针算术和reinterpret_cast。 这似乎有效(至少在gcc中),但我对标准的阅读(5.2.10 Reinterpret Cast)告诉我它是一个未定义的行为。那是对的吗?如果是这样,有没有办法在没有UB的情况下实现这个设计?
完整的可编辑示例如下:http://ideone.com/C9CCa8
// a buffer contains array of A followed by B, laid out like this
// | A[N - 1] ... A[0] | B |
class A
{
size_t index;
//...
// using reinterpret_cast to get to B object
const B* getB() const
{
return reinterpret_cast<const B*>(this + index + 1);
}
};
class B
{
size_t a_count;
//...
virtual ~B() {}
// using reinterpret_cast to get to the array member
const A* getA(size_t i) const
{
return reinterpret_cast<const A*>(this) - i - 1;
}
};
// using placement new to construct all objects in raw memory
B* make_record(size_t a_count)
{
char* buf = new char[a_count*sizeof(A) + sizeof(B)];
for(auto i = 0; i < a_count; ++i)
{
new(buf) A(a_count - i - 1);
buf += sizeof(A);
}
return new(buf) B(a_count);
}
答案 0 :(得分:4)
使用placement new时,您可以确保目标内存与数据类型正确对齐,否则它是未定义的行为。在A的数组之后,不能保证buf的对齐对于类型B的对象是正确的。对reinterpret_cast的使用也是未定义的行为。
未定义的行为并不意味着它不起作用。它可能用于特定的编译器,以及一组特定的类类型和指针偏移等。但是您不能将此代码放在任意符合标准的编译器中并保证它可以工作。
使用这些黑客强烈暗示您没有正确设计解决方案。
答案 1 :(得分:2)
这是一个有趣的问题。问题是this
+ index + 1
指向的是什么。如果真的是B
,那应该是。{
没问题(假设A*
足够大
包含B*
而不会丢失值):“转换prvalue of
输入'指向T1的'指向'指向T2'的类型(其中T1和
T2是对象类型,其中T2的对齐要求
并不比T1更严格,并回到原来的类型
产生原始指针值。“(§5.2.10/ 7)因为你已经
使用相同的表达式(基本上)来获取地址
你构建B
,你唯一可以合法做的事情
使用this + index + 1
将其转换回B*
。
但是,因为无论如何你需要每个元素中的index
变量,
为什么不将它保存为指针而不是索引。
最后:关于代码,这是一个可怕的解决方案
可读性和稳健性。特别是,如果B
更严格
对齐要求比A
,你可以很容易地结束
B
未对齐。如果你在路上改变了什么,B
可能最终会有更严格的对齐要求。我会避免的
这个解决方案不惜一切代价。
答案 2 :(得分:1)
您发布的示例代码没有显示问题,因为它恰好对两个类具有相同的对齐要求(并且使用了很好的偶数A类对象)。我稍微修改了你的例子,以证明如果alignof(A)&lt;对齐(B)并使用奇数A:http://ideone.com/eC7l17
现在你得到这个输出:
B starts at 0x9003008, needs alignment 4, misaligned by 0
B has 0 As
B starts at 0x900306a, needs alignment 4, misaligned by 2
B has 1 As
A[]
B starts at 0x90030cc, needs alignment 4, misaligned by 0
B has 2 As
A[]
A[]
如果你试图使用指向B的错位指针(从A [0]中恢复)会发生有趣的事情。
Avi Berger已经建议修复。我将尝试为任意A和B提出一个通用的模板来做正确的事情。
| A[N - 1] ... A[0] | <padding> | B |
其中填充是基于alignof(A)和alignof(B)
计算的答案 3 :(得分:0)
当您有一个依赖于多个父项的子对象时,似乎会出现问题。在您的情况下,使用原始指针,如
const B* A::getB() const
{
return (B*)(this + index + 1);
}
或
const B* A::getB() const
{
return (B*)((void*)this + sizeof(A) * (index + 1));
}
应该产生你想要实现的完全相同的指针算法。我从this doc中理解的是(从那里取得的例子):
class Base1 {public: virtual ~Base1() {}};
class Base2 {public: virtual ~Base2() {}};
class Derived: public Base1, public Base2 {public: virtual ~Derived() {}};
// ...
Derived obj;
Derived* dp = &obj;
Base1* b1p = dp;
Base2* b2p = dp; // [1]
Derived* dps = static_cast<Derived*>(b2p); // [2]
Derived* dpr = reinterpret_cast<Derived*>(b2p); // [3]
dp
是指向对象Derived
的指针,该布局基本上类似于Base1
,Base2
和Derived
的串联:
---- address 1: used by Derived and Base1
---- members of Base1: roughly sizeof(Base1))
---- address 2: used by Base2
---- members of Base2: roughly sizeof(Base2))
---- members of Derived
(虽然我真的认为这完全是针对具体实现的,但这是我对布局的理解)。
如果要指向Base2
对象中的父Derived
对象,则等于运算符(行[1]
)会正确转换为父Base2
地址。 static_cast
运算符(行[2]
)使用编译时已知的层次结构返回到原始值。 oder手上的reinterpret_cast
类似于C样式转换,并且由于它在指向Base2
的指针上操作,因此返回指向Derived
中dpr
对象的错误指针。
回到你最初的问题,我认为只要你的两个类在层次结构方面没有依赖关系,你就不会有任何问题。然而,使用诸如void *
和显式指针算法(sizeof(A)
)之类的强制转换似乎更合适。
我很想知道在多大程度上它会改善性能,反对拥有A
s数组和指向唯一B
的指针。