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

标签 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 上,输入双关语 &Base::vfoo 和 &Child::vfoo 到 int 使它们都为“1”(vbar 为“9”),这看起来是 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 才能工作这一事实似乎有点奇怪。但是它在微软的编译器上不起作用(将 &Base::vfoo 双关到 size_t 会返回一些垃圾而不是虚拟表偏移量)(这里的一些实验表明这里的正确偏移量对于 vfoo 是 0,对于 vbar 是 4)

我很清楚这些东西是实现定义的,但我希望有一种方法可以做到这一点,它至少适用于一些常见的编译器(gcc、msvc 和 clang),因为 vtables 在这方面非常标准点(即使它需要特定于编译器的代码)?

有什么办法吗?

注意 1:我只需要它来处理单一继承。我不使用多重继承或虚拟继承

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

最佳答案

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


因为 Base::vfooChild 覆盖,所以 decltype(&Child::vfoo) 的类型是 void ( Child::*)() 不同于 decltype(&Base::vfoo)void (Base::*)()。 因此

std::is_same<decltype(&Base::vfoo) , decltype(&Child::vfoo)>::value

( 事实上,在枚举隐式转换集的C++标准草案n3337的第4条中,4.11 Pointer to member conversions [conv.mem]/2,

  1. A prvalue of type “pointer to member of B of type cv T”, where B is a class type, can be converted to a prvalue of type “pointer to member of D of type cv T”, where D is a derived class (Clause 10) of B. If B is an inaccessible (Clause 11), ambiguous (10.2), or virtual (10.1) base class of D, or a base class of a virtual base class of D, a program that necessitates this conversion is ill-formed. The result of the conversion refers to the same member as the pointer to member before the conversion took place, but it refers to the base class member as if it were a member of the derived class. The result refers to the member in D’s instance of B. Since the result has type “pointer to member of D of type cv T”, it can be dereferenced with a D object. The result is the same as if the pointer to member of B were dereferenced with the B subobject of D. The null member pointer value is converted to the null member pointer value of the destination type.

,指出从 decltype(&Base::vfoo)decltype(&Child::vfoo) 的隐式转换是合法的,但没有提到其中之一反方向。 此外,5.2.9 静态转换 [expr.static.cast]/12

  1. A prvalue of type “pointer to member of D of type cv1 T” can be converted to a prvalue of type “pointer to member of B” of type cv2 T, where B is a base class (Clause 10) of D, if a valid standard conversion from “pointer to member of B of type T” to “pointer to member of D of type T” exists (4.11), and cv2 is the same cv-qualification as, or greater cv-qualification than, cv1. Then null member pointer value(4.11) is converted to the null member pointer value of the destination type. If class B contains the original member, or is a base or derived class of the class containing the original member, the resulting pointer to member points to the original member. Otherwise, the result of the cast is undefined. [Note: although class B need not contain the original member, the dynamic type of the object on which the pointer to member is dereferenced must contain the original member; see 5.5. — end note ]

,指出使用 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&Child::vfoo 的类型彼此不同,没有任何显式转换.)


OTOH,因为 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. The result of the unary & operator is a pointer to its operand. The operand shall be an lvalue or a qualified- id. If the operand is a qualified-id naming a non-static member m of some class C with type T, the result has type “pointer to member of class C of type T” and is a prvalue designating C::m. Otherwise, if the type of the expression is T, the result has type “pointer to T” and is a prvalue that is the address of the designated object (1.7) or a pointer to the designated function. [ Note: In particular, the address of an object of type “cv T” is “pointer to cv T”, with the same cv-qualification. — end note ] [ Example:

    struct A { int i; };
    struct B : A { };
    ... &B::i ...       // has type int A::*
    

    — end example ]

,说明此行为。 还发现了对本段的有趣讨论 here .)


总而言之,我们可以使用decltype(&Base::...), decltype(&Child::...) 检查每个成员函数是否被覆盖> 和 std::is_same 如下:

Live DEMO (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.");

顺便说一句,我们还可以定义以下宏来简化这些操作:

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

然后让我们写

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.

关于c++ - 测试子类是否覆盖基类中的虚函数,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55564311/

相关文章:

c++ - 如何在 QT 中解析关联的 JSON 数组?

c++ - 尽可能多地屏蔽 C++ 中的类实现

c++ - 输入字符串时使用 cin 无限循环,而需要输入数字

c++ - 跨编译器的诊断不一致,以缩小非类型模板参数中的转换范围

c++ - 如何在 Windows 8 上使用 C++ 将 .jpg 转换为 .png?

c++ - 找到MFC Dialog的当前位置

c++ -::SomeMethod() 是什么意思 - 作用域解析运算符

c++ - free() 运算符是否从动态变量中删除地址?

C++ 在对象中保留 Lambda 表达式

c++ - 无法通过move_pages()查询