我正在寻找一种检查子类是否覆盖其基类上的函数的方法。如果成员函数指针不是虚拟的,则比较成员函数指针可以很好地工作,但是如果它们是是虚拟的,则它不起作用。这段示例代码本质上就是我遇到的麻烦。
class Base {
public:
virtual void vfoo(){ cout << "foo"; }
virtual void vbar(){ cout << "bar"; }
void foo(){ cout << "foo"; }
void bar(){ cout << "bar"; }
};
class Child : public Base {
public:
void vfoo(){ cout << "foo2"; }
void foo(){ cout << "foo2"; }
};
int main (){
//non-virtual cases, these work correctly
cout << (&Base::foo == &Child::foo) << endl; //outputs false (good)
cout << (&Base::bar == &Child::bar) << endl; //outputs true (good)
//virtual cases, these do not work correctly
cout << (&Base::vfoo == &Child::vfoo) << endl; //outputs true (BAD, child::vfoo and base::vfoo are DIFFERENT FUNCTIONS)
cout << (&Base::vbar == &Child::vbar) << endl; //outputs true (good, child::vbar and base::vbar are the same)
return 0;
}
从逻辑上讲,没有理由不应该这样做,但是C ++规范则相反(通过实现定义比较虚拟函数的地址是实现定义的)。
在GCC上,键入punning&Base :: vfoo和&Child :: vfoo来将它们都设为“ 1”(而vbar为“ 9”),以int表示它们是vtable偏移量。以下代码似乎可以正确地从vtable中获取函数地址,并正确报告Child :: vfoo和Base :: bfoo的不同地址,以及vbar的相同地址
template<typename A, typename B>
A force_cast(B in){
union {
A a;
B b;
} u;
u.b = in;
return u.a;
};
template<typename T>
size_t get_vtable_function_address_o(T* obj, int vtable_offset){
return *((size_t*)((*(char**)obj + vtable_offset-1)));
};
template<typename T, typename F>
size_t get_vtable_function_address(T* obj, F function){
return get_vtable_function_address_o(obj, force_cast<size_t>(function));
};
int main (){
Base* a = new Base();
Base* b = new Child();
cout << get_vtable_function_address(a, &Base::vfoo) << endl;
cout << get_vtable_function_address(b, &Base::vfoo) << endl;
cout << get_vtable_function_address(a, &Base::vbar) << endl;
cout << get_vtable_function_address(b, &Base::vbar) << endl;
return 0;
}
这在GCC上可以正常工作,尽管我必须从vtable偏移中减去1才能使它工作,这一事实似乎有点不可思议。但这在Microsoft的编译器上不起作用(将&Base :: vfoo调整为size_t会返回一些垃圾而不是虚拟表偏移量)(此处的一些实验表明,此处的正确偏移量对于vfoo为0,对于vbar为4)>
我很清楚,这些东西是实现定义的,但是我希望有一种方法可以至少在一些常见的编译器(gcc,msvc和clang)上运行,因为vtables在这里很标准点(即使它需要编译器特定的代码)?
有什么办法吗?
注1:我只需要此即可处理单继承。我不使用多重继承或虚拟继承
注2:再次强调我不需要可调用函数,只需要测试子类是否覆盖了特定的虚函数。如果有一种方法可以执行此操作而无需深入研究vtables,那么那将是首选。
答案 0 :(得分:2)
在C ++ 11及更高版本中,通过decltype
和std::is_same
比较函数类型,我们可以获得所需的结果。
(如果无法使用C ++ 11,则仍可以为此目的使用typeid
和operator==(const type_info& rhs)
。)
由于Base::vfoo
被Child
覆盖,因此decltype(&Child::vfoo)
的类型为void (Child::*)()
,并且与decltype(&Base::vfoo)
的{{1}}不同。
因此
void (Base::*)()
是std::is_same<decltype(&Base::vfoo) , decltype(&Child::vfoo)>::value
。
(实际上,在C ++标准草案n3337的第4条中,它枚举了隐式转换的集合, 4.11指向成员转换的指针[conv.mem] / 2
- 类型“指向cv T类型的B成员的指针”的prvalue(其中B是类类型)可以转换为“指向cv T类型D的成员的指针”的prvalue,其中D是B的派生类(第10条)。如果B是D的不可访问的(第11条),模棱两可(10.2)或虚拟(10.1)的基类,或D的虚拟基类的基类,则该程序是必需的此转换格式不正确。转换的结果指向与指向转换之前的成员的指针相同的成员,但是它引用基类成员,就好像它是派生类的成员一样。结果引用D的B实例中的成员。由于结果的类型为“指向cv T类型D的成员的指针”,因此可以使用D对象取消引用。结果与使用D的B子对象取消引用B成员的指针相同。将null成员指针值转换为目标类型的null成员指针值。
,指出从false
到decltype(&Base::vfoo)
的隐式转换可能是合法的,但未提及相反方向之一。
此外, 5.2.9静态强制转换[expr.static.cast] / 12 ,
- “指向 cv1 T类型的D的成员的指针”类型的prvalue可以转换为“ to的指针类型”的prvalue。 类型为 cv2 T的B的成员”,其中B是D的基类(第10条),如果是从“类型T的B的指针到成员”到“成员的指针”的有效标准转换存在类型T的D”(4.11),并且 cv2 具有与 cv1 相同的cv资格,或具有更高的cv资格。然后,将空成员指针值(4.11)转换为目标类型的空成员指针值。如果类B包含原始成员,或者是包含原始成员的类的基类或派生类,则指向成员的结果指针将指向原始成员。否则,强制转换的结果是不确定的。 [注意:尽管类B不必包含原始成员,但取消引用成员指针的对象的动态类型必须包含原始成员;参见5.5。 — 尾注]
,指出使用decltype(&Child::vfoo)
从static_cast
到decltype(&Child::vfoo)
的显式转换也是合法的。
那么在这种情况下,彼此之间的法律强制转换是
decltype(&Base::vfoo)
并且此void (Child::*pb)() = &Base::vfoo;
void (Base ::*pc)() = static_cast<void(Base::*)()>(&Child::vfoo);
意味着static_cast
和&Base::vfoo
的类型彼此不同,没有任何显式强制转换。)
OTOH,因为&Child::vfoo
没有被Base::vbar
覆盖,所以Child
的类型为decltype(&Child::vbar)
,并且与void (Base::*)()
相同。
因此
decltype(&Base::vbar)
是std::is_same<decltype(&Base::vbar) , decltype(&Child::vbar)>::value
。
(似乎是n3337的 5.3.1一元运算符[expr.unary.op] / 3 ,
一元&运算符的结果是指向其操作数的指针。操作数应为左值或限定- ID 。如果操作数是 qualified-id ,命名为类型T的某个类C的非静态成员m,则结果的类型为“指向类型T的类C的成员的指针”,并且是一个prvalue指定厘米。否则,如果表达式的类型为T,则结果的类型为“指向T的指针”,并且是prvalue,它是指定对象(1.7)的地址或指向指定函数的指针。 [注意:特别是,类型为 cv T的对象的地址是“指向 cv T的指针”,具有相同的cv限定。 —尾注] [示例:
true
—最终示例]
,说明此行为。 here也对此段进行了有趣的讨论。)
总而言之,我们可以使用struct A { int i; };
struct B : A { };
... &B::i ... // has type int A::*
,decltype(&Base::...)
和decltype(&Child::...)
来检查是否重写了每个成员函数,如下所示:
Live DEMO (GCC / Clang / ICC / VS2017)
std::is_same
顺便说一句,我们还可以定义以下宏来简化这些操作:
// Won't fire.
static_assert(!std::is_same<decltype(&Base::foo) , decltype(&Child::foo)> ::value, "oops.");
// Won't fire.
static_assert( std::is_same<decltype(&Base::bar) , decltype(&Child::bar)> ::value, "oops.");
// Won't fire.
static_assert(!std::is_same<decltype(&Base::vfoo), decltype(&Child::vfoo)>::value, "oops.");
// Won't fire.
static_assert( std::is_same<decltype(&Base::vbar), decltype(&Child::vbar)>::value, "oops.");
然后让我们写
#define IS_OVERRIDDEN(Base, Child, Func) \
(std::is_base_of<Base, Child>::value \
&& !std::is_same<decltype(&Base::Func), decltype(&Child::Func)>::value)