6

When using the curiously recurring template pattern, I am unable to refer to typedefs belonging to the derived class only if I attempt to reference them from the base class; gcc complains no type named 'myType' in class Derived<...>. This seems inconsistent with what is otherwise possible using typedefs, templates, and curiously recurring relationships.

Consider:

/* crtp.cpp */

#include <iostream>
using namespace std;

// case 1. simple.

class Base {
public:
    typedef int a_t;

    a_t foo;
};

class Derived : public Base {
    a_t bar;
};

// case 2. template.

template<typename T>
class tBase {
public:
    typedef T b_t;
    T foo;
};

template <typename T>
class tDerived : public tBase<T> {
    typename tBase<T>::b_t bar;
};

// case 3. curiously recurring.

template <typename T, typename D>
class tCuriousBase {
public:
    typedef T c_t;
    c_t foo;
};

template <typename T>
class tCuriousDerived : public tCuriousBase<T,tCuriousDerived<T> > {
    typename tCuriousBase<T,tCuriousDerived<T> >::c_t bar;
};

// case 4. curiously recurring with member reference.

template <typename T, typename D>
class tCuriousMemberBase {
public:
    T foo;

    T get() {
        return static_cast<D*>(this)->bar;
    }
};

template <typename T>
class tCuriousMemberDerived : public tCuriousMemberBase<T, tCuriousMemberDerived<T> > {
public:
    T bar;

    tCuriousMemberDerived(T val) : bar(val) {}
};

// case 5. curiously recurring with typedef reference.

template <typename T, typename D>
class tCuriousTypeBase {
public:
    typedef T d_t;
    d_t foo;
    typename D::c_t baz;
};

template <typename T>
class tCuriousTypeDerived : public tCuriousTypeBase<T, tCuriousTypeDerived<T> > {
public:
    typedef T c_t;
    typename tCuriousTypeBase<T,tCuriousTypeDerived<T> >::d_t bar;
};

// entry point

int main(int argc, char **argv) {
    Derived::a_t one = 1;
    tDerived<double>::b_t two = 2;
    tCuriousDerived<double>::c_t three = 3;
    double four = tCuriousMemberDerived<double>(4).get();
    tCuriousTypeBase<double, tCuriousDerived<double> >::d_t five = 5;
    // tCuriousTypeDerived<double>::d_t six = 6; /* FAILS */

    cout << one   << endl;
    cout << two   << endl; 
    cout << three << endl;
    cout << four  << endl;
    cout << five  << endl;
    // cout << six << endl;
}

From (1), we see that typedefs are indeed inherited from base to derived; a typedef declared in the base class can be accessed through the derived class.

From (2), we see that this is still true if both classes are templates.

From (3), we see that this typedef inheritance can still exist in the presence of a curiously recurring template relationship; we refer to the base's typedef via the derived class in our declaration of three.

From (4), we see that member variables of the derived class may be readily accessed from the base class.

From (5), we see that we can access a typedef defined on a template parameter (this works when we declare five using types that are not related by inheritance).

As soon as we make the template parameter a derived class in (6), however, suddenly the typedef becomes inaccessible, even though it is seemingly as well-defined as any member variable in the derived class.

Why is this?

Peter Wood
  • 23,859
  • 5
  • 60
  • 99
trbabb
  • 1,894
  • 18
  • 35

1 Answers1

10

This is the "circular dependecies", or "incomplete-type" alter-ego.

Template "meta-programming" is "programming types", but it requires a certain level of sematics to be known to properly instantiate types.

Consider this analogy:

class A; //forwarded

class B
{
   A* pa; //good: we know how wide a pointer is, no matter if we don't know yet anything about A.
   A a; // bad: we don't know how much space it requires
};

class A
{
  float m;
}; // now we know, but it's too late

This can be solved by placing A before B, but if A is

class A
{
   B m;
};

Thhere is no other solution than pointers, since A recursion will be infinite. (A should contain itself, not refer to another copy)

Now, with the same analogy, let's program "types":

template<class D>
class A
{
   typedef typename D::inner_type my_type; //required D to be known when A<D> is instantiated...
   my_type m; // ... otherwise we cannot know how wide A<D> will be.
};

This declaration is not itself bad,until we start to define D as ...

class D: //here we only know D exist
    public A<D> //but A size depende on D definition...
{
  ....
  typedef long double; inner_type
  ....
}; // ....we know only starting from here

So, basically, we don't know (yet) how wide is A at the time need to use it to create D.

One way to break this "circularity" is to use some "traits classes" outside of the CRT loop:

struct traits
{
   typedef long double inner_type;
   ....
};

template<class D, class Traits>
class A
{
  // this is easy: Traits does not depend itself on A
  typedef typename Traits::inner_type my_type;
  ....
};

template<class Traits>
class D: public A<D, Traits>
{
  typedef typename Traits::inner_type inner_type;
};

An we can finally declare

typedef D<traits> D_Inst;

In other words, the coherence between A::my_type and D::inner_type is ensured by traits::inner_type, whose definition is independent.

Emilio Garavaglia
  • 20,229
  • 2
  • 46
  • 63
  • I don't think that analogy applies. The error is still thrown if `Base` instead declares a member function using a derived typedef return value, without ever naming a member variable based on `Derived`. In fact, the error persists if I only create a typedef in `Base` that aliases the typedef in `Derived`, and never again refer to it! Also, I think type resolution must inherently happen before actual class instantiation even can begin. There are no loops in the type definitions, so I still don't see why this can't work. – trbabb Apr 19 '13 at 08:39
  • Also, when I call `static_cast(this)->foo()`, the compiler has no problem resolving and checking the return type of `foo()`, which may itself depend on properties of `Base`. I don't see why resolving the type of a typedef is any harder. – trbabb Apr 19 '13 at 08:43
  • @trbabb: it's "analogy", not "identity". The variable declaration is just an example. The source of the problem is the fact that `D` is considered *incolmplete* at the time `A` is used to form `D` So you are forming a type using an incomplete one inside it. The way you use it is irrelevant (I just did an example, but it is not the only possible). It is the *incompleteness* that blocks the semantic analysis. – Emilio Garavaglia Apr 19 '13 at 08:49
  • @trbabb: ... About calling a function, that's a completely different beast: you are just pointing an entry of a symbol table whose content (call address) will be later defined by the linker. – Emilio Garavaglia Apr 19 '13 at 08:52
  • Type-checking that the return type of `foo()` in my example is compatible with anything I may assign it to happens long before the linker gets involved, no? – trbabb Apr 19 '13 at 08:56
  • yes and no: between the class braces, all functions are considered instantiated simultanmeously. fnA cannot call FnB if its prototype is not known, but if they are member they can call each other independently on the declaration order (dual pass semantic analysis of members). The problem -ultimatley- is that `A` is used BEFORE the D's open brace, so outside the scope of the second pass. You can be right if the scope of the second pass is exented starting from the `:`, but that's not what the actual specifications are. – Emilio Garavaglia Apr 19 '13 at 09:07
  • ... The technical reason is that, because the linking has to be a separate process, there cannot be a "second global pass" at the "global scope", (what is outside all the braces) since the global scope itself must exist across different independent translation units. (So such a "pass" will always be partial and may lead to different results depending on the compilation order) – Emilio Garavaglia Apr 19 '13 at 09:14