Search code examples
c++templatesc++11abstract-classtype-traits

How to use type traits to define a partially abstract template base class?


I'm working on the following idea:

There exists a generally abstract templated base class with mutiple template parameters. This class defines a contract guaranteeing the presence of certain methods i.e. a method getMax(). This method among others is generally purely virtual. Except there are special cases in which a sensible implementation can be given without the need to implement it manually every time in a derived class. So basically what I'm trying to achive is to (partially) implement the contracted methods already inside the abstract base class if the template arguments allow this.

I made a small example to illustrate the idea. (Note: The example is not perfect, for instance the implementation for std::string is very special and already implicitly forces TSize to be std::size_t)

#ifndef ABSTRACTBASE_H
#define ABSTRACTBASE_H

#include <type_traits>
#include <string>
#include <limits>

template <typename TSize, typename TVal1, typename TVal2>
class AbstractBase
{
    public:
        AbstractBase(){};
        virtual ~AbstractBase() {};
        virtual TSize getMax() const = 0; // <-- getMax should be generally 
                                          //     purely virtual.

    private:
        TVal1 value1;
        TVal2 value2;
};

//except when TVal1 is an arithmetic type in that case the following definition 
//shall become active.
template <typename TSize, 
          typename TVal1, 
          typename TVal2, 
          typename = typename std::enable_if<std::is_arithmetic<TVal1>::value, TSize>::type>
TSize AbstractBase<TSize, TVal1, TVal2>::getMax() const
{
    return std::numeric_limits<TVal1>::max();
}

//... or when TVal1 is a string where this defintion makes sense
template <typename TSize, 
          typename TVal1, 
          typename TVal2, 
          typename = typename std::enable_if<std::is_same<TVal1, std::string>::value, TSize>::type>
TSize AbstractBase<TSize, TVal1, TVal2>::getMax() const
{
    return value1.max_size();
}

//... in all other cases the getMax() method shall stay purely virtual and an 
//appropriate definition must be implemented inside a derived class

#endif //ABSTRACTBASE_H


#include "AbstractBase.h"
#include <string>
#include <iostream>

int main()
{
    AbstractBase<int, int, int> arthBase();
    AbstractBase<std::size_t, std::string, long> alphaBase();

    std::cout << arthBase.getMax() << std::endl;
    std::cout << alphaBase.getMax() << std::endl;
}

So I guess what is missing here is a method to actually also alter the declaration ov getMax() as virtual, though im not really sure if/how this is possible using type_traits.

Side note: I haven't worked with type traits very much yet. I know about the SFINAE principle behind it, which basically states that if the substitution for a template parameter fails the following code will be excluded from compilation rather than causing an error. What I haven't found out if the type_trait argument responsible for enabling/disabling the method has to be merged into the class template argument list like I did above, or if it is legal/possible to provide the type trait arguments in a seperate template argument list. In this case I guess it is impossible because the enable_if statement tests the classes template arguments which have to be declared/valid in this context.

Just in case you use really sophisticated type trait magic a more elaborate comment on that part is greatly appreciated.


Solution

  • The annoying part here is that the string version needs to access a member variable. So one way around that is to put the members in the most base class:

    template <typename TVal1, typename TVal2>
    class Members {
    protected:
        TVal1 value1;
        TVal2 value2;
    };
    

    Just outsource getMax() to a different type:

    // pure case
    template <typename TSize, typename TVal1, typename TVal2>
    struct VirtualMax : Members<TVal1, TVal2> {
        virtual TSize getMax() const = 0;
    };
    
    // integer case
    template <typename TSize, typename TVal1, typename TVal2>
    struct IntMax : Members<TVal1, TVal2> {
        TVal1 getMax() const { return std::numeric_limits<TVal1>::max(); }
    };
    
    // string case
    template <typename TSize, typename TVal1, typename TVal2>
    struct StringMax : Members<TVal1, TVal2> {
        size_t getMax() const {
            return this->value1.max_size();
        }
    };
    

    So then we write a type trait:

    template <typename TSize, typename TVal1, typename TVal2>
    using Base_t = std::conditional_t<
        std::is_same<TVal1, std::string>::value,
        StringMax<TSize, TVal1, TVal2>,
        std::conditional_t<
            std::is_arithmetic<TVal1>::value,
            IntMax<TSize, TVal1, TVal2>,
            VirtualMax<TSize, TVal1, TVal2>
            >
        >;
    

    And then use that alias:

    template <typename TSize, typename TVal1, typename TVal2>
    class AbstractBase
    : Base_t<TSize, TVal1, TVal2>
    { };