1

基本思想是创建一个可变大小的数组,在构建时固定,在单个分配单元中创建另一个类,以减少开销并提高效率。分配一个缓冲区以适应数组,另一个对象和放置 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);
}
4

4 回答 4

4

使用placement new 时,您需要确保目标内存与您的数据类型正确对齐,否则它是未定义的行为。在 A 的数组之后,不能保证 buf 的对齐对于 B 类型的对象是正确的。您对 reinterpret_cast 的使用也是未定义的行为。

未定义的行为并不意味着它不起作用。它可能适用于特定的编译器,以及一组特定的类类型和指针偏移量等。但是您不能将此代码放在任意符合标准的编译器中并保证它会起作用。

使用这些技巧强烈表明您没有正确设计解决方案。

于 2013-09-12T17:15:10.387 回答
2

这是一个有趣的问题。问题是它this + index + 1指向什么。如果它确实是 a B,则应该没有问题(假设 anA*足够大以包含 aB*而不会丢失值):“将 'pointer to T1' 类型的纯右值转换为 'pointer to T2' 类型(其中 T1 和T2 是对象类型,其中 T2 的对齐要求不比 T1 严格)并返回其原始类型会产生原始指针值。” (§5.2.10/7)由于您(基本上)使用相同的表达式来获取构造 的地址,因此您B唯一可以合法做的事情this + index + 1就是将其转换回 a B*

但是,既然您index无论如何都需要每个元素中的变量,为什么不将它保存为指针,而不是索引。

最后:就代码的可读性和健壮性而言,这是一个糟糕的解决方案。特别是,如果B有比 更严格的对齐要求A,您很容易得到 B未对齐的结果。而且,如果您在未来进行任何更改,B 最终可能会遇到更严格的对齐要求。我会不惜一切代价避免这种解决方案。

于 2013-09-12T17:17:22.210 回答
1

您发布的示例代码没有显示问题,因为它恰好对两个类具有相同的对齐要求(并且使用了很好的偶数 A 类对象)。我稍微修改了您的示例以演示如果 alignof(A) < align of(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) 计算的

于 2013-09-16T18:20:22.227 回答
0

当您有一个依赖于多个父对象的子对象时,问题似乎就会发生。在您的情况下,使用原始指针,例如

const B* A::getB() const 
{ 
  return (B*)(this + index + 1); 
}

或者

const B* A::getB() const 
{ 
  return (B*)((void*)this + sizeof(A) * (index + 1)); 
}

应该产生与您想要实现的完全相同的指针算法。我从这个文档中了解到的是(从那里获取的示例):

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是指向 object 的指针Derived,它的布局基本上类似于 , 的串联Base1Base2并按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]编译时已知的层次结构返回原始值。reinterpret_cast另一方面,就像 C 风格的强制转换一样,由于它对指向 的指针进行操作,因此Base2返回一个指向 中的Derived对象的错误指针dpr

回到你最初的问题,我认为你可能没有任何问题,只要它们在层次结构方面不是你的两个类之间的依赖关系。然而,在我看来,使用诸如void *显式指针算术 ( sizeof(A)) 之类的强制转换似乎更合适。

我很想知道它实际上会在多大程度上提高性能,而不是拥有As 数组和指向 unique 的指针B

于 2013-09-12T19:00:36.840 回答