c++inlinevirtual-functionspointer-to-membervtable

MSVC calls wrong virtual method when storing a pointer to virtual method in a static inline variable


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.

https://developercommunity.visualstudio.com/t/MSVC-calls-wrong-virtual-function-when-p/10901289#T-ND10903843


Solution

  • It's a bug in MSVC - which stems from the fact that Foo2 is incomplete, if you remove std::conditional_t crutch and try to compile on clang you'll get an rather descriptive error:

    The code is the original with minor change to Decorator function:

    template<auto Func, typename Class>
    auto Decorator()
    {
        return [] <typename... Ts> (Ts...) -> void
        {
            Class Obj;
            return (Obj.*Func)();
        };
    };
    
    <source>:9:15: error: variable has incomplete type 'Foo2'
        9 |         Class Obj;
    

    But then MSVC will accept this code and go over the rails. If we examine the assembly output we'll see that:

    Outside these unusual circumstances:

    int main() 
    {
        Foo2 Obj;
        (Obj.*&Foo2::bar)();
    }
    

    Creates a call to [thunk]:Foo2::`vcall'{8,{flat}}' }' .

    Otherwise your code creates a call to:

    [thunk]:Foo2::`vcall'{0,{flat}}' }'

    Observed with godbolt on x86-64 asm.

    This is compile type error that propagates into runtime one. Since initially the pointer to member function gets translated to the wrong function vtable address.

    Further-more if you make the inheritance virtual as:

    struct Foo2 : virtual public Foo
    {
        virtual void bar() override
        {
            std::cout << "bbb\n";
        };
    
        static inline auto buz = Decorator<&Foo2::bar, Foo2>();
    };
    

    The MSVC compiler will bork.

    Now I'm not a standards guy so not sure what the right behavior is.

    But the correct way would be not to use an incomplete type when taking the address of the virtual member function. In other words do:

    struct Foo2 : public Foo
    {
        virtual void bar() override
        {
            std::cout << "bbb\n";
        };
    
        static inline auto buz = Decorator<&Foo::bar, Foo2>();
    };
    

    This works on MSVC, gcc and clang with your original code.