Wednesday 27 April 2011

Templates, inheritance and nested classes

I've recently had a world of fun and confusion with a strange combination of templating, inheritance and nested class in c++. The error messages along the way were rather weird, so in a bid to save time in future, here's the story:

Let's begin with a pretty standard templated class. It does useful things to some data which it keeps a hold of during its lifetime.
template <class T> class Foo
{
  public:
    Foo()  { /*...*/ }
    ~Foo() { /*...*/ }
    void SetStuff(T *data) { /*...*/ }
    void DoStuff(T *data) { /*...*/ }
    T *ViewStuff() { /*...*/ }
  private: 
    T *data;
}
OK, now let's assume that we have a set of helper functions that are only useful with the data in Foo while it's in Foo, and which should never be used externally but are always used whenever Foo wants to do stuff with the data. So we'll define a little nested helper class and squirrel the data away in it:
template <class T> class Foo
{
  public:
    Foo()  { /*...*/ }
    ~Foo() { /*...*/ }
    void SetStuff(T *data) { /*...*/ }
    void DoStuff(T *data)  { /*...*/ }
    T *ViewStuff() { /*...*/ }
  private:
    class DataWrapper
    {
      public:
        DataWrapper(T *data) { /*...*/ }
        ~DataWrapper() { /*...*/ }
        void HelperFunction() { /*...*/ }
        T *GetData() { /*...*/ }
      private:
        T *data;
    }
    DataWrapper *data;
}
Note that DataWrapper doesn't need to be templated; that's taken care of because it's already nested inside a templated class. Now let's imagine that SetStuff and DoStuff aren't safe methods to call all the time, and that sometimes you need to make sure that you can only use the safe methods, like viewing stuff. We just need a FooView. Fine, let's make a base class of Foo that has a copy constructor and only provides the safe methods. Then we can just take a FooView copy of Foo which we know is safe to hand over for use elsewhere. We want FooView to be the base of Foo, which is possibly slightly counterintuitive, because Foo does everything that FooView does and more besides.

template <class T> class FooView
{
  public:
    //Copy constructor to allow us to create views of a Foo
    FooView(FooView & source) { /*...*/ }
    ~FooView() { /*...*/ }
    T *ViewStuff() { /*...*/ }
  protected:
    //It doesn't make sense to create a FooView unless we give it something to 
    //copy. The descended Foo class will need a default constructor, though.
    FooView() { /*...*/ }
    class DataWrapper
    {
      public:
        DataWrapper(T *data) { /*...*/ }
        ~DataWrapper() { /*...*/ }
        void HelperFunction() { /*...*/ }
        T *GetData() { /*...*/ }
      private:
        T *data;
    }
    DataWrapper *data;

}
template <class T> class Foo : public FooView <T>
{
  public:
    Foo() { /*...*/ }
    ~Foo() { /*...*/ }
    void SetStuff(T *data) { /*...*/ }
    void DoStuff(T *data) { /*...*/ }
}

DataWrapper needs to be in the FooView class since both Foo and FooView need to understand it, but nothing else should (perhaps it's code that would be unsafe if used outside a FooView object). So far, so simple. But as soon as we try to implement anything in Foo that uses a DataWrapper, we end up in a world of pain.

Our first problem is that inside Foo, the compiler doesn't understand what a DataWrapper is. Let's add an implementation to SetStuff:
template <class T> class Foo : public FooView <T>
{
  public:
    Foo() { /*...*/ }
    ~Foo() { /*...*/ }
    void SetStuff(T * data)
    {
      DataWrapper *wrapper;
      wrapper = new DataWrapper(data);
      this->data = wrapper;
    }
    void DoStuff(T *data) { /*...*/ }
}

...which will result in the compiler error "DataWrapper was not declared in this scope". The issue is that we're in a templated class and the compiler doesn't know where to look for the definition of DataWrapper (which is also in a templated class). So let's give it a hint:

template <class T> class Foo : public FooView <T>
{
  public:
    Foo() { /*...*/ }
    ~Foo() { /*...*/ }
    void SetStuff(T *data)
    {
      FooView<T>::DataWrapper *wrapper;
      wrapper = new FooView<T>::DataWrapper(data);
      this->data = wrapper;
    }
    void DoStuff(T *data) { /*...*/ }
}

Which is pretty ugly. And leads to the baffling error "wrapper was not declared in this scope".

Huh?

But... but... that IS the declaration of wrapper!

Somehow the compiler has become so confused by the templating and whatnot that it's totally failed to parse
FooView<T>::DataWrapper *wrapper;
as a declaration. It's trying to perform some operation on wrapper as if it were already declared. I've no idea what. If you switch T for a type, the problem goes away -
FooView<double>::DataWrapper *wrapper;
works fine, apart from being a bit totally useless. So it seems that the compiler has missed the fact that
FooView<T>::DataWrapper
is a type name. Fortunately there's a keyword available to convince the compiler that no, honestly, it is a type name:

template <class T> class Foo : public FooView <T>
{
  public:
    Foo() { /*...*/ }
    ~Foo() { /*...*/ }
    void SetStuff(T *data)
    {
      typename FooView<T>::DataWrapper *wrapper;
      wrapper = new typename FooView<T>::DataWrapper(data);
      this->data = wrapper;
    }
    void DoStuff(T *data) { /*...*/ }
}

This is clearly absurd. If you're using DataWrappers in many places, your code is going to end up looking like it's been beaten to death with an ugly stick. Fortunately we can avoid all that nonsense by using a private typedef inside Foo:

template <class T> class Foo : public FooView <T>
{
  private:
    typedef typename FooView<T>::DataWrapper DataWrapper
  public:
    Foo() { /*...*/ }
    ~Foo() { /*...*/ }
    void SetStuff(T *data)
    {
      DataWrapper *wrapper;
      wrapper = new DataWrapper(data);
      this->data = wrapper;
    }
    void DoStuff(T *data) { /*...*/ }
}

And now you can just use DataWrapper inside Foo the same way as you would inside FooView - and as you probably expected to be able to use it in the first place. Obvious, eh?

EDIT - The typename keyword exists specifically to address this sort of situation, according to the description here. I guess it's one of those things that you either know (and hence will understand why you get an error about an undeclared variable from a line which you think is a declaration) or you don't and will be totally baffled by. It's not an error message that helps you towards an understanding of the source of the problem, that's for sure!

No comments: