Search code examples
c++templatesinheritancespecializationpartial-specialization

c++ class template partial specialization without specializing all member functions


let's say I have a class X<'T, N'>, which has a few functions defined and I want also class X<'T, 3'> to have them all, but also a few additional functions (a real life example would be a Vector<'T, N'> and Vector<'T, 3'> would have all Vector<'T, N'> functions, but could also implement rotation in 3D space)

template <class T, size_t N>
class X {
public:
    void a();
    X<T, N> b(X<T, N> x);
    // etc
    static X<T, n> c();
protected: 
    T data[N];
};

1) approach by partial specialization

template <class T>
class X<T, 3> {
public:
    void d();
};

template <class T>
using X3 = X<T, 3>;

a) [Y] std::hash<'X3<'T'>'> is the same as std::hash<'X<'T, 3'>'> so if I overload hash for X<'T, N'>, X3<'T'> also will work
b) [Y] function d is accessible
c) [N] functions a, b, c are not accessible
d) [Y/N] X3<'T'> x = X3<'T'>::c(); would work if c was accesible

2) approach by inheritance

template <class T>
class X3 : public X<T, 3> {
public:
    void d();
};

a) [N] std::hash<'X3<'T'>'> is not the same as std::hash<'X<'T, 3'>'> so if I overload hash for X<'T, N'>, X3<'T'> won't apply
b) [Y] function d is accesible
c) [Y/N] functions a, b, c are accessible, but b and c have similar cast issues (detailed at below)
d) [N] X3<'T'> x = X3<'T'>::c(); won't work, even though is accesible

explanation:
compiler knows how to convert X3<'T'> to X<'T, 3'> (so passing X3<'T'> as argument to function b works)
compiler does not know how to convert X<'T, 3'> to X3<'T'> (so assigning function b and c result to X3<'T'> variable does not work)

(managed to bypass it by creating a move constructor 'X3(X<'T, 3'>&& x) noexcept', but std::hash problem remains in this case)

Is there any way to combine benefits of both methods?
(other than using approach 1) and rewriting all methods like that:)

//...

X3<T> b(X3<T> x) {
   return X<T, 3>::b(x);
}

//...  

(or approach 2) and strange casting, move/copy constructors, and having to specialize templates for things like std::hash a few times)


Solution

  • In my experience use inheritance method (2) when you actually want a new class that inherits from the template. Use method (1) when the partial specialization is sufficiently different from the general case or general case is not defined at all and you just declare a bunch of partial specializations to make use of SFINAE.

    If you simply want to enable/disable methods of a class based on template parameters then utilize SFINAE. As this way you can centralize all your definitions in one place and reason about them more easily. This is the preferred method unless you want user to make the template specializations on their own - which isn't desirable in general.

    Example of SFINAE:

    template <class T, size_t N>
    class X {
    public:
        void a() {...};
        X<T, N> b(X<T, N> x);
        // etc
        static X<T, n> c() {...};
    
        template<size_t uN = N, std::enable_if_t<uN==3,int>=0>
        void d() {...};
    protected: 
        T data[N];
    };
    

    Here, X<int,3> x; x.d(); will compile but X<int,2> x; x.d(); will not as d() method is disabled unless N==3.

    You can read a lot more about usages of SFINAE in template functions and classes in numerous online guides (note that the syntax is quite different for functions and classes).