【问题标题】:Base class fields offset基类字段偏移
【发布时间】:2011-09-18 19:58:36
【问题描述】:

我有一些代码实现了一种运行时反射。为了在给定实例中获取指向类字段的指针,我基本上采用指向类实例的指针并添加一个固定偏移量,该偏移量为暴露给反射库的每个字段计算一次。

我的实现非常简单,因为我不需要支持多重继承,而且我犯了一个错误,即没有考虑到即使使用单一继承,这种情况也是可能的:

class A
{
public:
    unsigned int m_uiField;
};

class B : public A
{
    virtual void VirtualMethod()
    {
    }
};

int main()
{
    unsigned int uiOffsetA(reinterpret_cast<unsigned int>(&(reinterpret_cast<A *>(0)->m_uiField)));
    // uiOffsetA is 0 on VC9
    unsigned int uiOffsetB(reinterpret_cast<unsigned int>(&(reinterpret_cast<B *>(0)->m_uiField)));
    // uiOffsetB is 4 on VC9
}

在这种情况下,我的编译器放在每个 B 实例开头的虚拟表指针将 A 的字段偏移 4 个字节。

我的第一个想法是做一些类似于我对字段偏移量所做的事情,并将单个无符号整数存储为基类的偏移量,以添加到指向派生类实例的指针以及字段偏移量。因此,在初始化时,我为从基类继承的每个派生类调用此函数:

template <typename Base, typename Derived>
unsigned int GetBaseClassOffset()
{
    Derived *pDerived(reinterpret_cast<Derived *>(4));
    Base *pBase(pDerived);
    assert(pBase >= pDerived);
    return reinterpret_cast<unsigned int>(pBase) - reinterpret_cast<unsigned int>(pDerived);
}

一切似乎都适用于我使用 VC9 进行的测试。 但后来我想到,C++ 的这个领域可能依赖于实现,而对齐等其他事情可能会打破这一点。

最后我的问题是: 我可以假设基类的字段总是相对于指向派生类实例的指针定位在一个恒定的正偏移量处吗?

注意:我并不是说“在所有编译器中都保持不变”,我将使用一些代码(最终取决于编译器)在启动时检测此偏移量。

【问题讨论】:

  • 我很确定你不能假设任何这样的事情。继承的实现(尤其是多态、多重和虚拟继承)完全取决于编译器。我认为您可以假设的唯一事情是成员对象按其声明顺序排序,并且它们被适当地对齐。
  • @sehe: "仅限 POD 类型"。
  • Valerio,注意指针在 32 位操作系统上是 4 个字节。在 64 位上,指针是 8 个字节。
  • 我见过很多用 C++ 进行反射的尝试,它们都是非常讨厌的 hack。只需在基于块的流(如.png)中使用operator &lt;&lt;operator &gt;&gt;,其中块头是通过虚拟GetClassType 或其他东西访问的类类型。

标签: c++


【解决方案1】:

对于这种情况,您可以使用指向成员的指针:

现场观看:http://ideone.com/U4w7j

struct A
{
    unsigned int m_uiField;
};

struct B : A
{
    virtual void VirtualMethod() { }
};

int main()
{
    A instance_a;
    B instance_b;

    unsigned int A::*  ptrA = &A::m_uiField;
    unsigned int B::*  ptrB = &B::m_uiField;

    // application:
    unsigned int value = instance_a.*ptrA;
                 value = instance_b.*ptrA;
               //value = instance_a.*ptrB; // incompatible types
                 value = instance_b.*ptrB;

    // also:
    A* dynamic = new B();
    value = dynamic->*ptrA; // etc
}

我建议你也看看模板元编程特性(现在是 TR1 和 C++11 的一部分:),尤其是 is_pod 类型特征:

这很重要,因为在其他任何东西上使用 offsetof 都是危险的。

【讨论】:

  • 如果您谈论的是 Boost.MPL,那些不是 MPL 功能,或者您指的是一般元编程?它们是 Boost.TypeTraits 库的一部分。
  • K-ballo:你自己解释了。通用元编程。我想我记得它是在"The C++ Library and Extensions" 中引入的——不幸的是,书籍索引非常差(is_pod 没有条目,实际上template 也没有条目... :))
  • 我从一开始就没有使用字段指针的原因是它们不能转换为 unsigned int 然后像我一样使用偏移量。为了使用字段指针获取指向字段的指针,我需要一个虚函数或一个指向知道如何使用它的模板函数的函数指针。
  • @valerio:所以基本上,你是说,你更喜欢能够导致未定义的行为?如果你需要一个指向特定实例成员的指针,你可以拥有它。如果您需要一个指向“类型的任何实例中的成员”的“通用”指针,则应该使用指向成员的指针,否则您将最终陷入困境-起来。
  • 我不喜欢未定义的行为,我只是想确保预先计算常量偏移不是一种选择,因为它会更快并且占用更少的内存。
【解决方案2】:

无法在 C++(包括 C++11)中实现符合标准的反射。主要是因为成员偏移量不是标准化的,所以依赖于编译器。这取决于多态性的实现、对齐以及可能的其他因素。

您可以为特定编译器或有限范围的类实现反射。

Much more info about C++ reflection is here.

为 C++11 标准提出了反射支持,但由于需要更多时间而被推迟。

【讨论】:

    【解决方案3】:

    我可以假设基类的字段将始终定位在相对于指向派生类实例的指针的恒定正偏移处吗?

    不,你不能。为什么是正偏移?我希望在常见的实现中基成员被放置在之前派生类?

    注意标准的“操作符”(不完全是)offsetof,它将为您提供从结构到成员的偏移量。它通常实现为类似于您的宏,取消引用空指针。它可能对您没有帮助,因为它保证仅适用于 POD 类型的类。参见标准中的 18.2/4:

    宏 offsetof(type, member-designator) 在这个 International 中接受一组受限制的类型参数 标准。如果 type 不是标准布局类(第 9 条),则结果未定义。

    【讨论】:

    • 积极的意思是它们在指向派生类实例的指针的字节处或之后,而不是之前。
    猜你喜欢
    • 2020-08-11
    • 2013-01-05
    • 1970-01-01
    • 1970-01-01
    • 2018-06-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-06-13
    相关资源
    最近更新 更多