CRTP: Pass types from derived class to base class
Asked Answered
P

1

5

In CRTP, the base class can use functions and variables from derived class. However, the types from derived class cannot be used directly by base class, see code below:

#include <iostream>

template <class Derived>
class A {
public:
    //using Scalar = typename Derived::Scalar; // Error!
    static constexpr int NA1 = Derived::NB1;
    static constexpr int NA2 = Derived::NB2;
    static constexpr int NA3 = Derived::NB3;
};

template <int _N = 2>
class B : public A<B<_N>> {
public:
    using Scalar = double;
    static constexpr int NB1 = 1;
    static constexpr int NB2 = _N;
    static constexpr int NB3 { sizeof(Scalar) };
};

int main(int argc, char** argv)
{
    using Type = B<2>;
    std::cout << Type::NA1 << ' '
              << Type::NA2 << ' '
              << Type::NA3 << '\n';
}

// output:
// 1 2 8

If the line using Scalar = typename Derived::Scalar; is uncommented, an error occurs:

main.cpp:6:11: error: invalid use of incomplete type 'class B<2>'

I know that the type (Scalar) can be passed to base class as a template parameter, but why can't it be used just like the variables? Is it just a language rule? Or is there any logical restriction that makes this unable to implement?

Paperback answered 25/8, 2021 at 10:31 Comment(0)
H
9

Inside A, B is an incomplete type - a compiler hasn't yet seen the complete declaration of B, so you can't use Scalar inside the declaration of A. This is a natural restriction.

The difference between a type and a scalar in your example arises because instantiation of initialization of NA happens not at the point of declaration, but only after B was seen by a compiler (and became a complete type).

Let's change the code and force a compiler to use NA value inside the class declaration:

template <class Derived>
class A {
public:
    static constexpr int NA1 = Derived::NB1;

    std::array<int, NA1> foo();
};

Now you'll get essentially the same error:

<source>:8:41: error: incomplete type 'B<2>' used in nested name specifier
    8 |     static constexpr int NA1 = Derived::NB1;
      |                                         ^~~

This is similar to member functions: you can't use a CRTP base type in their declarations, but you can use that type in their bodies:

void foo() {
    std::array<int, NA1> arr;
    // ...
}

will compile because instantiation happens at the point where a base class is already a complete type.

Harrietteharrigan answered 25/8, 2021 at 11:7 Comment(2)
I was under the impression that NA* are all initialized with dependent names and hence two phase look up would mean Derived is actually bound to B by the compiler by the time instantiation happens. Seems I made some incorrect assumptions there. Would removing static and/or constexpr make my assumption correct?Weslee
@TanveerBadar Answering this question needs language lawyer assistance. If I got your question correctly, I suspect that the answer is no because Derived in A is still an incomplete type. The instantiation of A has to always precede the instantiation of B because A is a base class of B.Harrietteharrigan

© 2022 - 2024 — McMap. All rights reserved.