【问题标题】:Cannot cast "member pointer to derived class" to "member pointer to base class"无法将“派生类的成员指针”转换为“基类的成员指针”
【发布时间】:2015-01-26 18:28:39
【问题描述】:

使用指向基类的指针调用类的虚成员函数在 C++ 中当然是很常见的事情。所以我觉得奇怪的是,当你有一个成员指针而不是普通指针时,似乎不可能做同样的事情。请考虑以下代码:

struct B
{
    virtual void f();
};

struct D : B
{
    virtual void f();
};

struct E
{
    B b;
    D d;
};

int main()
{
    E e;

    // First with normal pointers:
    B* pb1 = &e.b;  // OK
    B* pb2 = &e.d;  // OK, B is a base of D
    pb1->f();  // OK, calls B::f()
    pb2->f();  // OK, calls D::f()

    // Now with member pointers:
    B E::* pmb1 = &E::b;  // OK
    B E::* pmb2 = &E::d;  // Error: invalid conversion from ‘D E::*’ to ‘B E::*’
    (e.*pmb1).f();  // OK, calls B::f()
    (e.*pmb2).f();  // Why not call D::f() ???

    return 0;
}

Visual C++ 继续说:

error C2440: 'initializing' : cannot convert from 'D E::* ' to 'B E::* ' Types pointed to are unrelated; conversion requires reinterpret_cast, C-style cast or function-style cast

我不明白为什么这些是“无关的”。为什么这不可能?


编辑:
我试图保持这是一个 C++ 问题,而不是关于我要解决的特定问题,但这本质上是我想要做的:

std::vector<B E::*> v;
v.push_back( &E::b ); // OK
v.push_back( &E::d ); // Error

B& g( E& e, int i )
{
  return e.*v[i];
}

E 是一个包含多个从 B 派生的成员的类。向量 v 用于组织(例如重新排序)指向这些成员的成员指针。向量 v 不经常变化。函数 g() 允许您使用 v 中的索引来选择 E 的成员之一。它被非常频繁地调用,并且每次都使用不同的 E。

如果你仔细想想,v 只是一个偏移量查找表。函数 g() 只需选择其中一个偏移量并将其添加到 E* 以返回 B*。函数 g() 被编译器内联,只编译成 4 个 CPU 指令,这正是我想要的:

// g( e, 1 )
mov         rax,qword ptr [v (013F7F5798h)]
movsxd      rcx,dword ptr [rax+4]
lea         rax,[e]
add         rcx,rax

我想不出标准为什么不允许将 D E::* 转换为 B E::* 的任何原因。

【问题讨论】:

  • 您是在询问有关 C++ 设计的信息,还是要求您了解不允许这样做的标准部分?或者你有一个具体的问题想要解决,并且正在以一种间接的方式寻求解决方案?
  • 嗨@Yakk,我在标准中找不到任何说明这是不允许的,所以如果它在那里,如果你能指出我将不胜感激。但我也想知道为什么不允许这样做,因为这似乎是一件合理的事情。我想不出编译器有什么问题。
  • 也许我错了,但成员变量的动态类型不是在编译时总是已知的吗?真的有一个合理的场景,你会实际使用它吗?
  • @MikeMB,我在原始问题中添加了更多信息。在这方面,我认为成员指针与常规指针没有任何不同——我认为在这两种情况下,编写通过指向公共基类的指针集合来操作不同对象的代码是很有用的。我仍然有兴趣了解为什么标准中禁止这样做,因为我很想尝试 Visual C++ 错误消息中的 reinterpret_cast“推荐”。
  • @Barnett 对于它的价值:我找不到任何技术原因会阻止该语言实现您正在寻找的东西。可悲的是,数据指针的成员是所有数据类型中最不支持的。

标签: c++


【解决方案1】:

简单的答案是,C++ 没有定义您尝试的转换,因此您的程序格式不正确。

考虑标准转换 (C++11§4/1):

标准转换是具有内置含义的隐式转换。第 4 条列举了完整的此类转换。

由于您没有执行任何转换,也没有定义任何自定义转换,因此您确实在执行此类标准转换。在不列举所有可能的此类转换的情况下,您的示例有两个显式感兴趣:pointer 转换和 指向成员的指针 转换。请注意,C++ 不将指向成员类型的指针视为指针类型的子集。

指向成员转换的指​​针在 C++11§4.11 中定义,并且正好包含两个转换:

  • 空成员指针转换允许将空指针常量转换为指向成员类型的指针 (4.11/1)。
  • 有些人为的第二次转换 (4.11/2):

    “指向类型 cv T 的 B 成员的指针”,其中 B 是类类型,可以转换为 [...] “指向类型 cv T 的 D 成员的指针”,其中 D 是派生类[...] B

将此与前面的 4.10 节进行对比,后者定义了三个指针转换:

  • 空指针转换 (4.10/1),它允许将空指针常量转换为指针类型。
  • 转换为指向void (4.10/2) 的指针,允许将任何指针类型转换为指向void 的指针。
  • 最后为什么它可以使用指针(但不能使用指向成员的指针)(4.1​​0/3):

    A [...]“指向 cv D 的指针”,其中 D 是类类型,可以转换为 [...]“指向 cv B 的指针”,其中 B 是基类 [... ]的D

因此,总结一下:您尝试的标准转换是为您的指针示例定义的,但对于指向成员变量的指针根本不存在。

【讨论】:

    【解决方案2】:

    C++ 不允许这种转换,许多其他人喜欢它,因为它会使虚拟继承的实现复杂化。

    struct A { int a; };
    struct B : virtual A { int b; };
    struct C : virtual A { int c; };
    struct D : B, C { int d };
    

    以下是编译器可能会尝试布置这些类的方式:

    A:  [ A::a ]
    B:  [ B::b ] [ A ]
    C:  [ C::c ] [ A ]
    D:  [ D::d ] [ B ] [ C ] [ A ]
    

    如果我们有一个指向 B 的指针,那么要获得一个指向其基类 A 的指针就不是一件容易的事,因为它与 B 对象的开头没有固定的偏移量。 A 可能位于 B::b 旁边,也可能位于其他位置,这取决于我们的对象是独立的 B 还是作为 D 基础的 B。没有办法知道我们有哪种情况!

    要进行强制转换,程序需要实际访问 B 对象并从中获取一个隐藏的基指针。所以真正的布局应该是这样的:

    A:  [ A::a ]
    B:  [ B::b | B::address-of-A ] [ A ]
    C:  [ C::c | C::address-of-A ] [ A ]
    D:  [ D::d | D::address-of-A ] [ B ] [ C ] [ A ]
    

    其中address-of-As 是编译器添加的隐藏成员。

    当我们谈论常规指针时,这一切都很好。但是当我们有一个指向成员的指针时,我们没有任何对象可以从中获取隐藏的基指针。因此,如果我们只有 B X::*,那么在没有实际 X 对象的情况下绝对无法将其转换为 A X::*

    虽然理论上可以允许这样的转换,但它会非常复杂。例如,指向成员的指针需要保存可变数量的数据(原始对象具有的所有隐藏的指向基值的指针)。

    理论上,C++ 仅允许将指向成员的指针转换为非虚拟基类(在此示例中,D X::*B X::*C X::*,但不是 A X::*)。或者至少我不明白为什么这对于实现来说可能是一个无法克服的问题。我想这没有完成,因为它会给标准带来额外的复杂性,而收益却很小。或者,标准可能不想排除不寻常的继承实现。例如,实现可能希望使用隐藏的指向基成员的指针来实现所有继承,就好像它始终是虚拟的(出于调试目的,或与其他语言兼容,或其他)。或者也许它只是被忽视了。也许在该标准的另一次修订中(2020 年?)

    【讨论】:

    • 由于我们在获取引用时知道派生最多的类(&amp;E::d 确保E 是派生最多的类),在这种情况下甚至可以这样做。虽然我当然不会假装知道标准委员会不将此转换添加到标准中的原因,但您的推理是合理的。
    • 不,当我们有一个D E::* 并且我们想要的转换是允许的时,我们实际上不知道它是否是顶级的。如果我们在 E 中有一个比 D 更派生的成员怎么办?
    • 再考虑一下,我仍然认为这是可能的,但可能会使成员指针的实现比现在更加复杂(禁止如此,因为它需要它们具有任意长度)。
    猜你喜欢
    • 2016-10-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-02-23
    • 2021-12-03
    • 2016-07-31
    • 1970-01-01
    相关资源
    最近更新 更多