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:
- runtime downcasting from base to derived
- compile time downcasting from base to derived (outside of class definition)
- 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
}
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 theifdef
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.