Search code examples
c++inner-classesforward-declarationcrtp

CRTP - visibility of the type of a nested leaf class


I would like to understand if it is possible to use a nested class of a leaf CRTP class in the base CRTP class. The example below demonstrates the problem.

#include <iostream>

using namespace std;

template<class T>
class A
{

protected:
    T* asLeaf(void)
        {return static_cast<T*>(this);}
    T const* asLeaf(void) const
        {return static_cast<T const*>(this);}
public:

    struct Inner
    {int a = 10;};

    void dispInner(void) const
        {std::cout << asLeaf()->inner.a << std::endl;}

    // I would like to use T::Inner in this class, e.g.
    // typename T::Inner mvInnerA;
    // However, I understand that it is not possible to
    // use it in the form that is stated above. Thus, 
    // I am looking for any possible workarounds. 

};


class B: public A<B>
{
public:
    struct Inner: public A<B>::Inner
    {int b = 20;};

protected:
    friend A<B>;
    B::Inner inner;

public:
    void dispInner(void) const
        {
            A<B>::dispInner();
            std::cout << asLeaf()->inner.b << std::endl;
        }
};


int main()
{

    B b;
    b.dispInner();

    return 0;

}

EDIT

I would like to provide several further comments based on the feedback that I have received:

  • I am aware that I may not be using adequate design practices. In particular, it may be questioned whether A should be aware of the existence of inner. However, I would like to define an object inner of the type B::Inner in A instead of providing the definition of inner in B and using it in A.
  • I am aware that I cannot forward declare B and/or B::Inner and of the reasons why this cannot be done. Thus, technically, the design problem does not have a solution. However, I am looking for a feasible workaround.

I have already considered several alternative solutions:

  • One of the possible feasible solutions is not to make attempts to 'define' B::Inner inner in A and use the member functions of A to provide the functionality that allows to modify the A<B>::Inner part of B::Inner inner.
  • Another possible solution is to define the classes A<B>::Inner and B::Inner explicitly (i.e. not as nested classes). However, I would prefer to avoid this, because, by design, it is not expected that any classes that do not derive from A will need to interact with A<B>::Inner or the classes that derive from A<B>::Inner

Both solutions that I have presented may be acceptable. However, I am looking for any feasible alternatives.


Solution

  • The standard say that:

    A class is considered a completely-defined object type (or complete type) at the closing } of the class-specifier.

    It follows that B isn't a completely-defined object when you specialize A as A<B>. Therefore you can't expect to be able to access its members or types or whatever from within the definition of A (even though you can call back the derived class from within the definition of a member method of A, that is perfectly legal instead other than the purpose of the CRTP idiom).
    In other terms, when you do this:

    typename T::Inner mvInnerA
    

    You have no guarantees that T is a completely-defined object and that's why you get the error.


    A few alternatives:

    1. You can define mvInnerType as a function instead of as a type and use it as a factory to create objects of type T::inner:

      [static] auto mvInnerA() {
          return typename T::Inner{};
      }
      

      Use it either as:

      auto foo = A<B>::mvInnerA();
      

      Or:

      auto foo = obj.mvInnerA();
      

      The right form depends on the fact that you make it static or not.
      Note that you can still use the hidden type somehow, even if its name isn't accessible:

      using HiddenType = decltype(A<B>::mvInnerA());
      HiddenType foo = A<B>::mvInnerA();
      
    2. You can define mvInnerA using a template a alias declaration like this:

      template<typename U = T>
      using mvInnerA = typename U::Inner;
      

      Then use it as:

      auto foo = A<B>::mvInnerA<>{};
      

      For the type T is (let me say) indirectly used through U only when mvInnerA is instantiated, you don't have the problem mentioned above. The price to pay for that is the presence of an annoying <> and the fact that one can pass a custom type to mvInnerA.