Pregunta

A coworker asked me today about code which looks somewhat like this:

#include <iostream>

template <class T>
class IBase {
public:
    virtual ~IBase() {}

public:
    virtual void foo() = 0;
};

template <class T>
class Base : public IBase<T> {
public:
    virtual void bar() {
        foo(); // compiler error
    }
};

class Derived : public Base<int> {
public:
    virtual void foo() {
        std::cout << "Hello World!\n";
    }
};

int main() {
    Derived d;
    d.bar();
}

At first he was getting a compiler error saying that "foo()" was not found. OK, so he tried to change it to IBase<T>::foo();. While that compiled, it resulting in a linker error. So immediately, I recalled that I've seen this type of problem before and suggesting that he write this->foo(); instead. Viola! problem solved!

Then he asked me why didn't plain foo(); work? Isn't this->x(); essentially the same as x();? Honestly, I have no idea, but he piqued my interest. So here we are:

In summary:

virtual void bar() {
    this->foo();       // works
    //IBase<T>::foo(); // linker error
    //foo();           // compiler error
}

The question is why is this-> required. And why won't the other options work?

¿Fue útil?

Solución

Because the base class member is a dependent name - its meaning depends on the template parameter, and so isn't known until the template is instantiated. The name isn't looked up in the generic IBase template, since that might be specialised to give it a different meaning before instantiation.

Qualifying it with IBase<T>:: calls the base-class function non-virtually; that's generally not what you want, especially if (as here) it's a pure virtual function with no implementation. Hence the linker error when you tried that.

Qualifying it with this-> tells the compiler that it's a member, and any further checking is deferred until the template is instantiated. The function is still called virtually.

Otros consejos

Imagine you are the compiler. You have just been reading through and compiling the code and now you have reached the bar function. In this function, you see that there is an attempt to do foo(). At this point, do you know what foo is? You don't. It could come from the base class, but you can't possibly know, because you don't know what T is yet. It's certainly possible that there might be a specialization of IBase for which foo is something else entirely.

When you stick this-> before the function call, it causes the compiler to treat it as a dependent name. That means the compiler will say "Okay, this depends on the type of this, which I do not know yet. I'll wait until later, when I know how the class is being instantiated, before I look for foo."

IBase<T>::foo(); gives a linker error because there simply is no definition for foo in IBase<T>.

#include <iostream>

template <class T>
class IBase {
public:
  virtual ~IBase() {}

public:
  virtual void foo() = 0;
};

int foo() { std::cout << "hello!\n"; }
template <class T>
class Base : public IBase<T> {
public:
  virtual void bar() {
    foo(); // which foo?!
  }
};
template <>
class IBase<int> {
public:
  virtual ~IBase() {}
//virtual void foo() = 0; -- no foo()!
};

class Derived : public Base<int> {
public:
  virtual void foo() {
    std::cout << "Hello World!\n";
  }
};

int main() {
  Derived d;
  d.bar();
}

The above illustrates why C++ does not allow members of dependent type parents to be implicitly found.

When you call foo() in Base, which foo() should be called? The one in IBase<T> or the free function foo()?

Either we put the decision off until later, or we go with the free function foo().

If we only go with the free function foo() if one is visible, then subtle changes in #include order can massively change what your program does. So if it should call the free function foo(), it must error if one is not found, or we are completely screwed.

If we defer the decision until later, it means less of the template can be parsed and understood until a later date. This moves more errors to the point of instantiation. It also results in some surprising behavior, like in the above case, where someone might think "I'm calling the method foo()", but actually ends up calling the free function foo() with no diagnostic.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top