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

Tyl*_*iel 5 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;
}
Run Code Online (Sandbox Code Playgroud)

从逻辑上讲,没有理由不应该这样做,但是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;
}
Run Code Online (Sandbox Code Playgroud)

尽管我必须从vtable偏移中减去1才能运行,但这在GCC上仍然可以正常工作。但这在Microsoft的编译器上不起作用(将&Base :: vfoo调整为size_t会返回一些垃圾而不是虚拟表偏移量)(此处的一些实验表明,此处的正确偏移量对于vfoo为0,对于vbar为4)

我很清楚,这些东西是实现定义的,但是我希望至少有一种方法可以在至少一些常见的编译器(gcc,msvc和clang)上运行,因为vtables在这一点上是非常标准的(甚至是否需要特定于编译器的代码)?

有什么办法吗?

注意1:我只需要这个就可以处理单一继承。我不使用多重继承或虚拟继承

注2:再次强调我不需要可调用函数,只需要测试子类是否覆盖了特定的虚函数。如果有一种方法可以做到而无需深入研究vtables,那么那将是首选。

Hir*_*oki 6

在 C++11 及以上版本中,通过decltype和比较函数类型std::is_same,我们可以得到想要的结果。(如果 C++11 对您不可用,您仍然可以使用typeidoperator==(const type_info& rhs)用于此目的。)


由于Base::vfoo通过覆盖Child的类型,decltype(&Child::vfoo)就是void (Child::*)()不同于和decltype(&Base::vfoo)void (Base::*)()。因此

std::is_same<decltype(&Base::vfoo) , decltype(&Child::vfoo)>::value
Run Code Online (Sandbox Code Playgroud)

false

(实际上,在枚举隐式转换集的 C++ 标准草案 n3337 的第 4 条中,4.11 指向成员转换的指​​针 [conv.mem] / 2

  1. “指向 cv T 类型 B 成员的指针”类型的纯右值,其中 B 是类类型,可以转换为“指向类型 cv T 的 D 成员的指针”类型的纯右值,其中 D 是派生类( B 的第 10 条。如果 B 是 D 的不可访问(第 11 条)、二义性(10.2)或虚拟(10.1)基类,或 D 的虚拟基类的基类,则需要这种转换的程序是畸形。转换的结果引用与转换发生之前指向成员的指针相同的成员,但它引用基类成员,就好像它是派生类的成员一样。结果引用了 D 的 B 实例中的成员。由于结果的类型为“指向 cv T 类型的 D 成员的指针”,因此可以使用 D 对象取消引用它。结果与指向 B 成员的指针被 D 的 B 子对象解除引用一样。

, 表示从decltype(&Base::vfoo)to的隐式转换decltype(&Child::vfoo)可以是合法的,但没有提到反向之一。另外,5.2.9 静态施法 [expr.static.cast] / 12

  1. 类型的“指针类型的d的部件A prvalue CV1 T”可被转换为类型的prvalue“指针B的成员”类型的CV2 T,其中B是d的基类(第10)中,如果存在从“指向 T 类型 B 成员的指针”到“指向 T 类型 D 成员的指针”的有效标准转换(4.11),并且cv2cv1具有相同的 cv 限定,或比cv1更大的 cv 限定。然后空成员指针值(4.11)被转换为目标类型的空成员指针值。如果类 B 包含原始成员,或者是包含原始成员的类的基类或派生类,则指向成员的结果指针指向原始成员。否则,转换的结果是未定义的。[注意:虽然B类不需要包含原始成员,但解除引用成员指针的对象的动态类型必须包含原始成员;见 5.5。—尾注]

, 声明使用static_castfrom decltype(&Child::vfoo)to的显式转换decltype(&Base::vfoo)也可以是合法的。那么在这种情况下彼此的合法强制转换是

void (Child::*pb)() = &Base::vfoo;
void (Base ::*pc)() = static_cast<void(Base::*)()>(&Child::vfoo);
Run Code Online (Sandbox Code Playgroud)

static_cast意味着该类型的&Base::vfoo&Child::vfoo没有任何明确的转换相互不同)。


OTOH,因为Base::vbar没有被覆盖Child的类型,decltype(&Child::vbar)就是void (Base::*)()与相同decltype(&Base::vbar)。因此

std::is_same<decltype(&Base::vbar) , decltype(&Child::vbar)>::value
Run Code Online (Sandbox Code Playgroud)

true

(似乎是n3337 的 5.3.1 一元运算符 [expr.unary.op] / 3

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

    struct A { int i; };
    struct B : A { };
    ... &B::i ...       // has type int A::*
    
    Run Code Online (Sandbox Code Playgroud)

    — 结束示例 ]

, 说明此行为。还可以在此处找到对本段的有趣讨论。)


综上所述,我们可以检查每个成员功能是否被覆盖或者不使用decltype(&Base::...)decltype(&Child::...)并且std::is_same如下所示:

现场演示 (GCC / Clang / ICC / VS2017)

// 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.");
Run Code Online (Sandbox Code Playgroud)

顺便说一句,我们还可以定义以下宏来使这些更容易:

#define IS_OVERRIDDEN(Base, Child, Func)                                 \
(std::is_base_of<Base, Child>::value                                     \
 && !std::is_same<decltype(&Base::Func), decltype(&Child::Func)>::value)
Run Code Online (Sandbox Code Playgroud)

然后让我们写

static_assert( IS_OVERRIDDEN(Base, Child, foo) , "oops."); // Won't fire.
static_assert(!IS_OVERRIDDEN(Base, Child, bar) , "oops."); // Won't fire.
static_assert( IS_OVERRIDDEN(Base, Child, vfoo), "oops."); // Won't fire.
static_assert(!IS_OVERRIDDEN(Base, Child, vbar), "oops."); // Won't fire.
Run Code Online (Sandbox Code Playgroud)