指向“内部结构”成员的指针是否被禁止?

21

我有一个嵌套的结构体,我想要一个指向其中一个嵌套成员的指针:

这合法吗?

struct InnerStruct
{
    bool c;
};
struct MyStruct {
    bool t;
    bool b;
    InnerStruct inner;
}; 

这个:
MyStruct mystruct;
//...
bool MyStruct::* toto = &MyStruct::b;

可以,但是:

bool MyStruct::* toto = &MyStruct::inner.c;

没有想法吗?

谢谢

以下是一些细节: 是 &MyStruct::b 而不是 mystruct::b; 该代码来自自定义RTTI/属性系统。 对于每个指定的类,我们都保留一个“属性”数组,包括成员指针。 它的使用方法如下:

//somewhere else in code...
( myBaseClassWithCustomRTTIPointer)->* toto = true;

我认为他想要执行 (mystruct.*(&MyStruct::inner.c)) = true 并使其与 mystruct.inner.c = true 完全相同。 - Johannes Schaub - litb
@Joe:在C++中,指向数据成员的指针不过是“偏移量”概念在语言层面上的实现。也就是说,内部上,指向数据成员的指针只是整个对象开头到该成员的偏移量而已。显然,子对象的成员也有它们自己在整个对象中的偏移量,因此允许 bool MyStruct::* 类型的指针指向 MyStruct::InnerStruct 成员,在 OP 的示例中是完全合理的。然而,语言并不允许这样做。根本没有相应的语法。 - AnT stands with Russia
感谢大家快速而准确的回答。我认为指向成员的指针比仅仅是基类偏移量更加复杂,因为它可以处理多重和虚拟继承。我可能会更改我的属性系统,使其使用offsetof并禁止“可怕的菱形继承”。 - benoitj
@AndreyT:不,我不是,或者我漏掉了什么。请查看https://dev59.com/HXNA5IYBdhLWcg3wAI3d的答案。在虚拟继承的情况下,使用offsetof访问成员会导致崩溃,而成员指针却可以正常工作。 - benoitj
@Johannes:所有成员指针都不仅仅是偏移量。但是,在典型的实现中,数据成员指针是简单的偏移量。仅仅使用偏移量就足以完全实现标准对于数据成员指针的要求。 - AnT stands with Russia
显示剩余6条评论
3个回答

23

是的,这是被禁止的。你不是第一个提出这个完全合理想法的人。在我看来,这是C ++中指向成员指针规范中明显的“缺陷”之一,但显然委员会不再对指向成员指针规范进行开发(与大多数“低级”语言特性一样)。

请注意,实现此功能所需的所有内容都已经在语言中了。指向成员的数据成员的指针与指向直接数据成员的指针没有任何区别。唯一缺少的就是初始化这种指针的语法。然而,委员会显然不感兴趣引入这样的语法。

从纯形式逻辑的角度来看,这应该被允许在C ++中。

struct Inner {
  int i;
  int j[10];
};

struct Outer {
  int i;
  int j[10];
  Inner inner;
};

Outer o;
int Outer::*p;

p = &Outer::i; // OK
o.*p = 0; // sets `o.i` to 0

p = &Outer::inner.i; // ERROR, but should have been supported
o.*p = 0; // sets `o.inner.i` to 0

p = &Outer::j[0]; // ERROR, but should have been supported
o.*p = 0; // sets `o.j[0]` to 0
// This could have been used to implement something akin to "array type decay" 
// for member pointers

p = &Outer::j[3]; // ERROR, but should have been supported
o.*p = 0; // sets `o.j[3]` to 0

p = &Outer::inner.j[5]; // ERROR, but should have been supported
o.*p = 0; // sets `o.inner.j[5]` to 0

指向数据成员的指针(typically implementation of pointer-to-data-member)通常只是从封闭对象的开头开始计算成员的字节偏移量(byte-offset)。由于所有成员(直接和间接)最终都按顺序在内存中排列,因此可以通过具体的偏移值来标识成员的成员。这就是我所说的这个特性的内部工作已经完全实现了,现在需要的只是初始化语法。

在C语言中,offsetof宏可以明确获得显式偏移量(explicit offsets),我可以使用offsetof(Outer, inner.i)offsetof(Outer, j[2])。不幸的是,这种功能在C++的指向数据成员(pointers-to-data-members)中并没有反映出来。


在C++0x中,我们可以执行sizeof(Outer::inner.i)decltype(Outer::inner.i)。我想知道是什么原因使得成员指针对这样的子元素更加复杂? - Johannes Schaub - litb
关于指向数组的指针,您可以这样做示例:Outer o; int (Outer::*p_array)[10]; p_array = &Outer::j; o.*p_array[3] = 0; - Grim Fandango
@Grim Fandango:是的,但那恰好不是重点。我们正在谈论的是指向子对象的能力。 - AnT stands with Russia

20

你关心的InnerStruct恰好包含在MyStruct实例中,但这并不影响你获取InnerStruct成员的指针。

bool InnerStruct::* toto2 = &InnerStruct::c;

编辑:重新阅读您的问题,我猜测您想定义一个指向外部结构体成员的指针,并直接指向内部结构体的成员。这是不允许的。要访问包含在外部结构体中的内部结构体成员,您必须创建一个指向内部结构体本身的指针,然后再创建一个指向其成员的指针。使用它时,您需要引用两个成员指针:

// Pointer to inner member of MyStruct:
InnerStruct MyStruct::* toto = &MyStruct::inner;

// Pointer to c member of InnerStruct:
bool InnerStruct::* toto2 = &InnerStruct::c;

// Dereference both to get to the actual bool:
bool x = mystruct.*toto.*toto2;

@JerryCoffin:感谢您的出色回答。您如何使用此符号来引用结构体<code>Outer { int i; int j [5]; }</code>中的j [3]元素? - Grim Fandango
@GrimFandango:你可以这样创建成员指针:int (Outer::* ptr_j)[5] = &Outer::j;。假设有一个 Outer 的实例 x,你可以这样解引用它:(x.*ptr_j)[3] = 1; - Jerry Coffin

4

正如 AnT 的回答所述,这似乎确实是一个标准漏洞,并且缺少表达您想要执行的正确语法。我的一位同事前几天遇到了这个问题,并引用了您的问题和其答案作为证明无法完成此操作的证据。好吧,我喜欢挑战,是的,它可以做到...但并不美观。

首先,重要的是要意识到成员指针基本上是与结构体指针的偏移量[1]。该语言确实有一个 offsetof 运算符,它非常类似于此,并且足够有趣,可以给我们所需的表现力。

我们立即遇到的问题是C++禁止转换成员指针。好吧,几乎可以说...对于此,我们有联合强制转换。正如我所说,这不美观!

最后,我们还需要知道要转换的正确指针类型。

因此,话不多说,这是代码(在 gcc 和 clang 上测试):

template <typename C, typename T, /*auto*/size_t P>
union MemberPointerImpl final {
  template <typename U> struct Helper
    { using Type = U C::*; };
  template <typename U> struct Helper<U&>
    { using Type = U C::*; };

  using MemberPointer = typename Helper<T>::Type;

  MemberPointer o;
  size_t i = P; // we can't do "auto i" - argh!
  static_assert(sizeof(i) == sizeof(o));
};

#define MEMBER_POINTER(C, M) \
  ((MemberPointerImpl<__typeof__(C),          \
      decltype(((__typeof__(C)*)nullptr)->M), \
      __builtin_offsetof(__typeof__(C), M)    \
  >{ }).o)

让我们先看一下宏MEMBER_POINTER。它有两个参数。第一个参数C是结构体,将作为成员指针的基础。用__typeof__包装它并不是必要的,但允许传递类型或变量。第二个参数M提供了一个表达式,表示我们想要指向的成员。 MEMBER_POINTER宏从这些参数中提取了另外两个信息,并将它们作为参数传递给MemberPointerImpl模板联合体。第一个信息是指向的成员的类型。这是通过使用空指针构造表达式来完成的,然后使用decltype进行操作。第二个信息是从基本结构到所讨论的成员的偏移量。
MemberPointerImpl内部,我们需要构造MemberPointer类型,这将是宏返回的内容。这是通过使用一个帮助结构来完成的,该结构删除了引用,如果成员是数组元素,则不会产生帮助性,同时也使得gcc和clang可以在诊断中为我们提供一个很好的完全展开的类型,如果我们将返回值分配给具有不匹配类型的变量。
因此,要使用MEMBER_POINTER,只需将代码更改为:
bool MyStruct::* toto = &MyStruct::inner.c;

to:

bool MyStruct::* toto = MEMBER_POINTER(MyStruct, inner.c);

[1] 好的,需要注意的是:这可能并不适用于所有架构/编译器,因此可移植代码编写者现在请离开!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接