MSVC Trickiness: CRTP Dependencies

I got complacent developing my game on a Mac and Linux boxes so far, and have neglected to actually compile my game on Windows, even though I from the start did intend Windows as a first-class deployment target. I’ve finally gotten around to it, and to no surprise, it’s not been particularly smooth. But some of the problems have been surprising to me.

One I had not previously encountered is this: One portion of my game uses a CRTP (Curiously Recurring Template Pattern) base, that abstracts away some repetitive bits that several derived types need. It looked something like this:

struct Meta {
    const char *stuff;
};

template <typename Derived>
struct Helper {
    static constexpr Meta kMeta = { Derived::kStuff };
};

struct Foo : Helper<Foo> {
    static constexpr const char *kStuff = "stuff";
};

This works fine on GCC, Clang, and ICC. But MSVC?

error C2039: 'kStuff': is not a member of 'Derived'

No way!

I did find this Stack Overflow question that seems to cover the same issue: CRTP compiling error, and it has a couple of workarounds (which do not directly work for me), as well as a partial explanation.

As a fuller explanation, I believe that (for C++17) this behavior is defined by [temp.point] ¶4:

For a class template specialization […], if [something not true in this case], [irrelevant]. Otherwise, the point of instantiation for such a specialization immediately precedes the […] definition that refers to the specialization.

In this case, the point of instantiation of Helper<Foo> is immediately before Foo. At this point, we can presume that Foo has been forward declared, but not necessarily provided with a definition. It’s in this context that Helper<Foo> is instantiated. At this time, in MSVC, it appears its static data member initializers are also evaluated – and it fails to find kStuff since Foo is only forward-declared at this time.

This clearly isn’t exactly what’s going on though, since if we provide it with an explicit forward declaration followed by an explicit instantiation, we get a different error:

template <typename T>
struct Base {
    static constexpr int x = T::y;
};

struct Foo;
template struct Base<Foo>;

struct Foo : Base<Foo> {
    static constexpr int y = 2;
};
error C2027: use of undefined type 'Foo'

Also, with this form, GCC and Clang start erroring as well:

error: incomplete type 'Foo' used in nested name specifier

This suggests that what’s really going on is MSVC is:

  • Foo defined with empty member list; compiler gets ready to start collecting members.
  • Compiler sees that Helper<Foo> is a base and instantiates it.
  • Instantiation looks up Foo::kStuff; it sees that Foo is defined but can’t find kStuff since the member list is empty; error.
  • Later if the above had succeeded, kStuff would have been added to Foo’s member list.

But it’s also not hard to disprove that theory either – for example, putting sizeof(T) inside of Base gets C2027 again, so clearly Foo is not actually marked as defined at that point, but there’s something funky going on such that we get C2039 instead of C2027 in the original example.

Anyway, I guess it doesn’t really matter what exactly is going on inside MSVC, as much as I would like to know. What I need is a workaround.

The Stack Overflow post suggests having the base define a function instead of a data member, e.g.:

template <typename Derived>
struct Helper {
    static constexpr Meta getMeta() { return { Derived::kStuff }; }
};

However, this isn’t suitable for my purposes – in this case, I wanted a static definition somewhere in memory with an unchanging address. A member function is still the way to do this, but it can’t be constexpr:

template <typename Derived>
struct Helper {
    static const Meta &getMeta() {
        static constexpr Meta kMeta{ Derived::kStuff };
        return kMeta;
    }
};

This works fine and solved my problem.

Tags: , ,

Leave a Reply