I'm encountering unexpected behavior on MSVC when storing a pointer to a virtual method in a static inline variable. The issue does not occur on GCC or Clang.
Specifically, when I store a pointer to a virtual method in a static inline variable (e.g. as part of a decorator pattern), the wrong virtual function is called, sometimes even the destructor (if it is first virtual method in the vtable).
Here's a minimal reproducible example:
#include <iostream>
template<auto Func, typename Class>
auto Decorator()
{
return [] <typename... Ts> (Ts...) -> void
{
using Cls = std::conditional_t<sizeof...(Ts) == 0, Class, void>; // crutch to compile
Cls Obj;
return (Obj.*Func)();
};
};
struct Foo
{
virtual void baz()
{
std::cout << "ccc\n";
}
virtual void bar()
{
std::cout << "aaa\n";
};
};
struct Foo2 : public Foo
{
virtual void bar() override
{
std::cout << "bbb\n";
};
static inline auto buz = Decorator<&Foo2::bar, Foo2>();
};
int main()
{
Foo2::buz(); // Displays "ccc"
}
If I remove inline, or make bar non-virtual, the bug disappears.
This only happens with MSVC (tested with /std:c++20). Clang and GCC correctly call the expected virtual function.
Why does MSVC misdispatch the virtual method call in this context? Is this a known compiler bug, ABI-related limitation, linker problem? UB? What's the correct and portable way to store pointers to virtual methods in static variables?
Any insight into this behavior would be appreciated. Especially explanations specific to MSVC’s handling of virtual method pointers.
UPD: Microsoft confirmed this is an ABI limitation unlikely to be fixed (via Jonathan Caves):
this is due to an ABI issue, both with the MSVC object model and the name decorator, so it is extremely unlikely we’ll be able to fix it any time soon.
newto avoid invoking the destructor, since in some iterations of reproducing the bug I placed destructor first instead of just virtual function. As for theconditional_tand the parameter pack: that a workaround to allow using an incomplete type (Class) at the point of lambda definition, while still compiling.Foo2isn't an incomplete type at the point you try to construct an instance (Cls Obj). Because member initializers are normally deferred until the type is complete, but here you use inference of the member type.