What is C++ Metafunction and How to Use It?
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
andtrue
. - 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 0≤i<3
returns int
if 3≤i<5
returns float
if 3≤i<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{};}() );
};