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

178 Views Asked by At

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?

2

There are 2 best solutions below

0
lobelk On BEST ANSWER

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.

15
alfC On

Not exactly sure if this answers your question, but I have seen that a way around this redundancy is to check for it in the implementation as soon as the nested TType is used in the base CRTP class.

(Since, in your case, the Base class is not doing anything, I put it in the constructor for illustration purposes; there might be better places to put the static assert)

#include<type_traits>

template <typename Impl, typename T>
struct Base
{
//  typename Impl::TType t;  // not working
    Base() {
      static_assert(std::is_same_v<T, typename Impl::TType>);
    }
};

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

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

int main()
{
  MyImplementation mi(3);

  return 0;
}

https://godbolt.org/z/TGnn7ofPY