Search code examples
c++templatesinheritancecrtptype-deduction

CRTP design where base class instantiates members of types specified by the derived class


As described in crtp-pass-types-from-derived-class-to-base-class by Evg, the compiler is unable to deduce the type Impl::TType in the declaration of class Base:

template <typename Impl>
struct Base
{
  typename Impl::TType t;
};

struct MyImplementation : public Base<MyImplementation>
{
  using TType = int;
  TType t;

  MyImplementation(TType n) : t(n) {};
};

int main()
{
  MyImplementation mi(3);

  return 0;
}

However, I want a CRTP design where the user writes an implementation which derives from Base with the only requirement that the implementation class specifies the type TType so that the Base class can instantiate members of that type. In other words, the class Base expects the implementation to define type TType. For example, it may have to perform the operation on an integer but various implementations can use different int types (e.g. int, unsigned int, long, short ...). TType is thus implementation-specific and should not concern the class Base. The brute approach is to do:

template <typename Impl, typename T>
struct Base
{
  T t;
};

struct MyImplementation : public Base<MyImplementation, T>
{
  using TType = T;  //uneccessary but left for consistency
  TType t;

  MyImplementation(TType n) : t(n) {};
};

But here the type TType does concern the Base and leaves more room for mistakes. For example, if a user wants to template TType, he/she is required to write class MyImplementation : public Base< MyImplementation<T>, T >, which is a bad API (especially if Base requires defining more than one type; e.g. public Base< MyImplementation<T, V, Q>, T, V, Q >), making it easy to make mistakes such as:

template <typename T, typename Q>
struct MyImplementation : public Base<MyImplementation<T, Q>, Q>
{
  using TType = T;  //user expects that Base will use T as TType, while Q will be used instead
};

Is anybody aware of a modern efficient approach to this?


Solution

  • This is what I ended up doing. It is not the nicest, but I think it is manageable and API does not suffer. Let me know if I missed something.

    #include <type_traits>
    #include <cstddef>
    #include <new>
    #include <iostream>
    
    template <typename T>
    concept HasTType = requires
    {
        typename T::TType;
        // other requirements for the implementation class
    };
    
    template <typename T, int S>
    class AssertConcept
    {
      protected:
        AssertConcept() {
          static_assert(sizeof(typename T::TType) < S, "TType is too big");
          static_assert(HasTType<T>, "concept HasTType not satisfied");
        }
    };
    
    template <class Impl, int S = 16>
    class Base : private AssertConcept<Impl, S>
    {
      private:
        // reserve stack memory for creation of objects of type TType
        std::byte _ptr_ttype[S];
    
      public:
        Base() {
          // create objects of type TType in the reserved memory
          ::new (static_cast<void*>(_ptr_ttype)) Impl::TType{};
        }
    
        auto see_value() {return *reinterpret_cast<Impl::TType*>(_ptr_ttype);}
    
    };
    
    struct MyImplementation : public Base<MyImplementation>
    {
      using TType = int;
      TType t;
    
      MyImplementation(TType n) : t(n) {};
    };
    
    int main()
    {
      MyImplementation mi(3);
    
      std::cout << mi.see_value() << std::endl;
    
      return 0;
    }
    

    This will check if the implementation satisfies the concept HasTType before class Base is instantiated and without any overhead. This is based on the fact that parent class' ctor is invoked before child class' ctor.

    Furthermore, if the class Base needs to instantiate and manage objects of type TType, it can do it using placement new. This has a small overhead because the size of TType is unknown and more memory than necessary needs to be allocated, but this can be acceptable in most cases. The user can also pass the size of TType as a template argument if he/she wants to avoid this overhead, but I consider this to be better than passing TType since the user mostly won't specify the size. Concept also needs to check that the size is not too small.

    The alternative is to pass TType as a template parameter of Base, which is in my opinion a bit worse API-wise, but at least the user can be assured T=TType.