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.