Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Test if a subclass overrides a virtual function in a base class

Tags:

c++

I'm looking for a way to check to see if a child class overrides a function on it's base class. Comparing member function pointers works perfectly fine if they aren't virtual, but if they are virtual it does not work. This sample code is essentially what I'm having trouble with.

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;
}

Logically there's no reason this shouldn't work, but the C++ spec says otherwise (comparing virtual functions addresses this way is implementation defined).

On GCC, type punning &Base::vfoo and &Child::vfoo to int has them both be "1" (and vbar is "9"), which look to be vtable offsets. The following code appears to correctly get the function addresses out of the vtable, and correctly reports different addresses for Child::vfoo and Base::bfoo, and the same address for 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;
}

This works fine on GCC, though the fact that I have to subtract 1 from the vtable offset for it to work seems a bit weird. But it doesn't work on microsoft's compiler (punning &Base::vfoo to a size_t returns some garbage instead of a virtual table offset) (some experimenting here suggests that the correct offsets here would be 0 for vfoo and 4 for vbar)

I am WELL AWARE that this stuff is implementation-defined, but I am hoping there is a way to do this that at least works on a few common compilers (gcc, msvc, and clang) since vtables are pretty standard at this point (even if it requires compiler-specific code)?

Is there any way to do this?

Note 1: I only need this to work on single inheritance. I don't use multiple or virtual inheritance

Note 2: Re-emphasizing that I do not need a callable function, I only need to test if a child class has overwritten a specific virtual function. If there's a way to do this without needing to dig into the vtables for stuff, then that would be preferred.

like image 926
TylerGlaiel Avatar asked Apr 07 '19 22:04

TylerGlaiel


People also ask

Is it mandatory to override virtual function derived class?

It is not mandatory for the derived class to override (or re-define the virtual function), in that case, the base class version of the function is used. A class may have virtual destructor but it cannot have a virtual constructor.

Can virtual functions be overridden?

Virtual functions are member functions whose behavior can be overridden in derived classes. As opposed to non-virtual functions, the overriding behavior is preserved even if there is no compile-time information about the actual type of the class.

What happens if you don't override virtual functions?

If you don't override a pure virtual function in a derived class, that derived class becomes abstract: class D2 : public Base { // no f1: fine. // no f2: fine, we inherit Base::f2.

Is it necessary to override every virtual function?

The virtual keyword can be used when declaring overriding functions in a derived class, but it is unnecessary; overrides of virtual functions are always virtual. Virtual functions in a base class must be defined unless they are declared using the pure-specifier.


1 Answers

In C++11 and over, comparing function types by decltype and std::is_same, we can get the desired results. (If C++11 is not available for you, you can still use typeid and operator==(const type_info& rhs) for this purpose.)


Since Base::vfoo is overridden by Child, the type of decltype(&Child::vfoo) is void (Child::*)() and different from decltype(&Base::vfoo) which is void (Base::*)(). Thus

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

is false.

( In fact, in the clause 4 of C++ standard draft n3337 which enumerates the set of implicit conversions, 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.

, states that the implicit conversion from decltype(&Base::vfoo) to decltype(&Child::vfoo) can be legal but does not mention the one of the reverse direction. In addition, 5.2.9 Static cast [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 ]

, states that the explicit conversion using static_cast from decltype(&Child::vfoo) to decltype(&Base::vfoo) can also be legal. Then legal casts with each other in this case are

void (Child::*pb)() = &Base::vfoo;
void (Base ::*pc)() = static_cast<void(Base::*)()>(&Child::vfoo);

and this static_cast means that the types of &Base::vfoo and &Child::vfoo are different with each other without any explicit cast.)


OTOH, since Base::vbar is not overridden by Child, the type of decltype(&Child::vbar) is void (Base::*)() and same with decltype(&Base::vbar). Thus

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

is true.

( It seems that 5.3.1 Unary operators [expr.unary.op] / 3 of n3337,

  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 ]

, states this behavior. Interesting discussion of this paragraph is also found here.)


In summary, we can check whether each member function is overridden or not using decltype(&Base::...), decltype(&Child::...) and std::is_same as follows:

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

BTW, we can also define the following macro to make these easer:

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

which then let us write

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.
like image 58
Hiroki Avatar answered Nov 15 '22 05:11

Hiroki