C++ static polymorphism (CRTP) and using typedefs from derived classes
Asked Answered
D

5

81

I read the Wikipedia article about the curiously recurring template pattern in C++ for doing static (read: compile-time) polymorphism. I wanted to generalize it so that I could change the return types of the functions based on the derived type. (This seems like it should be possible since the base type knows the derived type from the template parameter). Unfortunately, the following code won't compile using MSVC 2010 (I don't have easy access to gcc right now so I haven't tried it yet). Anyone know why?

template <typename derived_t>
class base {
public:
    typedef typename derived_t::value_type value_type;
    value_type foo() {
        return static_cast<derived_t*>(this)->foo();
    }
};

template <typename T>
class derived : public base<derived<T> > {
public:
    typedef T value_type;
    value_type foo() {
        return T(); //return some T object (assumes T is default constructable)
    }
};

int main() {
    derived<int> a;
}

BTW, I have a work-around using extra template parameters, but I don't like it---it will get very verbose when passing many types up the inheritance chain.

template <typename derived_t, typename value_type>
class base { ... };

template <typename T>
class derived : public base<derived<T>,T> { ... };

EDIT:

The error message that MSVC 2010 gives in this situation is error C2039: 'value_type' : is not a member of 'derived<T>'

g++ 4.1.2 (via codepad.org) says error: no type named 'value_type' in 'class derived<int>'

Dejadeject answered 15/5, 2011 at 5:9 Comment(4)
Just so you know, codepad.org can compile and run code for you, and I believe it uses gcc/g++. So you're never out of reach of g++ :)Lunatic
could you add what error you are getting so that i will be useful for the readers.Berardo
@Seth: Ideone uses gcc for sure, so it's yet another one :)Tenaculum
@Seth: thanks for the tip about codepad.org! @Sriram: Good call. I added them.Dejadeject
R
80

derived is incomplete when you use it as a template argument to base in its base classes list.

A common workaround is to use a traits class template. Here's your example, traitsified. This shows how you can use both types and functions from the derived class through the traits.

// Declare a base_traits traits class template:
template <typename derived_t> 
struct base_traits;

// Define the base class that uses the traits:
template <typename derived_t> 
struct base { 
    typedef typename base_traits<derived_t>::value_type value_type;
    value_type base_foo() {
        return base_traits<derived_t>::call_foo(static_cast<derived_t*>(this));
    }
};

// Define the derived class; it can use the traits too:
template <typename T>
struct derived : base<derived<T> > { 
    typedef typename base_traits<derived>::value_type value_type;

    value_type derived_foo() { 
        return value_type(); 
    }
};

// Declare and define a base_traits specialization for derived:
template <typename T> 
struct base_traits<derived<T> > {
    typedef T value_type;

    static value_type call_foo(derived<T>* x) { 
        return x->derived_foo(); 
    }
};

You just need to specialize base_traits for any types that you use for the template argument derived_t of base and make sure that each specialization provides all of the members that base requires.

Ringtailed answered 15/5, 2011 at 5:16 Comment(6)
I created one test file and compiled code of OP with putting a main() with Ubuntu g++ 4.4.1 It works fine. Declaring an object like Derived<int> gives error, I am trying to figure out why ?Trudeau
@Trudeau That's because in an empty main() no template instantiation takes place. Certain errors only come up when the compiler tries to instantiate and use the template.Helpmate
Right; you have to instantiate the template for there to be any errors here; if you add int main() { derived<int>().base_foo(); } to the bottom of my traits example (this forces instantiation of everything), it should compile with Visual C++ 2010, g++ 4.5.1, and the latest Clang builds.Ringtailed
@both, agreed. However, I would prefer to switch the arguments and make derived a default argument (this approach will not demand much code). I have posted in my answer.Trudeau
could you explain more about what you mean by this "derived is incomplete when you use it as a template argument to base in its base classes list". what way is it incomplete?Berardo
@JamesMcNellis and Sriram, I sense a confusion is possible. Slightly flattened version with derived not being a template: struct derived : base<derived> {...}; does make the base_traits not defined.Circosta
C
13

One small drawback of using traits is that you have to declare one for each derived class. You can write a less verbose and redondant workaround like this :

template <template <typename> class Derived, typename T>
class base {
public:
    typedef T value_type;
    value_type foo() {
        return static_cast<Derived<T>*>(this)->foo();
    }
};

template <typename T>
class Derived : public base<Derived, T> {
public:
    typedef T value_type;
    value_type foo() {
        return T(); //return some T object (assumes T is default constructable)
    }
};

int main() {
    Derived<int> a;
}
Cryotherapy answered 9/1, 2015 at 14:24 Comment(6)
what is the trait here?Achaea
This looks interesting, but I don't really understand it, could you explain it a bit more?Harem
@BenFarmer Instead of relying on the derived type to extract the value_type in the base, we directly pass it to the base class. The base class now has two template parameters. This is very practical if you have only or two types to pass, but if you have, this is not convenient.Coplanar
@BaptisteWicht Indeed, you could use variadic templates and tuples for a bit more flexibility.Cryotherapy
It is not redundant as you don't pass the type of the derived class but its template as template argument of the base class. (Note one can remove the typedef in the derived class)Cryotherapy
@Cryotherapy Yeah, I deleted my comment as I realised this wasn't doing what I thought. And being a template, in approximately 3 hours I'll have figured out what it is doing. ;-)Parton
P
12

In C++14 you could remove the typedef and use function auto return type deduction:

template <typename derived_t>
class base {
public:
    auto foo() {
        return static_cast<derived_t*>(this)->foo();
    }
};

This works because the deduction of the return type of base::foo is delayed until derived_t is complete.

Penetralia answered 12/8, 2016 at 13:30 Comment(1)
Does this technique limits to return value? Does it work on a field, or function parameter? Will C++17 help even more?Jac
H
3

An alternative to type traits that requires less boilerplate is to nest your derived class inside a wrapper class that holds your typedefs (or using's) and pass the wrapper as a template argument to your base class.

template <typename Outer>
struct base {
    using derived = typename Outer::derived;
    using value_type = typename Outer::value_type;
    value_type base_func(int x) {
        return static_cast<derived *>(this)->derived_func(x); 
    }
};

// outer holds our typedefs, derived does the rest
template <typename T>
struct outer {
    using value_type = T;
    struct derived : public base<outer> { // outer is now complete
        value_type derived_func(int x) { return 5 * x; }
    };
};

// If you want you can give it a better name
template <typename T>
using NicerName = typename outer<T>::derived;

int main() {
    NicerName<long long> obj;
    return obj.base_func(5);
}
Hyperbola answered 13/11, 2017 at 12:23 Comment(2)
Clever, but this has side effects. For example, T will not be able to be deduced in function tamplates, for example template<class T> f(outer<T>::derived x) (as opossed to template<class T> f(derived2<T> x).Rail
You can write template <class C, class T = typename C::value_type> auto f(C x). If you're worried about accepting arbitrary types you can add class = std::enable_if_t< std::is_same_v< C, NicerName<T> >, int> to the template parameters list but it gets quite long.Hyperbola
R
0

I know that this is basically the workaround you found and don't like, but I wanted to document it and also to say that it is basically the current solution to this problem.

I have been looking for a way to do this for a while and never found a good solution. The fact that it is not possible is the reason why ultimately, things like boost::iterator_facade<Self, different_type, value_type, ...> need many parameters.

Of course we would like something something like this to work:

template<class CRTP> 
struct incrementable{
    void operator++(){static_cast<CRTP&>(*this).increment();}
    using ptr_type = typename CRTP::value_type*; // doesn't work, A is incomplete
};

template<class T>
struct A : incrementable<A<T>>{
    void increment(){}
    using value_type = T;
    value_type f() const{return value_type{};}
};

int main(){A<double> a; ++a;}

If this was possible, all the traits of the derived class could be passed implicitly ot the base class. The idiom I found to get the same effect is to pass the traits to the base class entirely.

template<class CRTP, class ValueType> 
struct incrementable{
    void operator++(){static_cast<CRTP&>(*this).increment();}
    using value_type = ValueType;
    using ptr_type = value_type*;
};

template<class T>
struct A : incrementable<A<T>, T>{
    void increment(){}
    typename A::value_type f() const{return typename A::value_type{};}
//    using value_type = typename A::value_type;
//    value_type f() const{return value_type{};}
};

int main(){A<double> a; ++a;}

https://godbolt.org/z/2G4w7d

The drawback is that the trait in the derived class has to be accessed with a qualified typename or reenabled by using.

Rail answered 16/1, 2019 at 7:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.