Static interface checking

Static Interface Checking - also called Concept Cheking - is the effort to make early detections of template parameter problems. There are many advantages to catch template errors as soon as possible; like to avoid the problem of never instatiated templates, and unreadable error messages.

Interface Checking

Let suppose we want to check, wether the template parameter T has a Clone function implemented.

This is what C++ has not know:


interface Cloneable
{
    T* T::Clone() const;
}
Template <typename T>
class C
{
    // T must provide T* T::Clone() const
    ...
}

template <typename T>
class C
{
public:
    void SomeFunc( const T* t)
    {
        // ...
        t->Clone();
        // ...
    }
};

There are a number of problems with the code above:

  • if SomeFunc never used, than no error message

  • there might be a different parameter list (with default params)

  • nothing is known on return type

We need a place, wich always instantiated, and the Clone function is checked.


template <class T>
class C
{
public:
    ~C()
    {
        T* (T::*test)() const = &T::Clone;
        test;       // surpress warnings about unused variable
    }
};

Since a destructor always instantiated (except of using new operator without appropriate delete), the test pointer instantiated and checked by type. This process involves the existence checking of the Clone function with the accurate parameters.

The problem is that the destructor - a highly important function - is polluted by the checking criterias. It is better to concentrate that code into one place: validateRequirements.


template <class T>
class C
{
public:
    bool validateRequirements() const
    {
        T* (T::*test)() const = &T::Clone;
        test;       // surpress warnings about unused variable
        // .. other reqs.
        return true;
    }
    ~C()
    {
        // will disappear in release code
        assert( validateRequirements() );
    }
}

The correct separation of Concept Checking code is bringing it into a separate class (originally proposed by Alex Stepanov and Jeremy Siek):


//  4th attempt (Alex Stepanov and Jeremy Siek)
template <class T>
class HasClone
{
public:
    static void Constraints()
    {
        T* (T::*test)() const = &T::Clone;
        test;
    }
    HasClone() { void (*p)() = Constraints; }
};

template <class T>
class C : HasClone<T>
{
    // ...
};

Promotion

We discussed earlier, that one can not define a template max function returning the type of the greatest parameter value. However we can define rules encountered in compilation time to compute the return type.


#include <iostream>

template <class T1, class T2>
struct promote_trait  { };

#define DECLARE_PROMOTE(A,B,C)                  \
    template <> struct promote_trait<A,B>       \
    {                                           \
        typedef C T_promote;                    \
    };

DECLARE_PROMOTE(int,char,int);
DECLARE_PROMOTE(int,double,double);
DECLARE_PROMOTE(double,char,double);
DECLARE_PROMOTE(char,double,double);
//
// etc...
//

template <class T, class S>
typename promote_trait<T,S>::T_promote max ( T x, S y)
{
    if ( x > y )
        return x;
    else
        return y;
}

int main()
{
    int    i = 3;
    double d = 3.14;

    std::cout << max(i,d) << std::endl;
    return 0;
}

Here is a more detailed version:


#include <iostream>

template <class T>
struct precision_trait
{
    enum
    {
        precisionRank = 0,
        knowPrecisionRank = 0
    };
};

#define DECLARE_PRECISION( T, Rank)                 \
    template <>                                     \
    struct precision_trait<T>                       \
    {                                               \
        enum                                        \
        {                                           \
            precisionRank = Rank,                   \
            knowPrecisionRank = 1                   \
        };                                          \
    };

DECLARE_PRECISION(int,100)
DECLARE_PRECISION(unsigned int,200)
DECLARE_PRECISION(long,300)
DECLARE_PRECISION(unsigned long, 400)
DECLARE_PRECISION(float,500)
DECLARE_PRECISION(double,600)
DECLARE_PRECISION(long double,700)
//
// etc...
//

template <class T>
struct autopromote_trait
{
    typedef T T_numtype;
};
#define DECLARE_AUTOPROMOTE( T1, T2)                \
    template <>                                     \
    struct autopromote_trait<T1>                    \
    {                                               \
        typedef T2 T_numtype;                       \
    };

//
//  these are the automatic promotions to int type
//
DECLARE_AUTOPROMOTE( bool, int);
DECLARE_AUTOPROMOTE( char, int);
DECLARE_AUTOPROMOTE( unsigned char, int);
DECLARE_AUTOPROMOTE( short int, int);
DECLARE_AUTOPROMOTE( short unsigned int, unsigned int);

template <class T1, class T2, int promoteToT1>
struct promote2
{
    typedef T1 T_promote;
};
template <class T1, class T2>
struct promote2< T1, T2, 0>
{
    typedef T2 T_promote;
};
template <class T1_orig, class T2_orig>
struct promote_trait
{
    // handle promotion of small int/unsigned int
    typedef typename autopromote_trait<T1_orig>::T_numtype  T1;
    typedef typename autopromote_trait<T2_orig>::T_numtype  T2;

    enum
    {
        // true if T1 is higher ranked
        T1isBetter =
            precision_trait<T1>::precisionRank >
            precision_trait<T2>::precisionRank,

        // true if we know ranks for both T1 and T2
        knowBothRanks =
            precision_trait<T1>::knowPrecisionRank &&
            precision_trait<T2>::knowPrecisionRank,

        // true if we know T1 but not T2
        knowT1butNotT2 =
            precision_trait<T1>::knowPrecisionRank &&
            ! precision_trait<T2>::knowPrecisionRank,

        // true if we know T2 but not T1
        knowT2butNotT1 =
            ! precision_trait<T1>::knowPrecisionRank &&
            precision_trait<T2>::knowPrecisionRank,

        // true if T1 bigger than T2
        T1isLarger = sizeof(T1) > sizeof(T2),

        // we know T1 but not T2: true
        // we know T2 but not T1: false
        // otherwise, if T1 is bigger than T2: true
        defaultPromotion = knowT1butNotT2 ? false :
            ( knowT2butNotT1 ? true : T1isLarger)
    };

    // If we have both ranks, use them
    // If we have only one rank, use the unknown type  
    // If we have neither rank, than promote to the larger type
    enum
    {
        promoteToT1 = (knowBothRanks ? T1isBetter : defaultPromotion) ? 1 : 0
    };

    typedef typename promote2<T1,T2,promoteToT1>::T_promote T_promote;
};
template <class T, class S>
typename promote_trait<T,S>::T_promote max( T x, S y)
{
    if ( x > y )
        return x;
    else
        return y;
}

int main()
{
    int    i = 3;
    double d = 3.14;

    std::cout << max(i,d) << std::endl;
    return 0;
}

Conversion Checking

Most fundamental check is whether a type could be converted to an other.


template <class T, class U>
class Conversion
{
public:
    enum
    {
        exists = sizeof(Test(MakeT()))==sizeof(Small)
    };
    enum
    {
        sameType = false;
    };
private:
    typedef char Small;
    class Big { char dummy[2]; };

    static Small Test(U);
    static Big   Test(...);
    static T     MakeT();
};

typename <class T>
class Conversion<T,T>
{
public:
    enum { exists = 1, sameType = 1 };
};

int main()
{
    std::cout   << Conversion<double, int>::exists <<
                << Conversion<char, char*>::exists <<
                << Conversion<std::vector<int>,std::list<int> >::exists
                << std::endl;
    return 0;
}

Inheritance

The next step is to chech inheritance:


#define SUPERSUBCLASS(T,U)                              \
    ((Conversion<const U*, const T*>::exists  &&        \
    ! (Conversion<const T*, const void*>::sameType))

There is only 3 cases, when const U* converts implicitelly to const T* :

  • T is the same type as U

  • T is an unambigous public base of U

  • T is void


#define SUPERSUBCLASS_STRICT(T,U)                   \
    ( SUPERSUBCLASS(T,U)        &&                  \
    ! Conversion<const T, const U>::sameType )

Compile-Time Assertion

Based on compile-time decision we can decide to abort the program. This is parallel to run-time assertions:


//  pure declaration
template <bool> class CompileTimeAssert;

//  specialization for true
template <> class CompileTimeAssert<true>  {};

The trick is, that only specialization is exist: the general case has only declaration. This declaration is neccessary, because otherwise the specialization would be invalid.


//  usage:
CompileTimeAssert< SUPERSUBCLASS( X, Y) >   MUST_BE_SUPERCLASS;