Categories of Template Specializations

While templates are not near so uncaring as macros, there are still some things they just won't listen to. In specific: Inheritance.

Here's the setup. You have a template that exists primarily to be specialized1, and you already have several types for which you've specialized. Now, you want to make it work with a new group of types that all need to be treated the same way. Pondering the teachings of OO design, you recall that inheritance is the way to say that a group of types share something in common. Let's do a test, then.

Let's say that Other represents the existing type or types for which you template is already doing what it should, and you just want to preserve that. Let NewType1 and NewType2 be the types you want to add support for, which share some fundamental similarity and so should be treated alike by the template, but differently than Other. As suggested before, you try to express the similarity between the 'NewTypes' by making them both inherit from a common base, NewBaseType. Having done so, you write a specialization of your template to handle NewBaseType and (you hope) subclasses thereof. It might look something like this:

namespace test1{
    class NewBaseType{
    public:
        virtual std::string getString() const{
            return("NewBaseType!");
        }
    };

    class NewType1 : public NewBaseType{
    public:
        virtual std::string getString() const{
            return("NewType1!");
        }
    };

    class NewType2 : public NewBaseType{
    public:
        virtual std::string getString() const{
            return("NewType2!");
        }
    };

    class Other{
    public:
        std::string getString() const{ return("Other!"); }
    };

    template <class T>
    class Printer{
    private:
        T my_t;
    public:
        Printer<T>(const T& t):my_t(t){}
        void print() const{
            std::cout << "T says: " 
                << my_t.getString() << std::endl;
        }
    };

    template<>
    class Printer<NewBaseType>{
    private:
        NewBaseType my_base;
    public:
        Printer<NewBaseType>(const NewBaseType& base):
            my_base(base){}
        void print() const{
            std::cout << "A subclass of NewBaseType says: " 
            << my_base.getString() << std::endl;
        }
    };

    void doTest(){
        std::cout << "Test #1:" << std::endl;
        Printer<Other>(Other()).print();
        Printer<NewBaseType>(NewBaseType()).print();
        Printer<NewType1>(NewType1()).print();
        Printer<NewType2>(NewType2()).print();
    }
} //namespace test1

Now, you try calling doTest() and running it, only to get this output:

Test #1:
T says: Other!
A subclass of NewBaseType says: NewBaseType!
T says: NewType1!
T says: NewType2!

That's not what we wanted to see; the template is treating both NewType1 and NewType2 as though they were any old T. This is a bad sign, and points to the fact that the template is uninterested in details of ancestry (apparently templates are very egalitarian). As an attempt to salvage matters you might try:

    void doTest(){
        std::cout << "Test #1:" << std::endl;
        Printer<Other>(Other()).print();
        Printer<NewBaseType>(NewBaseType()).print();
        Printer<NewBaseType>(NewType1()).print();
        Printer<NewBaseType>(NewType2()).print();
    }

But it's not going to work:

Test #1:
T says: Other!
A subclass of NewBaseType says: NewBaseType!
A subclass of NewBaseType says: NewBaseType!
A subclass of NewBaseType says: NewBaseType!

Sure, you've picked out the specialization you wanted, but you're also slicing; when you say Printer<NewBaseType>, that's exactly what you get, with no dynamic binding fanciness. Back to the drawing board, then.


Here's a rather different approach. What you really wanted to do wasn't to tell the compiler "NewType1 and NewType2 are very similar, they're both NewBaseTypes, and I want you to treat NewBaseTypes like so", it was to tell the compiler "Here's is a type, nevermind where I got it, and I want you to treat it like so". The point isn't so much the similarity of the new types, but that you want to be able to tell the compiler, via the template, to choose different behavior at certain times. So what about writing something like this:

namespace test2{

    class NewType1{
    public:
        std::string getString() const{ return("NewType1!"); }
    };

    class NewType2{
    public:
        std::string getString() const{ return("NewType2!"); }
    };

    class Other{
    public:
        std::string getString() const{ return("Other!"); }
    };

    template <class T>
    class Wrapper{};

    template <class T>
    class Printer{
    private:
        T my_t;
    public:
        Printer<T>(const T& t):my_t(t){}
        void print() const{
            std::cout << "T says: " 
                << my_t.getString() << std::endl;
        }
    };

    template<class T>
    class Printer< Wrapper<T> >{
    private:
        T my_t;
    public:
        Printer< Wrapper<T> >(const T& t):my_t(t){}
        void print() const{ 
            std::cout << "The T in the Wrapper says: " 
                << my_t.getString() << std::endl;
        }
    };

    void doTest(){
        std::cout << "Test #2:" << std::endl;
        Printer<Other>(Other()).print();
        Printer< Wrapper<NewType1> >(NewType1()).print();
        Printer< Wrapper<NewType2> >(NewType2()).print();
    }
} //namesapce test2

First off, the code is a lot simpler. We don't actually need an inheritance hierarchy at all, although one could be present if needed for other reasons. We also do away with a need for dynamic binding via the use of virtual member functions; since the template only works directly on the type it was told to deal with virtual function were useless anyway, as we saw before. Instead, we use the Wrapper type as a tag that says to the template "here's a type, but try to handle it with that specific behavior". In fact the Wrapper itself is utterly empty, like the swipe of color left by a highlighter—its presence alone does the whole job. Here's what test2::doTest() produces:

Test #2:
T says: Other!
The T in the Wrapper says: NewType1!
The T in the Wrapper says: NewType2!

The advantages? It's easy, and it's correct, doing what we'd originally intended. The (possible) disadvantages? The new behavior we added to the template isn't restricted to the types that we intended it for. I can just as well instantiate a Printer< Wrapper<Other> > or a Printer< Wrapper<MyCrazyDifferentType> > and get the behavior that was designed for the NewTypes. Maybe that's a problem, maybe not. On the one hand it's more flexible. On the other, it does allow something we might not have wanted to allow, but perhaps that can be rebutted with "It hurts when you do that? Then don't do that!"


  1. Because templates are how you ask a C++ compiler to write code for you, as mentioned before, and specializations are how you have it choose bits of code that can do completely different things if necessary. 

No Comments

Comment on this post