14

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

14
  • @JaMiT Cls is Foo2 which is incomplete. godbolt.org/z/rTsMsWMdM Commented May 8 at 18:32
  • 1
    @DominikKaszewski You're right - I’ve cleaned up the example now. Originally, I used new to avoid invoking the destructor, since in some iterations of reproducing the bug I placed destructor first instead of just virtual function. As for the conditional_t and the parameter pack: that a workaround to allow using an incomplete type (Class) at the point of lambda definition, while still compiling. Commented May 8 at 19:25
  • 3
    I wonder if Foo2 isn'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. Commented May 8 at 20:35
  • 1
    My wild guess is since the type is incomplete MSVC doesn't expect vcalls and haven't fixed the vtable to account for the base class. Commented May 8 at 20:47
  • 1
    @AnArrayOfFunctions: If the type is indeed incomplete, then MSVC's bug is in trying to compile the code instead of outputting a diagnostic about creating an instance of an incomplete type. Commented May 8 at 21:05

2 Answers 2

18

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.

Sign up to request clarification or add additional context in comments.

2 Comments

The underlying issue appears to be that MSVC is a bit over-eager about parsing declarations, and allows names in the declaration statement to become available before they should, sometimes. This can allow types to be used before they're complete, and allow instances of the class to be accessed by code inside the class body (before the instances are actually declared, and while the class is still incomplete), in certain situations, so I suspect the same thing's going on here. (Line 7 comment based on C++17, the wording has changed slightly since then.)
1

Indeed, as the other answer states, it is a bug in MSVC handling of virtual function pointers for incomplete classes, reported here: https://developercommunity.visualstudio.com/t/Wrong-virtual-function-is-called/10901516

The code from the question can be reduced even further:

template<auto Func>
constexpr auto Decorator() {
    return [](auto && obj) {
        return (obj.*Func)();
    };
};

struct Foo {
    virtual constexpr int baz() {
        return 1;
    }
};

struct Foo2 : Foo {
    virtual constexpr int bar() {
        return 2;
    };
    static inline auto buz = Decorator<&Foo2::bar>();
};

static_assert( Foo2::buz(Foo2{}) == 2 );

int main() {
    return Foo2::buz(Foo2{}); // return 1 in MSVC
}

The MSVC bug appears only in runtime (the program returns 1 instead of expected 2). At the same time, constant evaluation of Foo2::buz(Foo2{}) works fine in MSVC. Online demo: https://gcc.godbolt.org/z/864d5nGzs

The workaround from the other answer:

static inline auto buz = Decorator<&Foo::bar, Foo2>();

will not work if the virtual function bar is not present in Foo and is introduced only in Foo2.

Another workaround that I can suggest is to change the definition of Decorator, eliminating the lambda and the "crutch" from it:

template<auto Func, typename Class>
struct Decorator {
    constexpr auto operator ()() const {
        Class Obj;
        return (Obj.*Func)();
    };
};
...
struct Foo2 : public Foo
{
    virtual void bar()  {
        std::cout << "bbb\n";
    };
    static inline Decorator<&Foo2::bar, Foo2> buz;
};

Now the program is correctly processed by Clang, GCC, EDG and MSVC: https://gcc.godbolt.org/z/TK7fxr3Gr

2 Comments

Interesting... In my real codebase I was using a functor (not a lambda), similar to your Decorator struct, and still got the same issue on MSVC. So this workaround might not be reliable in some cases.
@ArtemSelivanov, the benefit of class template here is that its operator() is not instantiated immediately when Foo2 is incomplete, unlike lambda's one.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.