Is it possible to access child types in c++ using CRTP?
Asked Answered
D

5

18

I have this toy example,

template <typename TChild>
struct Base {
    template <typename T>
    using Foo = typename TChild::template B<T>;
};

struct Child : Base<Child> {
    template <typename T>
    using B = T;
};


using Bar = Child::Foo<int>;

which fails to compile. The intention is that I have a parent class that provides type computations based on members of the child class. The child class is provided via CRTP. However the line

using Foo = typename TChild::template B<T>;

fails to compile:

<source>: In instantiation of 'struct Base<Child>':
<source>:16:16:   required from here
<source>:13:11: error: invalid use of incomplete type 'struct Child'
   13 |     using Foo = typename TChild::template B<T>;
      |           ^~~
<source>:16:8: note: forward declaration of 'struct Child'
   16 | struct Child : Base<Child> {
      |        ^~~~~

Am I being naive in expecting such a construct to work?

Failing code at https://godbolt.org/z/5Prb84

Droopy answered 17/3, 2021 at 7:21 Comment(3)
Using a traditional "traits" class might be a better option.Wysocki
@evg you might be right but I have come up with a solution for the above https://mcmap.net/q/676439/-is-it-possible-to-access-child-types-in-c-using-crtp but I don't see why it should when the original attempt doesn't. The template still has to be checked. It looks like one pathway has more relaxed rules than the other.Droopy
@Droopy using a nested type requires a complete type of nested and of enclosing class. Base template is instantiated at the moment of deriving some child class from it. If it got a child class of its own, the trait class declared before the concrete Base, that one is a complete class (and will be instantiated before). Using members of Base works only because they aren't types and template parameter is considered a complete type at the moment of instantiation in case of CRTP. One still should be careful and sometimes use this-> to avoid conflicts with global scope.Barium
W
12

Let me post another way to do it:

template<typename TChild, class T>
struct GetB {
    using Type = typename TChild::template B<T>;
};

template<typename TChild>
struct Base {
    template<typename T>
    using Foo = typename GetB<TChild, T>::Type;
};

struct Child : Base<Child> {
    template<typename T>
    using B = T;
};

I don't have a language-lawyer-type explanation why this works, but it should be related to having an additional level of indirection. When a compiler sees

using Foo = typename TChild::template B<T>;

it can (and will) check and complain right at this point that an incomplete type is used. However, when we wrap access to B<T> into a function or a struct,

using Foo = typename GetB<TChild, T>::Type;

then we're not accessing internals of TChild at this point, we're just using the name of it, which is fine.

Wysocki answered 17/3, 2021 at 8:12 Comment(0)
H
9

Issue with CRTP is that derived class is incomplete in CRTP definition, so you cannot use its using.

in

template <typename T>
using Foo = typename TChild::template B<T>;
  • complete type of TChild would be required because of the ::.
  • TChild is non-dependent of template T, so first pass check should be done (but fails)

You might use external traits to handle that situation

template <typename C, typename T>
struct Traits_For_Base
{
    using type = typename C::template B<T>;
};

template <typename TChild>
struct Base {
    template <typename T>
    using Foo = typename Traits_For_Base<TChild, T>::type;
};

Traits_For_Base<TChild, T> is dependent of T, so nothing to do for first pass check. and, with second pass check (dependent of T), Child would be complete.

Demo

or you might change your alias to make in type dependent of the template parameter of the class Base:

template <typename TChild>
struct Base {
    template <typename T,
              typename C = TChild,
              std::enable_if_t<std::is_same_v<C, TChild>, int> = 0> // To avoid hijack
    using Foo = typename C::template B<T>;
};

C is dependent of template, so cannot be checked in first phase.

Demo

Huck answered 17/3, 2021 at 9:32 Comment(0)
D
4

The following gives the exact same result as required. It's still a mystery as to why it should work when the previous technique does not.

#include <type_traits>
#include <string>

template <typename TChild>
struct Base {

    template <typename T>
    static auto foo(){
        return typename TChild::template B<T>();
    }

    template <typename T>
    using Foo = std::decay_t<decltype(foo<T>())>;
};

struct Child : Base<Child> {
    template <typename T>
    using B = T;
};

static_assert(std::is_same<Child::B<int>,int>::value,"");
static_assert(std::is_same<Child::B<std::string>,std::string>::value,"");

https://godbolt.org/z/b6Y5Tb

Droopy answered 17/3, 2021 at 7:43 Comment(1)
If you want to also deal with a class type without default constructor or with private or deleted default constructor, or a type which is not legal as a return type, you could use type_identity<typename TChild::template B<T>>{} as the return type, and then Foo = typename decltype(foo<T>())::type. (std::type_identity is not in C++14, but trivial to write your own)Esdras
B
1

The problem is at stands is that instantiation of nested template requires a complete type of enclosing class and a declaration of template B:

template <typename TChild>
struct Base {

    // TChild should be complete at the moment of this declaration
    // template B should be declared at this moment.
    template <typename T>
    using Foo = typename TChild::template B<T>;
};

Instantiation of Base

struct Child : Base<Child>   
/*  TChild = Child at this moment is incomplete */
{
    template <typename T>
    using B = T;
    /* Point where template B begins to exist */
}; /* point where Child is complete */

Those rules are by design of language, and their goal is to avoid forcing compiler to make multiple passes forth and back through code, possibly in infinite recursion, to actually instantiate what did you mean. Weak-typed interpreter languages often don't have such problem as they can "correct" themselves later.

Case 1. The static function solution works for the reason that there is no type declaration was done. You had declared a template of function which actually has a global scope, but a concrete function or type aren't created yet.

struct Base {
    template <typename T>
    static auto constexpr getFoo() {
        return typename TChild::template B<T>{};
    }
};

struct Child : Base<Child> {
    template <typename T>
    using B = T;
    /* At this point we can instantiate Child::getFoo<int>()*/
}; /* Child is complete now */

At this point instantiation of Child::getFoo<T> is possible but all it requires is the return type of function.

using Bar = decltype(Child::getFoo<int>());

You can place this declaration into Child AFTER the declaration B, because B<int> would be complete at this point. You still can't declare it in Base

Case 2. Your solution declares another template Foo, it doesn't instantiate it within Base. This template isn't explicitly dependant on TChild but required prototype of foo() to exist at point of instantiation.

template <typename TChild>
struct Base {

    template <typename T>
    static auto foo(){
        return typename TChild::template B<T>();
    }

    // Foo is a template 
    template <typename T>
    using Foo = std::decay_t<decltype(foo<T>())>;
};

The instantiation happens at point where you would use Base::Foo<T>, which you actually didn't. That declaration is a no-op in your solution. It is legal to use it after the B declaration. You can't use it within Base or anywhere before B was declared.

Now what if you actually need to use an instance of B in Base? Here comes trait class solution:

Case 3. Traits can be templates specialized for child classes or concrete classes, that's a design choice. A trait serves as a base class for the CRTP base class and is a form of mixin. It's role is to provide useful declarations for CRTP. One of possible solutions for the most flexible trait naming:

template <typename TChild, template<class> typename Trait>
struct Base : public Trait<TChild> {

    // Trait<TChild>:: tells compiler that Foo is dependant on TChild 
    // and is declared in base class Trait. As compiler had reached this
    // point, the substitution was successful and thus Trait is complete
    using Foo = typename Trait<TChild>::template B<int>;

    // Foo is assumed to be a complete type, we can use it here!
    Foo make_foo() { return Foo{}; }
};

// Declaring trait template in this case.
template <typename T> struct ChildTrait;

// And specializing
template <>
struct ChildTrait<struct Child> {
    template <typename T>
    using B = T; 
};

struct Child : Base<Child,ChildTrait> {    
    using Bar = typename Base::Foo;
};

static_assert(std::is_same<Child::B<int>,int>::value,"");
static_assert(std::is_same<Child::B<std::string>,std::string>::value,"");

The idea here is that Trait<TChild> = ChildTrait<Child> has to be and can be a complete class within Base, or we could not derive Base from it. With slight modifications (eliding using, static_assert, typename use) this would compile in C++98 as it doesn't require decltype. This method used by some implementation of standard components, e.g. by std:: streams.

Traits may describe concrete storage types, allocators, etc. WHat's important is that resulting concrete types have no relation but have a shared interface declared in Base.

Barium answered 17/3, 2021 at 9:55 Comment(0)
C
0

Does this in any way do what you mean?

#include <type_traits>
template <typename TChild>
struct Base {
    template <typename T>
    static auto constexpr getFoo() {
        return typename TChild::template B<T>{};
    }
};

struct Child : Base<Child> {
    template <typename T>
    using B = T;
};


using Bar = decltype(Child::getFoo<int>());
static_assert(std::is_same_v<Bar, int>);

I've essentially replaced the template alias with a template static function which returns a default constructed object of the type you wanted the template alias to encode, so decltype-ing its result should give the type you want.

Charade answered 17/3, 2021 at 7:35 Comment(1)
close. I came up with a similar solution myself before I saw yours except mine ( see below ) keeps the form I wanted. Still +1Droopy

© 2022 - 2024 — McMap. All rights reserved.