MSVC’s /experimental:constevalVfuncNoVtable is non-conforming

P1064 “Allowing Virtual Function Calls in Constant Expressions,” adopted for C++20, permits you to mark virtual functions as constexpr and then call them at compile-time. In fact, you can even mark them consteval (also a C++20 feature), which means you can call them only at compile-time. Thus (Godbolt):

struct Base {
  consteval virtual int f() const { return 1; }
};
struct Derived : Base {
  consteval int f() const override { return 2; }
};

constexpr const Base *m() { static constexpr Derived d; return &d; }

static_assert(std::is_polymorphic_v<Base>);
static_assert(std::is_polymorphic_v<Derived>);
static_assert(sizeof(Base) == 8);
static_assert(sizeof(Derived) == 8);
static_assert(typeid(*m()) == typeid(Derived));
static_assert(m()->f() == 2);

None of the static_asserts above should be surprising. Base is certainly polymorphic (it has virtual methods); the result of typeid and the behavior of m()->f() are mandated by the Standard; and sizeof(Base) is 8 because of its 8-byte vptr. Now, Base’s vptr is really useful only at compile time, because f, the only virtual function in Base’s vtable, is callable only at compile time. So you might think we could somehow save some space by giving Base a “consteval-only vptr” — omitting the vptr from its struct layout at runtime. However, there are several problems with that idea.

First: Can we give Base two different layouts — include the vptr in the constexpr-time layout and omit it from the runtime layout? No, because that would make sizeof give two different values depending on whether the expression sizeof(Base) was seen in a manifestly constant-evaluated context. That would be a nightmare.

Second: If we omit Base’s vptr at compile time and at runtime, (A) how would that affect its type traits? and (B) where would we hang its runtime type information?

Today I learned that MSVC 16.11 (way back in 2021) added an experimental switch to see what happens if you answer those questions the “wrong” way. The switch is named /experimental:constevalVfuncNoVtable. If you turn it on, you’ll observe the following surprising behavior (Godbolt):

static_assert(!std::is_polymorphic_v<Base>);
static_assert(!std::is_polymorphic_v<Derived>);
static_assert(sizeof(Base) == 1);
static_assert(sizeof(Derived) == 1);
static_assert(typeid(*m()) == typeid(Base));
static_assert(m()->f() == 2);

In this experimental mode, MSVC omits Base’s vptr — changing its sizeof at both compile time and runtime. Yet m()->f() continues to perform virtual dispatch and call Derived::f()! (The compiler’s built-in constexpr interpreter knows the true dynamic type of every object created at constexpr time, so this is easy for it. And you can’t call f at runtime.)

In this mode, MSVC decides that Base is actually non-polymorphic (despite the Standard’s clearly stating that it is: it has virtual methods). Having discarded polymorphic-ness, MSVC also classifies Base as empty and trivially copyable; furthermore, typeid(*m()) can skip the evaluation of *m() (because such evaluation is required only for glvalue expressions of polymorphic type) and invariably return typeid(Base).

Could MSVC make this mode’s behavior more conforming by patching their type-traits to report that Base is polymorphic (and non-empty, and non-trivially copyable), and making typeid report the correct dynamic type via the same compiler magic that powers the dynamic dispatch in m()->f()? Almost, but not quite. They could use compiler magic to make typeid work polymorphically at constexpr time; but without a vtable to hang the RTTI on, typeid would certainly have to act non-polymorphically at runtime, as seen here.

Conclusion: MSVC’s five-year-old /experimental:constevalVfuncNoVtable gives you a non-conforming experience, in which types with all-consteval virtual functions are treated as non-polymorphic. Its opposite, /experimental:constevalVfuncVtable, gives conforming behavior that matches GCC and Clang (as far as I know).

Posted 2026-03-12