使用指向基类的指针调用类的虚拟成员函数当然是在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 :: *的任何理由。
答案 0 :(得分:4)
简单的答案是,C ++没有定义您正在尝试的转换,因此您的程序生成错误。
考虑标准转换(C ++11§4/ 1):
标准转化是具有内置含义的隐式转化。第4条列举了全套此类转换。
由于您没有执行任何演员表,也没有定义任何自定义转换,因此您确实正在执行此类标准转换。如果不枚举所有可能的此类转换,则会对您的示例明确感兴趣:指针转换和指向成员转换的指针。请注意,C ++不会将指向成员类型的指针视为指针类型的子集。
指向成员转换的指针在C ++11§4.11中定义,并且只包含两个转换:
“指向cv T类型B的成员的指针”,其中B是类类型,可以转换为“指向cv T类型D的成员的指针”,其中D是派生类[...] B
void
(4.10 / 2)的指针,允许将任何指针类型转换为指向void
的指针。
“指向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?)