1. Introduction
Add a function to the standard library which takes a member function pointer as an argument, and returns the same member function pointer devirtualized:
template < typename Pmf > requires ( is_member_function_pointer_v < Pmf > && is_same_v < Pmf , remove_cvref_t < Pmf > > ) constexpr Pmf devirtualize ( Pmf pmf , pmf_class_t < Pmf > const volatile * pobj = nullptr );
We can invoke a base member function on a derived object at compile time.
11.7.3 Virtual functions [class.virtual] 16 Explicit qualification with the scope operator (7.5.4.3) suppresses the virtual call mechanism. [Example 10: class B { public: virtual void f(); }; class D : public B { public: void f(); }; void D::f() { /* ... */ B::f(); } Here, the function call in D::f really does call B::f and not D::f. — end example]”
Currently C++ does not allow invoking a base member function on a derived object at runtime -- the member function pointer always resolves to the member function of the derived object. While this does make sense for traditional runtime polymorphism, this behavior is less desired when selecting member functions for other types of runtime polymorphism as it occurs a superfluous dual dispatch.
Just as the programmer decides whether to call the virtual function virtually or not at compile time, the programmer should be able to do the same at runtime.
2. Motivation
2.1. Sample code
class Abstract { public : virtual void some_virtual_function () = 0 ; virtual void some_virtual_function () const = 0 ; }; class Base : public Abstract { public : void some_virtual_function () override { } void some_virtual_function () const override { } }; class Derived : public Base { public : void some_virtual_function () override { } void some_virtual_function () const override { } void some_deducing_this_member_function ( this Derived ) { } }; int main () { Derived derived ; Derived * pderived = & derived ; Derived & rderived = derived ; derived . some_virtual_function (); // Direct call to Derived::some_virtual_function pderived -> some_virtual_function (); // Virtual call in general case to Derived::some_virtual_function rderived . some_virtual_function (); // Virtual call in general case to Derived::some_virtual_function // NOTE: A derived class can call its base class member function directly // at compile time using a qualifier in these cases Base:: derived . Base :: some_virtual_function (); // Direct call to Base::some_virtual_function pderived -> Base :: some_virtual_function (); // Direct call to Base::some_virtual_function rderived . Base :: some_virtual_function (); // Direct call to Base::some_virtual_function void ( Base ::* bmfp )() = & Base :: some_virtual_function ; void ( Derived ::* dmfp )() = & Derived :: some_virtual_function ; void ( Derived ::* dmfpc )() const = static_cast < void ( Derived ::* )() const > ( & Derived :: some_virtual_function ); // NOTE: A derived class can NOT call its base class member function at runtime ( derived . * bmfp )(); // Derived::some_virtual_function ( derived . * dmfp )(); // Derived::some_virtual_function ( derived . * dmfpc )(); // Derived::some_virtual_function const return 0 ; } void Base_some_virtual_function ( Base & instance ) { // Direct call to Base::some_virtual_function instance . Base :: some_virtual_function (); } void const_Base_some_virtual_function ( const Base & instance ) { // Direct call to Base::some_virtual_function instance . Base :: some_virtual_function (); }
Looking closer at the following lines:
Derived derived ; void ( Base ::* bmfp )() = & Base :: some_virtual_function ; ( derived . * bmfp )(); // Derived::some_virtual_function
Even though the programmer explicitly stated that they want to call Base’s some_virtual_function, Derived’s some_virtual_function is always called. The programmer was never given the same choice that they have at compile time. Again this is correct for traditional runtime polymorphism. However, for a callback using raw pointers, function_ref [2], or nontype function_ref [3], the end programmer wants the choice and really should choose based on the scenario.
2.2. Unknown state
For instance, if the programmer received some pointer or reference to some instance and as such doesn’t know the exact type of the instance than the logical and safe choice is to call the method virtually even though it incurs dual dispatch costs. This scenario occurs frequently when integrating with 3rd party libraries that are unaware of the callback type and as such the callback type is instantiated late.
function_ref < void () > factory ( Base & base ) { // base could be of type Base, Derived or some other class not known at this function's creation // in which case dual dispatch is still a good idea function_ref < void () > fr = { nontype <& Base :: some_virtual_function , some_virtual_tag_class > , base }; return fr ; }
A library could provide an overload via a tag class, in this example some_virtual_tag_class, in order to allow the programmer to choose whether the function will be called virtually or directly, in this case the former.
2.3. Known state
However, if the programmer knows the instantiated type, likely because the programmer was the creator, then the programmer wants to avoid the superfluous cost of calling the member function through the member function pointer. This scenario occurs even more frequently when the programmer is calling his own code, his team member’s code or a 3rd party library that is aware of the callback type and provide callback instances. As such the callback type is instantiated early.
2.3.1. Known state - Exact
In this scenario, the state is known and is the same type of the class that houses the member function. As such there is no need resolve the member function at runtime since the type is known.Base base ; function_ref < void () > fr = { nontype <& Base :: some_virtual_function , some_direct_tag_class > , base };
Again a library could provide an overload via a tag class, in this example some_direct_tag_class, in order to allow the programmer to choose whether the function will be called virtually or directly, in this case the later.
2.3.2. Known state - Derived
This would also work safely for derived state calling their base class member functions at runtime without the additional member function pointer costs. After all, Derived is still a Base.Derived derived ; function_ref < void () > fr = { nontype <& Base :: some_virtual_function , some_direct_tag_class > , derived };
It should ne noted, that besides callbacks of single functions this functionality could be of benefit to callbacks of n-ary functions such as found in proxy [4], dyno [5] and boost ext te [6].
3. Possible implementations
3.1. GNU g++, LLVM clang, Intel ICX
#include <cassert>// assert #include <cstdint>// uintptr_t #include <type_traits>// is_member_function_pointer, is_polymorphic, is_same, remove_cvref namespace std { /* ============ The next 14 lines are to get the 'Class' from R '(Class:*)(Params...)' */ template < typename Pmf > struct pmf_class ; template < bool ex , typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) noexcept ( ex ) > { using t = C ;}; template < bool ex , typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) const noexcept ( ex ) > { using t = C ;}; template < bool ex , typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) volatile noexcept ( ex ) > { using t = C ;}; template < bool ex , typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) const volatile noexcept ( ex ) > { using t = C ;}; template < bool ex , typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) & noexcept ( ex ) > { using t = C ;}; template < bool ex , typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) const & noexcept ( ex ) > { using t = C ;}; template < bool ex , typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) volatile & noexcept ( ex ) > { using t = C ;}; template < bool ex , typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) const volatile & noexcept ( ex ) > { using t = C ;}; template < bool ex , typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) && noexcept ( ex ) > { using t = C ;}; template < bool ex , typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) const && noexcept ( ex ) > { using t = C ;}; template < bool ex , typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) volatile && noexcept ( ex ) > { using t = C ;}; template < bool ex , typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) const volatile && noexcept ( ex ) > { using t = C ;}; template < typename Pmf > using pmf_class_t = pmf_class < Pmf >:: t ; /* =================================================================================== */ template < typename Pmf > requires ( is_same_v < Pmf , remove_cvref_t < Pmf > > && is_member_function_pointer_v < Pmf > ) constexpr Pmf devirtualize ( Pmf pmf , pmf_class_t < Pmf > const volatile * const pobj = nullptr ) { using Class = pmf_class_t < Pmf > ; if constexpr ( std :: is_polymorphic_v < Class > ) { using std :: uintptr_t ; static_assert ( sizeof ( void ( * )( void )) == sizeof ( uintptr_t ) ); // The next line ensures that a member function // pointer is the size of two pointers static_assert ( ( 2u * sizeof ( uintptr_t )) == sizeof ( pmf ) ); // The next line checks if the address stored inside the // member function pointer has the least significant bit set. // If the bit is not set, the function is already devirtualised. auto addr = * static_cast < std :: uintptr_t *> ( static_cast < void *> ( & pmf )); if ( addr & 1u ) // we need to devirtualise { -- addr ; // Subtract 1 to get the real index into the vtable std :: uintptr_t const index = addr / sizeof ( void ( * )( void )); void ( ** vtable )( void ) = nullptr ; if ( nullptr == pobj ) { // Compiler support needed here to // get the address of Class's vtable vtable = __get_vtable ( Class ); } else { vtable = * ( void ( *** )( void )) pobj ; } void ( *& p )( void ) = * static_cast < void ( ** )( void ) > ( static_cast < void *> ( & pmf )); p = vtable [ index ]; } } else { // If 'Class' is not polymorphic, then every function pointer // must be non-virtual. The following 'assert' makes sure of this: assert ( 0u == ( 1u & * static_cast < std :: uintptr_t *> ( static_cast < void *> ( & pmf )) ) ); } return pmf ; } } // close namespace std
Tested and working on GodBolt: https://godbolt.org/z/hxf3n9Ws9
3.2. Microsoft Visual C++
#include <cstring>// memcmp #include <type_traits>// is_member_function_pointer, is_polymorphic, is_same, remove_cvref namespace std { /* ============ The next 26 lines are to get the 'Class' from 'R(Class:*)(Params...)' */ template < typename Pmf > struct pmf_class ; template < typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) noexcept > { using t = C ;}; template < typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) const noexcept > { using t = C ;}; template < typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) volatile noexcept > { using t = C ;}; template < typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) const volatile noexcept > { using t = C ;}; template < typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) & noexcept > { using t = C ;}; template < typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) const & noexcept > { using t = C ;}; template < typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) volatile & noexcept > { using t = C ;}; template < typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) const volatile & noexcept > { using t = C ;}; template < typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) && noexcept > { using t = C ;}; template < typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) const && noexcept > { using t = C ;}; template < typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) volatile && noexcept > { using t = C ;}; template < typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) const volatile && noexcept > { using t = C ;}; template < typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) noexcept ( false) > { using t = C ;}; template < typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) const noexcept ( false) > { using t = C ;}; template < typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) volatile noexcept ( false) > { using t = C ;}; template < typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) const volatile noexcept ( false) > { using t = C ;}; template < typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) & noexcept ( false) > { using t = C ;}; template < typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) const & noexcept ( false) > { using t = C ;}; template < typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) volatile & noexcept ( false) > { using t = C ;}; template < typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) const volatile & noexcept ( false) > { using t = C ;}; template < typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) && noexcept ( false) > { using t = C ;}; template < typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) const && noexcept ( false) > { using t = C ;}; template < typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) volatile && noexcept ( false) > { using t = C ;}; template < typename R , class C , typename ... P > struct pmf_class < R ( C ::* )( P ...) const volatile && noexcept ( false) > { using t = C ;}; template < typename Pmf > using pmf_class_t = pmf_class < Pmf >:: t ; /* =================================================================================== */ template < typename Pmf > requires ( is_member_function_pointer_v < Pmf > && is_same_v < Pmf , remove_cvref_t < Pmf > > ) constexpr Pmf devirtualize ( Pmf pmf , pmf_class_t < Pmf > const volatile * const pobj = nullptr ) { using Class = pmf_class_t < Pmf > ; if constexpr ( std :: is_polymorphic_v < Class > ) { // If we have a virtual member function pointer, // it will contain a pointer to a thunk containing // two CPU instructions as follows: // (1) 64-Bit : 48 8b 01 mov rax,QWORD PTR [rcx] // ff 60 ZZ jmp QWORD PTR [rax+ZZ] // // (2) 32-Bit : 8b 01 mov eax,DWORD PTR [ecx] // ff 60 ZZ jmp DWORD PTR [eax+ZZ] // // The 8-Bit vtable index is the byte marked ZZ char unsigned const * pf = * static_cast < char unsigned **> ( static_cast < void *> ( & pmf )); if ( 0x48 == * pf ) ++ pf ; // indicates 64-Bit instruction if ( 0 == std :: memcmp ( " \x8b\x01\xff\x60 " , pf , 4u ) ) { pf += 4u ; unsigned const index = * pf / sizeof ( void ( * )( void )); void ( ** vtable )( void ) = nullptr ; if ( nullptr == pobj ) { // Compiler support needed here to // get the address of Class's vtable vtable = __get_vtable ( Class ); } else { vtable = * ( void ( *** )( void )) pobj ; } void ( *& p )( void ) = * static_cast < void ( ** )( void ) > ( static_cast < void *> ( & pmf )); p = vtable [ index ]; } } return pmf ; } } // close namespace std
4. Design considerations
Unlike calling base class member functions with derived instances at compile time via a qualifier, it is undesirable to add keywords/vernacular/qualifiers at the point of function call, that is to choose direct or virtual at call time. This incurs additional runtime costs of potentially increased member function pointer size and execution time, even for those that don’t need the choice as in traditional runtime polymorphism.
Derived derived ; void ( Base ::* bmfp )() = & Base :: some_virtual_function ; // NOTE: This is NOT desired ( derived . * bmfp )() direct ; // Base::some_virtual_function ( derived . * bmfp )() virtual ; // Derived::some_virtual_function
It is also undesirable to add keywords/vernacular at the point of member function ponter initialization. This also incurs additional runtime costs of potentially increased member function pointer size and execution time, even for those that don’t need the choice as in traditional runtime polymorphism.
Derived derived ; void ( Base ::* bmfp1 )() = & Base :: some_virtual_function direct ; void ( Base ::* bmfp2 )() = & Base :: some_virtual_function virtual ; What is desired is no change to member function pointer , at all ! Rather , a new intrinsic consteval function would be created called to_free_function_pointer . This function would not take member function pointer at runtime but only a member function pointer initialization statement , & class_name :: member_function_name , at compile time . Technically , it could also take a static_cast of a member function pointer initialization to a specific member function pointer type to allow explicit choosing of overloaded methods . What gets returned is just a free function pointer that points to the actual member function or a thunk where the this reference is the first parameter . For Deducing this [ 7 ] member functions , the this type could be a value instead of a reference and as such this new function would just be a passthrough . This intrinsic function would also be overloaded for free functions in which case it would just be a passthrough . Derived derived ;// initialized with member functions void ( * bfp1 )( Base & ) = to_free_function_pointer ( & Base :: some_virtual_function ) ;void ( * dfp )( Derived & ) = to_free_function_pointer ( & Derived :: some_virtual_function ) ;void ( * dfpc )( const Derived & ) = to_free_function_pointer ( static_cast < void ( Derived ::* )() const > ( & Derived :: some_virtual_function )) ;void ( * ddtfp )( Derived ) = to_free_function_pointer ( & Derived :: some_deducing_this_member_function ) ;// initialized with free functions (always passthrough) void ( * bfp2 )( Base & ) = to_free_function_pointer ( Base_some_virtual_function ) ;void ( * bfp3 )( const Base & ) = to_free_function_pointer ( const_Base_some_virtual_function ) ;
With this, Base::some_virtual_function can be called at runtime, simply initialized, like [member] function pointers, without having to use a lambda. The end result is member function pointers’ initialization syntax can be used to select member functions with the knowledge that the selected is the one that actually will be called.
NOTE: The following is invalid code because there is no function when the member function declaration is pure.
void ( * bfp )( Abstract & ) = to_free_function_pointer ( & Abstract :: some_virtual_function );
The brevity of the name of the intrinsic function is less relevant as it will in all likelihood be concealed in library implementations rather than used directly.
Summary The advantages to C++ with this proposal is manifold.
A seemingly oversight in C++ gets fixed by allowing calling a base member function from a derived instance at runtime Mitigates a bifurcation by allowing one to interact with member functions regardless of whether they are a Deducing this [7:1] member function or a legacy member function Mitigates another bifurcation by allowing one to interact with functions regardless of whether they are a member function or a free function Matches existing practice by allowing users to use member function pointer initialization to select member functions with the confidence that it is the function that will be called
5. Proposed wording
The proposed wording is relative to [N4950].
In subclause 13.10.3.1.11 [temp.deduct.general], append a paragraph:
11 -- Monkeys eat lots of bananas
6. Impact on the standard
This proposal is a library extension. The addition has no effect on any other part of the standard.
7. Impact on existing code
No existing code becomes ill-formed. The behaviour of all existing code is unaffected.