简体   繁体   中英

Inheriting from a template class using the inheriting class

When I inherit from a class the compiler has to know the definition of the base class in order to create it. But when I inherit from a template class using oneself (the inheriting class), how can the compiler create the code? It does not know the size of the class yet.

#include <iostream>

template <class T> class IFoo
{
public:
     virtual T addX(T foo, double val) = 0;
     // T memberVar; // uncomment for error
};

class Foo : public IFoo<Foo>
{
public:
    Foo(double value)
        : m_value(value) {}

    Foo addX(Foo foo, double b) override 
    { 
        return Foo(foo.m_value + b); 
    }

    double m_value;
};

int main()
{
    Foo foo1(1);
    Foo foo2 = foo1.addX(foo1, 1);

    std::cout << foo2.m_value;
}

First I thought it works because it's an interface but it also works with a regular class.

When I store the template as a member i get an error that Foo is undefined, just as I expected.

With this definition of template class IFoo , the compiler does not need to know the size of Foo to lay out IFoo<Foo> .

Foo will be an incomplete class in this context (not "undefined" or "undeclared") and usable in ways that any incomplete type can be used. Appearing in a member function parameter list is fine. Declaring a member variable as Foo* is fine. Declaring a member variable as Foo is forbidden (complete type required).

The general concept here is called the Curiously Recurring Template Pattern or CRTP . Searching on that will get lots of hits. see: https://stackoverflow.com/questions/tagged/crtp .

However there is a simple explanation that likely answers your question without getting too much into CRTP. The following is allowed in C and C++:

struct foo {
    struct foo *next;
    ...
};

or with two types:

struct foo;
struct bar;

struct foo {
    struct bar *first;
    ...
};

struct bar {
    struct foo *second;
    ...
};

So long as only a pointer to a struct or class is used, a complete definition of the type doesn't have to be available. One can layer templates on top of this in a wide variety of ways and one must be clear to reason separately about the type parameterizing the template and its use within the template. Adding in SFINAE (Substitution Failure Is Not An Error), one can even make templates that do no get instantiated because things cannot be done with a given type.

how can the compiler create the code?

Answering this question would be the same as answering this question: How can the compiler compile that ?

struct Type;
Type func(Type);

Live example

How can you define a type that doesn't exist and yet declare a function that use that type?

The answer is simple: There is no code to compile with that actually use that non-existing type. Since there is no code to compile, how can it even fail?

Now maybe you're wondering what is has to do with your code? How does it make that a class can send itself as template parameter to it's parent?

Let's analyze what the compiler see when you're doing that:

struct Foo : IFoo<Foo> { /* ... */ };

First, the compile sees this:

struct Foo ...

The compiler now knows that Foo exists, yet it's an incomplete type.

Now, he sees that:

... : IFoo<Foo> ...

It knows what IFoo is, and it knows that Foo is a type. The compiler now only have to instanciate IFoo with that type:

template <class T> struct IFoo
{
     virtual T addX(T foo, double val) = 0;
};

So really, it declares a class, with the declaration of a function in it. You saw above that declaring a function with an incomplete type works. The same happens here. At that point, Your code is possible as this code is:

struct Foo;
template struct IFoo<Foo>; // instanciate IFoo with Foo

So really there's no sorcery there.


Now let's have a more convincing example. What about that?

template<typename T>
struct IFoo {
    void stuff(T f) {
        f.something();
    }
};

struct Foo : IFoo<Foo> {
    void something() {}
};

How can the compiler call something on an incomplete type?

The thing is: it don't. Foo is complete when we use something . This is because template function are instantiated only when they are used.

Remember we can separate functions definition even with template?

template<typename T>
struct IFoo {
    void stuff(T f);
};

template<typename T>
void IFoo<T>::stuff(T f) {
    f.something();
}

struct Foo : IFoo<Foo> {
    void something() {}
};

Great! Does it start looking exactly the same as your example with the pure virtual function? Let's make another valid transformation:

template<typename T>
struct IFoo {
    void stuff(T f);
};

struct Foo : IFoo<Foo> {
    void something() {}
};

// Later...
template<typename T>
void IFoo<T>::stuff(T f) {
    f.something();
}

Done! We defined the function later, after Foo is complete. And this is exaclty what happens: The compiler will instanciate IFoo<Foo>::stuff only when used. And the point where it's used, Foo is complete. No magic there either.


Why can't you declare a T member variable inside IFoo then?

Simple, for the same reason why this code won't compile:

struct Bar;
Bar myBar;

It doesn't make sense declaring a variable of an incomplete type.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM