【问题标题】:provide member pointer to the member itself提供指向成员本身的成员指针
【发布时间】:2013-05-17 07:25:36
【问题描述】:

我正在用 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;
}

【问题讨论】:

  • 似乎是一个非常复杂的方法,为什么不简单的 setter/getter 呢?或者我敢说,即使是简单的struct
  • 用于消除getter/setter地狱,学习模板和C++。结果不会显得我想的那么无用。
  • __Propertyi 是为实现保留的名称。请不要使用以下划线或双下划线开头的名称。
  • 当你有属性时(_j_k 等)你会定义内部类n 次吗?或者有模板?如果您有模板,则意味着您将通用逻辑应用于所有属性;后者没有给你任何超过structclass 与公共成员。如果每种类型都有一个类,那么您现在就有了n 类,然后将其扩展。我的观点;它比 getter/setter 地狱更糟糕。在这种情况下,如果您真的想这样做,最好使用一个简单的宏 #define PROPERTY(name, type) type get##name() const {...}; void set##name(type v_) { name = v_; }...
  • 费尔南德斯,谢谢。

标签: c++ templates


【解决方案1】:

首先...

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

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

言归正传……

ownerpointer = this - &mother::i

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

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

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

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

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

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

offsetof(structname, membername)

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

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

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

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

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

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

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

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

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

我在 SO 上对此的问题和答案是 here

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

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

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

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

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

一旦你开始进行指针运算,你必须非常小心的一个领域是编译器的“别名分析”——它如何决定两个指针是否可能指向同一个东西(为了决定当它可以安全地将值保存在寄存器中并且不回溯到内存以查看通过别名指针的写入是否更改了它时)。我曾经问过this question,但事实证明我接受的答案是一个问题(我可能应该回去做点什么)因为虽然它正确描述了问题,但它建议的解决方案(使用基于联合的双关语) 仅对 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 类型也很重要 - 不能保证指针的位宽与 long 的位宽匹配。

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

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


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

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

【讨论】:

  • 很高兴看到这个很棒的答案。嗯,这比我想象的要难。我宁愿考虑另一种方式来实现它。
猜你喜欢
  • 1970-01-01
  • 2013-05-02
  • 2018-03-24
  • 2018-10-29
  • 1970-01-01
  • 2021-12-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多