-1

This code compiles and works only when calling CRTP's methods:

template <typename CRTP>
struct crtp
{
    // using type = typename CRTP::type; // will not compile
    void print() const
    {
        using type = typename CRTP::type; // compiles
        static_cast<const CRTP&>(*this).print();
    }
};

#include <iostream>

int main()
{
    struct foo : crtp<foo>
    {
        using type = int;
        void print() const {
            std::cout << "John Cena\n";
        };
    } f{};

    const crtp<foo>& c = f;
    c.print();

    return 0;
}

Upon crtp<foo>'s instantiation, foo is incomplete. But its methods can be used.

However, CRTP's types can't used dues CRTP's incompleteness outside functions.

Why is it allowed to defer a check for incompletness until a class's function is called, but not upon object's instatiation?


When using CRTP::type within the class body (uncommented), one will get a compilation error:

<source>:4:33: error: no type named 'type' in 'foo'
    using type = typename CRTP::type; // will not compile
                 ~~~~~~~~~~~~~~~^~~~
Sergey Kolesnik
  • 3,009
  • 1
  • 8
  • 28

2 Answers2

2

Note that even if you write a member function definition inside the class template definition, it is not instantiated when the class template is instantiated.

foo can't be defined until its base class crtp<foo> has been completely defined, but the type alias in the definition of crtp<foo> requires foo's definition to be known, which requires crtp<foo> to already be defined, which requires foo, and so on...

Member functions, on the other hand, are not compiled until after their class has been fully defined, so the type alias is fine there.
(That is, instantiating void crtp<foo>::print() const works because it only happens after foo has been defined and crtp<foo> has been instantiated.)

Separating the class template definition from the member function definition makes the two phases clearer:

template <typename CRTP>
struct crtp
{
    // Can't compile unless CRTP is defined at this point.
    using type = typename CRTP::type; 
    void print() const;
};


template <typename CRTP>
void crtp<CRTP>::print() const
{
    // Also can't compile unless CRTP is defined at this point, but
    // this is a later point than the class definition.
    using type = typename CRTP::type;
    static_cast<const CRTP&>(*this).print();
}

molbdnilo
  • 64,751
  • 3
  • 43
  • 82
  • clearer indeed. – Sergey Kolesnik Aug 31 '22 at 09:46
  • Is it possible to trick the compiler with SFINAE to use the types within CRTP's body? – Sergey Kolesnik Aug 31 '22 at 09:48
  • You can *"inverse"* relation ship with external type_trait: `typename struct inner_type; template <> struct inner_type { using type = int; };` and use `typename inner_type::type;`. – Jarod42 Aug 31 '22 at 09:54
  • A very nasty "add another level of indirection" version I found (by trial and error) is `using type = typename std::remove_reference())>::type;`, but I can't explain why that worked so I'm not even sure that it should... – molbdnilo Aug 31 '22 at 10:00
1

Within CRTP, derived classes are incomplete:

struct foo : crtp<foo> // foo incomplete here
{
    using type = int;
    void print() const {
        // foo complete here
        std::cout << "John Cena\n";
    }
}; // foo complete here

foo is also complete in void crtp<foo>::print() const.

Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • That I have noticed and stated in the question. THe question boils down to why is it allowed to be complete within a function body, but not within a class body, given the class has not been instantiated. What is the reasoning behind that logic? – Sergey Kolesnik Aug 31 '22 at 09:31
  • basically I have in my question "Why is A allowed but not B", and your answer is "A is allowed, B is not" – Sergey Kolesnik Aug 31 '22 at 09:36