20120202

More on Template Partial Specializations and Inheritance

Last time we talked about template partial specialization and some ways that it gets hung up. I posted a sort of solution which kind of sort of works, but in fact did not solve the problem I was having. I should have probably tried applying it to my code before posting it, huh? Such is the haste of impatient programmers.

The way I used the template last time was too simplistic. What I didn't do was include any class names in the template parameters, only values. It turns out this causes some interesting, unintuitive issues. Let's take a look at a code sample.

I apologize for the contrived, abstract example; I was having trouble coming up with anything good. We have a class called TimesN with a template parameter specifying what N is, which is baked into the class. It has a DoTimes function that takes the other number and does the multiplication. That other number can either be produced by some object or is simply a number itself, depending on the template parameter T.

#include <windows.h>
#include <stdio.h>

// Typically, T is a helper class that implements GetNum()
template <typename T, int N>
class TimesN
{
    public:
    virtual void DoTimes(T t);
};

class NumberWrapper
{
    public:
    NumberWrapper(int t) : t(t) { }
    NumberWrapper() : t(0) { } // aside: MSVC11 doesn't support peer ctors

    int GetNum() const { return t; }

    private:
    int t;
};

template <typename T, int N>
void TimesN<T, N>::DoTimes(T t)
{
    // just do the multiplication
    printf("t=%d, N=%d, t*N=%d\n", t.GetNum(), N, t.GetNum() * N);
}



// subclass and specialize for T=int, as shown in last blog post
template <int N>
class IntTimesN : public TimesN<int, N>
{
    public:
    void DoTimes(int t);
};

template <int N>
void IntTimesN<N>::DoTimes(int t)
{
    printf("t=%d, N=%d, t*N=%d\n", t, N, t * N);
}

int __cdecl wmain(int argc, wchar_t* argv[])
{
    TimesN<NumberWrapper, 3> x;
    x.DoTimes(NumberWrapper(6));

    IntTimesN<4> y;
    y.DoTimes(5);
    return 0;
}

This is exactly as prescribed in my last blog post, and that post worked fine, so this one should too, right? WRONG! When you compile this, you're greeted by:

main.cpp(28) : error C2228: left of '.GetNum' must have class/struct/union
        type is 'int'
        main.cpp(26) : while compiling class template member function 'void TimesN<T,N>::DoTimes(T)'
        with
        [
            T=int,
            N=4
        ]
        main.cpp(36) : see reference to class template instantiation 'TimesN<T,N>' being compiled
        with
        [
            T=int,
            N=4
        ]
        main.cpp(52) : see reference to class template instantiation 'IntTimesN<N>' being compiled
        with
        [
            N=4
        ]

If you read the error message carefully, you'll see it is trying to work out the implementation of IntTimesN::DoTimes. I suppose it makes sense, due to the inheritance we've put in place, that this would require instantiating the template for the superclass, TimesN<int>. And now the class TimesN<int> has to be compiled in full, irrespective of its use as a superclass, the same as if I'd simply declared an object of type TimesN<int>. And this requires compiling the DoTimes<int> method, which of course fails.

WHEW. Well, I was confused/annoyed by this before, but now it makes perfect sense to me. Writing helps solve problems, you guys!

Oh right, the solution? Yes yes, I worked around this. Thinking critically about what's going on, the problem is trying to compile the DoTimes<int> method, so the most direct route is to make sure this method does not have an implementation. That's right, we're going to switch up the inheritance. TimesN will become an abstract base class, and we'll have two child classes, ObjectTimesN<T, N> (which subsumes the former implementation from TimesN) and IntTimesN<N>, which stays the same.

The code, for your viewing pleasure:

#include <windows.h>
#include <stdio.h>

template <typename T, int N>
class TimesN
{
    public:
    virtual void DoTimes(T t) = 0;
};

template <typename T, int N>
class ObjectTimesN : public TimesN<T, N>
{
    public:
    void DoTimes(T t);
};

class NumberWrapper
{
    public:
    NumberWrapper(int t) : t(t) { }
    NumberWrapper() : t(0) { }

    int GetNum() const { return t; }

    private:
    int t;
};

template <typename T, int N>
void ObjectTimesN<T, N>::DoTimes(T t)
{
    printf("t=%d, N=%d, t*N=%d\n", t.GetNum(), N, t.GetNum() * N);
}



template <int N>
class IntTimesN : public TimesN<int, N>
{
    public:
    void DoTimes(int t);
};

template <int N>
void IntTimesN<N>::DoTimes(int t)
{
    printf("t=%d, N=%d, t*N=%d\n", t, N, t * N);
}

int __cdecl wmain(int argc, wchar_t* argv[])
{
    ObjectTimesN<NumberWrapper, 3> x;
    x.DoTimes(NumberWrapper(6));

    IntTimesN<4> y;
    y.DoTimes(5);
    return 0;
}

I've taken the liberty of highlighting the differences from the first code sample. I've actually incorporated this solution into my project and it is working just fine now. A lot of hair tearing, I tell ya what, but now me and template partial specializations are on good terms again.

0 comments:

Post a Comment