2

我正在用 C++ 实现我的类似 C# 的属性类。所以我必须提供对内部字段(mother::_i)到属性字段(mother::i)的访问权限。

我找到了一些解决方案,但没有完美的解决方案。

首先,我创建了一个方法,通过调用 RProperty<...>::SetOwner(mother&) 之类的方法在运行时提供所有者(在这种情况下为母亲)的指针。但它需要额外的代码才能在运行时使用我的属性类和成本。

其次我想出这个RProperty的指针和它自己的成员指针可以找到所有者的指针。显然,ownerpointer = this - &mother::i。但是提供指向成员本身的成员指针会给我编译时错误。我尝试了一种使用“空”结构来提供指向属性的成员指针的棘手方法。但事实证明 sizeof(struct empty) 不为零。每个实例都会花费不必要的额外内存。我在这个问题上停留了几天。

有人有好主意吗?:)

代码有效但并不完美:

#include "stdafx.h"
struct empty{};

template<typename TCLASS, typename TFIELD>
class RPropertyBase
{
protected:
    RPropertyBase(){ }

    TCLASS& getOwner() const {  };
};
template<typename TCLASS, typename TFIELD, TFIELD TCLASS::*PFIELD, empty TCLASS::*PTHIS>
class RProperty : RPropertyBase<TCLASS, TFIELD>
{
protected:
    TCLASS& getOwner() const { return *(TCLASS*)((unsigned int)this-(unsigned int)&(((TCLASS*)0)->*PTHIS)-sizeof(empty) ); }

public:
    RProperty<TCLASS, TFIELD, PFIELD, PTHIS>& operator=(const TFIELD& A){ getOwner().*PFIELD = A; return *this; }
    operator TFIELD&() const { return getOwner().*PFIELD; }
};

class mother
{
    int _i;

    template<typename C>
    struct __Propertyi : public RProperty<C, int, &C::_i, &C::_empty>
    {
        using RProperty<C, int, &C::_i, &C::_empty>::operator=;
    };
public:
    empty _empty;
    __Propertyi<mother> i;
};

int _tmain(int argc, _TCHAR* argv[])
{
    mother a;
    a.i = 1;
    int bb = (a.i);
    return 0;
}
4

1 回答 1

4

第一的...

所以我必须提供对内部字段(mother::_i)到属性字段(mother::i)的访问权限。

以下划线开头的标识符在 C++ 中是保留的——只有编译器和它的库应该使用它们。包含双下划线的标识符也被保留。但是,带有单个尾随下划线的标识符(例如,i_可以)。

进入正题...

ownerpointer = this - &mother::i

看起来您正在尝试从指针中减去成员指针,这是您无法做到的。成员指针有点像类型布局中的偏移量,但这以两种方式分解......

  1. 这不是他们旨在提供的抽象。

  2. 无论如何它都不准确 - 一旦您允许多重继承和虚拟继承,特定成员在类型中出现的偏移量不仅取决于它在定义它的基本类型中的位置,还取决于您的子类型重新看。

如果你真的想做知道类型布局的指针运算,那当然是可能的,但它是一种使用 C 级特性的 C 编程技术。上下文也有一些重大限制。

关键思想是不要尝试使用成员指针作为偏移量,而是使用实际的偏移量。这会花费您的类型安全性,但是,只要您包装类型不安全的代码并绝对确定它是正确的,就应该没问题。

基本工具是offsetof,它是 C++ 继承自 C...

offsetof(structname, membername)

您可以查找该宏的实现,但不要复制它 - 标准要求编译器提供某种方式来实现有效的宏,但适用于一个编译器的实现可能不适用于另一个。但是,两种常见的方法是...

  • 查看地址为零的结构的虚构实例中成员的地址。与此有关的问题(例如,想象中的实例显然没有有效的虚拟指针)是某些限制的部分原因。
  • 使用编译器提供的特殊“内在”函数,这也是保留那些带下划线的标识符的原因之一。

原则上,使用该偏移量,您可以将指针转换为char*via void*,进行算术运算,然后再次转换回所需的类型。

第一个问题很明显——某些成员(即static那些成员)在每个实例中都没有固定的偏移量,无论实例如何,它们都位于固定地址。显而易见,但也许最好说出来。

下一个问题来自offsetof我链接的那个文档......

type 应为 POD 类(包括联合)。

您正在查看类型的布局。您还需要将该布局应用于子类型。因为您已经放弃了 C++ 多态抽象并且直接处理偏移量,所以编译器无法为您处理任何运行时布局解析。各种与继承相关的问题会使偏移计算无效 - 多重继承、虚拟继承、当基类没有虚拟指针时具有虚拟指针的子类型。

因此,您需要使用 POD 结构进行布局。您可以摆脱单继承,但不能拥有虚拟方法。但是还有另一个烦恼——POD 是一个有点过分的术语,显然不仅仅与是否offsetof有效有关。具有非 POD 数据成员的类型不是 POD。

我用多路树数据结构解决了这个问题。我曾经offsetof实现数据结构(因为不同的时间)。我将它包装在一个模板中,该模板使用structandoffsetof来确定节点布局。在整个系列的编译器和编译器版本中,这很好,直到我切换到 GCC 版本,它开始到处发出警告。

我关于这个的问题和答案在这里

这个问题offsetof可能已经在 C++11 中得到解决——我不确定。在任何情况下,即使结构中的成员是非 POD,该结构仍将具有在编译时确定的固定布局。即使编译器向您抛出警告,偏移量也是可以的,幸运的是,在 GCC 中可以将其关闭。

offsetof我链接的那个文档中的下一个问题......

type 应为标准布局类(包括联合)。

这是来自 C++11 的新版本,老实说,我自己并没有真正考虑过。

最后一个问题——实际上,将指针视为地址是无效的。当然,编译器将指针实现为地址,但是有很多技术细节,编译器编写者一直在他们的优化器中利用这些。

一旦你开始进行指针运算,你必须非常小心的一个领域是编译器的“别名分析”——它如何决定两个指针是否指向同一个东西(为了决定什么时候它可以安全地将值保存在寄存器中而不是返回内存以查看通过别名指针的写入是否更改了它)。我曾经问过这个问题,但事实证明我接受的答案是一个问题(我可能应该回去做点什么)因为虽然它正确描述了问题,但它建议的解决方案(使用基于联合的双关语)仅对 GCC 正确,C++ 标准不保证。

最后,我的解决方案是将指针算术(和指针)隐藏char*在一组函数中......

inline void* Ptr_Add  (void* p1, std::ptrdiff_t p2)
{
  return (((char*) p1) + p2);
}

inline void* Ptr_Sub  (void* p1, std::ptrdiff_t p2)
{
  return (((char*) p1) - p2);
}

inline std::ptrdiff_t Ptr_Diff (void* p1, void* p2)
{
  return (((char*) p1) - ((char*) p2));
}

inline bool  Ptr_EQ (void* p1, void* p2)  {  return (((char*) p1) == ((char*) p2));  }
inline bool  Ptr_NE (void* p1, void* p2)  {  return (((char*) p1) != ((char*) p2));  }
inline bool  Ptr_GT (void* p1, void* p2)  {  return (((char*) p1) >  ((char*) p2));  }
inline bool  Ptr_GE (void* p1, void* p2)  {  return (((char*) p1) >= ((char*) p2));  }
inline bool  Ptr_LT (void* p1, void* p2)  {  return (((char*) p1) <  ((char*) p2));  }
inline bool  Ptr_LE (void* p1, void* p2)  {  return (((char*) p1) <= ((char*) p2));  }

这种std::ptrdiff_t类型也很重要——指针的位宽不能保证与 a 的位宽匹配long

在这些函数之外,所有指针要么是正确的类型,要么是void*. C++void*进行了特殊处理(编译器知道它可以为其他指针类型起别名),因此它似乎可以工作,尽管可能有一些我不记得的细节。抱歉 - 这些事情很难,尤其是现在优化器有时在“令人讨厌的学究”意义上很聪明,我只有在绝对必要时才会触碰这个邪恶的代码。

最后一个问题 - 我已经提到指针不是地址。一个奇怪的是,在某些平台上,两个不同的指针可能会映射到不同地址空间中的同一地址 - 例如,参见哈佛架构,它具有不同的指令地址空间。因此,即使两个指针之间的偏移量在一定范围内也是无效的,这无疑在标准中进行了复杂的详细描述。一个单一的结构就是一个单一的结构——显然它存在于一个地址空间上,static成员可能例外——但不要仅仅假设指针算法总是有效的。


长话短说 - 是的,可以从成员的地址中减去成员的偏移量以找到结构的地址,但是您必须使用实际的偏移量(而不是成员指针)并且存在限制和技术性可能意味着你甚至不能以这种方式解决你的问题(例如,我不确定你是否能够使用偏移量作为模板参数),当然这意味着它比看起来更难。

最终,外卖建议是,如果您阅读本文,请将其视为警告。不要做我做过的事情。我希望我没有,你可能也会。

于 2013-05-17T09:54:45.533 回答