无法将“成员指针指向派生类”转换为“指向基类的成员指针”

时间:2015-01-26 18:28:39

标签: c++

使用指向基类的指针调用类的虚拟成员函数当然是在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()允许你使用索引选择E的一个成员到v。它经常被调用,每次都有不同的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 :: *的任何理由。

2 个答案:

答案 0 :(得分:4)

简单的答案是,C ++没有定义您正在尝试的转换,因此您的程序生成错误。

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

  

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

由于您没有执行任何演员表,也没有定义任何自定义转换,因此您确实正在执行此类标准转换。如果不枚举所有可能的此类转换,则会对您的示例明确感兴趣:指针转换和指向成员转换的指针。请注意,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):

      

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

  •   
  因此,要将其全部包装起来:您尝试的标准转换是针对您的指针示例定义的,但对于指向成员变体的指针则不存在。

答案 1 :(得分:4)

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还是B的基础。我们无法知道我们有哪种情况!< / p>

要进行强制转换,程序需要实际访问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-A是编译器添加的隐藏成员。

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

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

理论上,C ++只允许将指针转换为非虚拟基类(在此示例中为D X::*B X::*C X::*,而不是{{} 1}})。或者至少我不明白为什么它可能是一个不可逾越的实现问题。我想这没有做到,因为它会给标准带来额外的复杂性,但收效甚微。或者标准可能不希望排除继承的异常实现。例如,实现可能希望使用隐藏的指向基类的成员实现所有继承,就像它始终是虚拟的(用于调试目的,或与其他语言兼容,或其他)。或许它只是被忽视了。也许在标准的另一个修订版(2020?)