Inheriting from a template class using the inheriting class with C++20 concepts

Viewed 340

I'm trying to write a Singleton base class similar to Ogre3Ds Ogre::Singleton using C++20's std::default_initializable.

#include <concepts>

template <std::default_initializable T>
struct Singleton {};

class Foo: public Singleton<Foo> {};

This code does not compile:

error C7602: 'Singleton': the associated constraints are not satisfied

Would I expect it to? If not: Why?

I'm using the latest MSVC with /std:c++latest.

2 Answers

The reason why you can't do this is because at the point where Foo is given as a template parameter to Singleton, it is an incomplete type, and an incomplete type is not initializable at all (so it can not be default-initialized)

Here is a sample program that give a way more understandable error:

template<typename T>
struct A
{
    T value{};
};

struct B : A<B>{};

Compiler error (GCC 10.2)

<source>:5:7: error: 'A<T>::value' has incomplete type
    5 |     T value{};
      |       ^~~~~
<source>:8:8: note: forward declaration of 'struct B'
    8 | struct B : A<B>
      |        ^

Compiling this with GCC gives a more helpful error message:

...
/opt/compiler-explorer/gcc-10.2.0/include/c++/10.2.0/type_traits:895:52:
error: static assertion failed: template argument must be a complete
class or an unbounded array
895 |       static_assert(std::__is_complete_or_unbounded(__type_identity<_Tp>{}),
    |                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~

In particular, at the point of declaring Singleton<Foo>, Foo is not yet a complete type: the class body has not opened yet. As such there is no way for the compiler to know at that point whether or not the concept can truly be satisfied.

You can see a similar issue with the use of static assertions; consider, for example, the following (which also fails to compile for much the same reason):

template <typename T>
struct Singleton {
    static_assert(std::is_nothrow_destructible_v<T>);
};

class Foo: public Singleton<Foo> {};

If you wish to enforce the constraint regardless, you can still use concepts: you just need to use it in a location where the type will be a complete type. For example, you might static_assert that the concept holds in the destructor:

template <typename T>
struct Singleton {
    ~Singleton() {
        static_assert(std::default_initializable<T>);
    }
};

struct Foo : Singleton<Foo> {};             // compiles
struct Bar : Singleton<Bar> { Bar(int); };  // doesn't compile (when used)

(Live)

Related