测试子类是否覆盖基类中的虚函数

时间:2019-04-07 22:41:09

标签: c++

我正在寻找一种检查子类是否覆盖其基类上的函数的方法。如果成员函数指针不是虚拟的,则比较成员函数指针可以很好地工作,但是如果它们是虚拟的,则它不起作用。这段示例代码本质上就是我遇到的麻烦。

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,那么那将是首选。

1 个答案:

答案 0 :(得分:2)

在C ++ 11及更高版本中,通过decltypestd::is_same比较函数类型,我们可以获得所需的结果。 (如果无法使用C ++ 11,则仍可以为此目的使用typeidoperator==(const type_info& rhs)。)


由于Base::vfooChild覆盖,因此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

  
      
  1. 类型“指向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成员指针值。
  2.   

,指出从falsedecltype(&Base::vfoo)的隐式转换可能是合法的,但未提及相反方向之一。 此外, 5.2.9静态强制转换[expr.static.cast] / 12

  
      
  1. “指向 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。 — 尾注]
  2.   

,指出使用decltype(&Child::vfoo)static_castdecltype(&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

  
      
  1. 一元&运算符的结果是指向其操作数的指针。操作数应为左值或限定-   ID 。如果操作数是 qualified-id ,命名为类型T的某个类C的非静态成员m,则结果的类型为“指向类型T的类C的成员的指针”,并且是一个prvalue指定厘米。否则,如果表达式的类型为T,则结果的类型为“指向T的指针”,并且是prvalue,它是指定对象(1.7)的地址或指向指定函数的指针。 [注意:特别是,类型为 cv T的对象的地址是“指向 cv T的指针”,具有相同的cv限定。 —尾注] [示例:

    true
         

    —最终示例]

  2.   

,说明此行为。 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)