Home > Back-end >  static constexpr pointer to member function downcasting with multiple inheritance
static constexpr pointer to member function downcasting with multiple inheritance

Time:12-29

I have a Derived class that inherits from multiple Base classes. Each of these base classes has its own get function. At runtime I have an index corresponding to the base class whose get function I want to call.

I have hundreds of these Base classes so I was thinking of storing their get functions in a constexpr array of pointer-to-member functions, but I ran into a number of inconsistencies between clang, gcc, and msvc and now I'm not sure what the correct behavior is. I have a different version of the code that fills this array at runtime and it works perfectly, but it's doing needless work.

I reduced the problem to two base classes instead of hundreds and tried three types of pointer-to-member function calls:

  1. runtime downcasting from base to derived
  2. compile time downcasting from base to derived (outside of class definition)
  3. compile time downcasting from base to derived (inside class definition)

I tried three compilers with C 20 support:

  • Clang works for all three
  • GCC fails for #3
  • MSVC fails for #2 and #3

On GCC and MSVC it doesn't set the this pointer to the correct base class. It always uses the address of the first base class, printing 0 instead of 1.

#include <iostream>

struct Base0
{
    int value = 0;
    void get() { std::cout << value << std::endl; }
};
struct Base1
{
    int value = 1;
    void get() { std::cout << value << std::endl; }
};

struct Derived : Base0, Base1
{
    #ifdef _MSC_VER // static constexpr downcast pointer-to-member fails in msvc
    static const inline auto Get0 = reinterpret_cast<void(Derived::*)()>(&Base0::get);
    static const inline auto Get1 = reinterpret_cast<void(Derived::*)()>(&Base1::get);
    #else
    static constexpr auto Get0 = static_cast<void(Derived::*)()>(&Base0::get);
    static constexpr auto Get1 = static_cast<void(Derived::*)()>(&Base1::get);
    #endif
};

auto runtimeGet0 = static_cast<void(Derived::*)()>(&Base0::get);
auto runtimeGet1 = static_cast<void(Derived::*)()>(&Base1::get);

constexpr auto constexprGet0 = static_cast<void(Derived::*)()>(&Base0::get);
constexpr auto constexprGet1 = static_cast<void(Derived::*)()>(&Base1::get);

int main()
{
    Derived d;

    // 1
    (d.*runtimeGet0)(); // clang: 0, gcc: 0, msvc: 0
    (d.*runtimeGet1)(); // clang: 1, gcc: 1, msvc: 1

    // 2
    (d.*constexprGet0)(); // clang: 0, gcc: 0, msvc: 0
    (d.*constexprGet1)(); // clang: 1, gcc: 1, msvc: 0

    // 3
    (d.*Derived::Get0)(); // clang: 0, gcc: 0, msvc: 0
    (d.*Derived::Get1)(); // clang: 1, gcc: 0, msvc: 0
}

Try it here

Questions:

  • Which compiler is doing the right thing?
  • Why would storing the pointer-to-member function inside the class definition vs outside make a difference?
  • Why does MSVC need reinterpret_cast? Note that I'm using the /vmg and /vmm compiler flags. If I comment out the ifdef I get the peculiar error:

error C2440: 'static_cast': cannot convert from 'void (__cdecl Outer::Base0::* )(void)' to 'void (__cdecl Outer::Derived::* )(void)' note: Cast from base to derived requires dynamic_cast or static_cast

CodePudding user response:

The implicit member pointer conversion (static_cast is not required) requires that the derived class is complete, which it isn't in your static member initializers, which are not a complete-class context, see [conv.mem]/2.

The result of using reinterpret_cast is unspecified except for the round-trip conversion back to the original type. Using it directly in a member access causes undefined behavior. See [expr.reinterpret.cast]/10.


So, MSVC is correct to reject the static_cast and Clang and GCC should issue a diagnostic for it as well. Given that this should be ill-formed, there is no sense in asking whether the behavior of the program produced by GCC and Clang for #3 is wrong except if the developers intended this to be allowed as an extension in which case I wouldn't know where the intended behavior is documented. constexpr should not change anything about this.

Because using reinterpret_cast like this causes undefined behavior, MSVC is also not wrong in outputting unexpected results for #3, again except if MSVC is intending to support this as an extension, in which case I don't know the intended behavior.

For #2 (d.*constexprGet1)() MSVC is wrong (according to standard-conformance) to output 0. It should be 1. constexpr does not affect the value of the member pointer.

  • Related