Home > database >  No member named "XXX" in a simple CRTP case
No member named "XXX" in a simple CRTP case

Time:12-29

Here I have a simple CRTP case:

#include <cstddef>
#include <utility>

template <typename Impl>
class base
{
    constexpr static size_t impl_num = Impl::num;
};

template <typename Impl>
class deriv : public base<deriv<Impl>>
{
    friend class base<deriv<Impl>>;

    constexpr static size_t num = Impl::num_in;
};

class actual_impl
{
public:
    constexpr static size_t num_in = 10;
};

using my_type = deriv<actual_impl>;

int main()
{
    my_type a{};
}

This snippet compiles fine but when I change the base class to:

#include <cstddef>
#include <utility>

template <typename Impl>
class base
{
    constexpr static std::make_index_sequence<Impl::num> idx{};
};

template <typename Impl>
class deriv : public base<deriv<Impl>>
{
    friend class base<deriv<Impl>>;

    constexpr static size_t num = Impl::num_in;
};

class actual_impl
{
public:
    constexpr static size_t num_in = 10;
};

using my_type = deriv<actual_impl>;

int main()
{
    my_type a{};
}

Clang complains that error: no member named 'num' in 'deriv<actual_impl>'. I'm just confused why the first case works but not the second one, what's the fundamental difference between these two since it seems to me that in both cases Impl::num_in are used in base class.

In general, is it possible for base class to use typedefs or constexprs from Impl?

CodePudding user response:

The fundamental difference is the moment when you're trying to access the internals of the Impl class. Impl in base<Impl> is an incomplete type, and there are certain restrictions on what you can do with it.

In particular, you can't access num data member inside base, that's why the line

constexpr static std::make_index_sequence<Impl::num> idx{};

causes a compilation error. Note that to define the base class, a compiler has to know the value of Impl::num right at that moment.

In contrast to that, in your first example, Impl::num is used only to initialize a value of impl_num, which otherwise doesn't depend on Impl::num. Instantiation of that initialization happen later, at the point when Impl becomes a complete type. Hence, there is no error.

If you slightly change the definition,

template<typename Impl>
class base {
    constexpr static decltype(Impl::num) impl_num = Impl::num;
    // or 
    constexpr static auto impl_num = Impl::num;
}

and make impl_num type dependent on Impl, you'll get the same error by the same reason.

Adding indirection doesn't help, the following code also fails to compile:

template<typename Impl>
class base {
    constexpr static size_t impl_num = Impl::num;
    constexpr static std::make_index_sequence<impl_num> idx{};
};

In general, is it possible for base class to use typedefs or constexprs from Impl?

It depends. You can use them only in contexts where instantiations happen when Impl is a complete type. For example,

template<typename Impl>
class base {
public:
    void foo() {
        decltype(Impl::num) impl_num = 0;
    }
};

is fine, but

template<typename Impl>
class base {
public:
    decltype(Impl::num) foo() { 
        return 0;
    }
};

is not.

The standard trick to avoid potential problems with incomplete types in CRTP is the introduction of a helper traits class:

// Just forward declarations
template<typename Impl> class deriv;
class actual_impl;

using my_type = deriv<actual_impl>;

template<class> struct traits;
template<> struct traits<my_type> {
    using num_type = std::size_t;
};

template <typename Impl>
class base {
public:
    typename traits<Impl>::num_type foo() {
        return 0;
    }
};

// Now actual definitions
// ...

Here, to access traits<Impl> internals, Impl doesn't have to be a complete type.

  • Related