What is C++ metafunction and how to use it?

Introduction

In C++ metaprogramming, a metafunction receives types and/or integral values, and after performing some logics returns types and/or integral values. Normal functions manipulate values, but the focus of a metafunction is types.

Definition

A metafunction is defined via struct. This is a simple metafunction which returns the input type and value:

template<typename T, int i> // T is input
struct GetMyInfo{
    using type = T;  // type is output type
    static constexpr int value = i; // value is output value
};

The content of <> is the metafunction parameters. type and value are the output.

Let’s employ the metafunction:

GetMyInfo<double, 2>::type x; // double x;

We defined a variable x with the output type of the metafunction which is double.

The output value can be used also:

std::cout<<GetMyInfo<double, 2>::value; // prints 2

Condition via template specialization

A condition is achieved via template specialization. If multiple struct templates accept some parameters, a compiler chooses the one that is more specific. Let’s define this pseudo-function:

// pseudo-code
f(bool cond){
    if cond==true return int
    if cond==false return double
}

The code is like this

template<bool cond>
struct GetType{
    using type = int;
};

template<>
struct GetType<false>{
    using type = double;
};

GetType<true>::type i; // int i;
GetType<false>::type d; // double d;
  • The first case is the default or generic case as it accepts both false and true.
  • The second case is specific, only accepts false.
  • Therefore, if we pass true, only the first case is matched and selected.
  • But passing false both cases are matched but the second case is selected because it is more specific.

Let’s write a metafunction that returns true if a type is double:

template<typename T>
struct IsDouble{
    static constexpr bool value = false;
};

template<>
struct IsDouble<double>{
    static constexpr bool value = true;
};

int main(){
    std::cout<<IsDouble<double>::value<<"\n"; // true
    std::cout<<IsDouble<int>::value<<"\n"; // false
    return 0;}

The same ideas as previous expample:

  • The first struct is the default case which accepts any type.
  • The second struct is the specific case which only accepts double. This will be selected for double because it is more specific.

True and false types

To avoid excessive writing, there are std::true_type and std::false_type that a struct can inherit from. Let’s write IsDouble<T> again:

template<typename T>
struct IsDouble : std::false_type{};

template<>
struct IsDouble<double>:std::true_type{};

int main(){
    std::cout<<IsDouble<double>::value<<"\n"; // true
}

Same types

Let’s write a metfunction that return true if two types are the same:

template<typename T, typename U>
struct AreSame: std::false_type{};

template<typename T>
struct AreSame<T,T>: std::true_type{};

int main(){
    std::cout<<AreSame<int,int>::value<<"\n"; //true
    std::cout<<AreSame<int,double>::value<<"\n"; // false
    return 0;
}

This metafunction is similar to std::is_same<T,U> in type_traits header. There are many useful traits in that header, check them before writing your metafunctions.

Is pointer

A metafunction that tells us if a type is a pointer and also returns the type of pointer’s target would be like:

template<typename T>
struct IsPointer{
    static constexpr bool value = false;
    using innerType = T;
};

template<typename T>
struct IsPointer<T*>{
    static constexpr bool value = true;
    using innerType = T;
};

int main(){
    std::cout<<IsPointer<int*>::value; // true
    std::cout<<IsPointer<int>::value; // false
    IsPointer<int*>::innerType x; // int x;
    IsPointer<int>::innerType y; // int y;
    return 0;
}

This is similar to std::is_pointer<T> from type_traits header.

Can you write a metafunction that tells if a type is reference?

More conditions

To this point, we focused on true/false cases, but template specialization can be expanded for many conditions. For example, let’s write a metafunction that has this pseudo-code

f(int i)
    if i==0 return bool
    if i==1 return int
    else return double

The metafunction is:

template<int i>
struct SelectType { using type = double;};

template<>
struct SelectType<0> { using type = bool;};

template<>
struct SelectType<1> { using type = int;};

SelectType<0>::type y; // bool y;
SelectType<1>::type z; // int z;
SelectType<10>::type x; // double x;

Extract inner-types

Using metafunctions, for a type U<T>, we can extract sub-type T. For example, let’s write a meta-function that extracts the inner type of std::vector<T>, T.

template<typename T>
struct GetVectorSubType{};

template<typename T>
struct GetVectorSubType<std::vector<T>>{
    using type = T;
};

GetVectorSubType<std::vector<int>>::type x; // int x;
GetVectorSubType<double>::type x; // Error...

Note that because the default case has no type definition, we get an error if any type other than std::vector<T> is passed.

Now let’s write another one that returns the size and inner type of std::array:

template<typename T>
struct GetArrayInfo{
    static constexpr bool isArray = false;
};

template<typename T, size_t n>
struct GetArrayInfo<std::array<T,n>>{
    using type = T;
    static constexpr size_t size = n;
    static constexpr bool isArray = true;
};

int main(){
    int i;
    std::array<double,3> arr;
    
    std::cout<<GetArrayInfo<decltype(i)>::isArray; // false
    
    std::cout<<GetArrayInfo<decltype(arr)>::isArray; // true
    std::cout<<GetArrayInfo<decltype(arr)>::size; //3 
    
    using ArrayType = GetArrayInfo<decltype(arr)>::type;
    std::cout<<std::is_same_v<ArrayType, double>; // true 
    return 0;
}

Integral constant

Integer values are accepted as template parameters but for a metafunction that accepts only types, like std::is_same<T,U> the integer values cannot be used. To overcome that we can define a type for integer values:

template<int i>
struct int_const{
    static constexpr int value = i;
};

using one_t = int_const<1>;
using two_t = int_const<2>;
using namespace std;
int main(){
    cout<<is_same_v<one_t,two_t>; // false
    cout<<is_same_v<one_t,one_t>; // true
    return 0;
}

This is very similar to std::integral_const. This type template is useful in tag-dispatch technique.

Constexpr for value calculation

Before constexpr of C++11, metafunctions were used for compile-time calculations. For example, Factorial function is found this way:

template<int i>
struct Factorial{
    static const int value = i * Factorial<i-1>::value;
};

template<>
struct Factorial<0>{
    static const int value = 1;
};

Since C++11, we have constexpr functions which are possible to be calculated at compile-time, see the factorial using constexpr:

constexpr int factorial(int n)
{
    return n <= 1 ? 1 : (n * factorial(n - 1));
}

This is way cleaner than using templates, therefore, in the new codes, where possible, use constexpr functions instead of templates for value calculations.

Constexpr for higher-level logic

Having some basic metafunctions (or type traits), we can use if-constexpr and constexpr functions instead of writing more metafunctions for complex logic.

For example let’s write a function that receives an object, and returns the size of it. I am using GetArrayInfo metafunction I defined previously:

template<typename T>
auto printSize(T t){
    if constexpr(GetArrayInfo<T>::isArray)
        std::cout<< t.size();
    else if constexpr(std::is_same_v<T,std::string>)
        std::cout<< 1;
    else 
        std::cout<<"Unknown type";
}

using namespace std;
int main(){
    int i;
    array<int,3> arr;
    printSize(i); // unknown type
    printSize(arr); // 3
    return 0;
}

Another example, let’s write a function that

if 0i<3
    returns int
if 3i<5
    returns float
if 3i<7 
    returns double
else
    shows error

The code will be like this:

template<int i>
auto f(){
    if constexpr (0<=i && i<3)
        return int{};
    else if constexpr (3<=i && i<5)
        return float{};
    else
        return double{};

}

using namespace std;
int main(){
    using outcome1_t = decltype(f<1>());
    using outcome4_t = decltype(f<4>());
    using outcome7_t = decltype(f<7>());

    cout<<boolalpha<<is_same_v<outcome1_t, int>;
    cout<<boolalpha<<is_same_v<outcome4_t, float>;
    cout<<boolalpha<<is_same_v<outcome7_t, double>;
    return 0;
}

Lambda

Also, it’s good to know that since C++20, a lambda can be used in metafunctions to set values and types:

template<typename T>
struct Get{
    static constexpr int byteSize = 0;
};
template<>
struct Get<int>{
    static constexpr int byteSize = [](){return 4;}();
    using type = decltype( [](){return int{};}() );
};

Subscribe

I notify you of my new posts

Latest Posts

Comments

0 comment