Skip to content

Latest commit

 

History

History
410 lines (317 loc) · 14.6 KB

block-6b-metafunctions.md

File metadata and controls

410 lines (317 loc) · 14.6 KB

Metafunctions

Templates were originally meant to help us with making generic algorithms and containers. But soon people realized that the language feature is much more powerful. It's actually a separate programming language inside C++.

As a demonstration, let us consider this very simple factorial function.

int fact(int n) {
    if (n == 0)
        return 1;
    else
        return n * fact(n - 1);
}

It can be rewritten to calculate the factorial at compile time, with using class template instantiations. We use the following substitutions:

  • Functions will become classes.
  • Function arguments will become template arguments.
  • The return value will be stored in the static variable named value by convention, eg. fact(6) will be Fact<6>::value.
  • We can do loops and recursion by instantiating another class.
  • We can do conditionals with specializations.

The template version, the so-called metafunction will be:

template <int N>
class Fact {
    public:
        static constexpr int value = N * Fact<N - 1>::value;
};

template <>
class Fact<0> {
    public:
        static constexpr int value = 1;
};


int main() {
    std::cout << Fact<6>::value;
}

constexpr means "constant expression": constant in a sense that it is calculated at compile time.

Nowadays noone ever writes template metaprograms like this, maybe only as an exercise. Since C++11, we also have constexpr functions, which are even more powerful since C++14. A constexpr function's value is calculated at compile time, given that the arguments are known at compile time. So you can write something like this:

constexpr int fact(int n) {
    if (n == 0)
        return 1;
    else
        return n * fact(n - 1);
}

int myarr[fact(6)];     // ok, since it can be calculated at compile time

int main() {
    std::cout << fact(6) << std::endl;  // calculated at compile time
    
    int i;
    std::cin >> i;
    std::cout << fact(i) << std::endl;  // calculated at run time
}

Still these exercises help you with understanding the advanced topics of C++ templates.

Is it a class? – SFINAE

We'll discuss a metafunction which can tell whether a type is a class or it is a built-in type, eg.

int main() {
    std::cout << IsClass<std::ostream>::value << std::endl; // true, is a class
    std::cout << IsClass<double>::value << std::endl;       // false, not a class
}

This will also explain some details and tricks about C++ templates which are frequently used with template metaprograms. The implementation of the metafunction is this:

template <typename T>
class IsClass {
  private:
    template <typename U>
    static constexpr bool check(int U::* ptr) {
        return true;
    }
    
    template <typename U>
    static constexpr bool check(...) {
        return false;
    }
    
  public:
    static constexpr bool value = check<T>(0);
};

By following the specializations generated by the compiler we can figure out how it works. Let's try IsClass<std::ostream> first! With T = std::ostream, the static variable value is defined as:

    static constexpr bool value = check<std::ostream>(0);

To get its value, the check function template is to be called. However, check is actually an overload, there are two functions with the same name. So firstly, the compiler will check the header of the functions (the body is not important yet). The template argument is explicitly specified, so U = std::ostream is substituted in both headers:

    template <>
    static constexpr bool check<std::ostream>(int std::ostream::* ptr);
    
    template <>
    static constexpr bool check<std::ostream>(...);

Now the compiler has to select one of the two via the usual function overload resolution mechanism. The argument is 0. The value zero can be treated as a null pointer, so the first version could be called (an attribute member pointer can be null as well). The second version could also be called: ... here is the "variadic function of unknown types" language feature inherited from C (like in printf).

Which one is better? In this case, the compiler will prefer the better overload, with less conversions, which is in this case the first one. A member pointer is more concretized than an unknown type. It will return true, and yes: std::ostream is a class.

What about IsClass<double>? The static variable will be like this:

    static constexpr bool value = check<double>(0);

So we'll have to follow the overload resolutions for the check function again. Firstly, the headers:

    template <>
    static constexpr bool check<std::ostream>(int double::* ptr);
    
    template <>
    static constexpr bool check<std::ostream>(...);

A strange thing happens: we have a syntax error in the first function's header. There is no such thing as an int double::*, doubles do not have attributes, so they can't have member pointers. However, the compilation is not stopped; the compiler will throw away the first function, in the hope that some other overload will work. In this is actually the case, as the other overload can be called. And it will return false, which is fine, because double is not a class.

The most important takeaway here is that C++ has this rule regarding templates: whenever a template argument substitution creates a syntax error, just ignore that function or class specialization, and continue the compilation. The above example shows why this is so: other overloads or specializations might still work. This rule is called SFINAE, Substitution Failure Is Not An Error, and we can exploit it to control how the compiler uses our template functions and classes.

Usage of SFINAE

With the use (actually the abuse) of the SFINAE rule, we can actually control how the compiler instantiates templates. Imagine that we have a function with an integer argument of some size. It can be an int, a short, maybe a long long. It is important for that function that the argument is actually signed; unsigned int or unsigned short won't do.

This is not a usual specialization of the function or the set of overloads. We'd like to allow instantiation of the function based on some property of the type, and not simply for a list of types or a single type. So we'll have to do the following:

  • Create a metafunction, which takes the type as an argument, and tells whether the type satisfies some criterion. (Is it a signed integer or not?)
  • Use SFINAE to disable instantiation of the function, if the function will not work.

The metafunction for checking signedness looks like this:

template <typename T>
class IsSigned {
    public:
        static constexpr bool value = T(~0) < T(0);
};


int main() {
    std::cout << IsSigned<int>::value << std::endl;
    std::cout << IsSigned<short>::value << std::endl;
    std::cout << IsSigned<unsigned int>::value << std::endl;
    std::cout << IsSigned<unsigned short>::value << std::endl;
}

It is based on the fact that in two's complement notation, a number with all bits as 1's (like 111111111) is the representation of –1. Which is actually smaller than zero. For unsigned values, 11111111... is a very large number, eg. for short it is 65535, definitely greater than zero. We could have used std::is_signed<T> from #include <type_traits> as well.

SFINAE is usually based on a class like this:

// empty class, will be instantiated for EnableIf<false>
template <bool ENABLE>
class EnableIf {
};

// instantiated for EnableIf<true>
template <>
class EnableIf<true> {
  public:
	using type = void;	
};

So EnableIf<false>::type is a nonexistent member (syntax error), and EnableIf<true>::type is equivalent to void. The bool template argument tells if the class should generate a syntax error or not. This is the same as std::enable_if.

The solution is:

template <typename T, typename ENABLE = typename EnableIf<IsSigned<T>::value>::type>
void func(T number) {
    // ...
}

int main() {
    int a = 0;
    func(a);    // ok

    short b = 0;
    func(b);    // ok

    // unsigned c;
    // func(c);    // would be compile error
}

The idea is that we deliberately put the piece of code into the header of the function (in this case: the template argument list), which creates the compile error. If T is a signed number, typename ENABLE = void will be applied, the compiler can use this default template argument after deducing T. However if it is unsigned, the EnableIf part is a syntax error, therefore the function cannot be compiled.

The next example shows a more real life example.

advance: stepping iterators efficiently

This section will introduce void_t, that is also part of the standard since C++17. the definition is this:

template <typename ... T>
using void_t = void;

This looks like the most unusable thing every. You can instantiate the template with any type, like void_t<int> or void_t<double, char>, but every time you will get an alias for void.

They one implements a metafunction with void_t is something like this:

template <typename ... T>
using void_t = void;

template <typename T, typename SFINAE = void>
class IsRandomAccessIterator {
    public:
        static constexpr bool value = false;
};

template <typename T>
class IsRandomAccessIterator<T, void_t<decltype(T() + 0)>> {
    public:
        static constexpr bool value = true;
};

int main() {
    std::cout << IsRandomAccessIterator<std::list<int>::iterator>::value << std::endl;
    std::cout << IsRandomAccessIterator<std::vector<int>::iterator>::value << std::endl;
}

The SFINAE argument name gives a hint on what is happening. To understand the internals, it's nice to follow the instantiations. Firstly for std::list<int>::iterator, the following class declarations and arguments are checked by the compiler:

template <>
class IsRandomAccessIterator<std::list<int>::iterator, void> { /* ... */ };

template <>
class IsRandomAccessIterator<std::list<int>::iterator, void_t<decltype(std::list<int>::iterator() + 0)>> { /* ... */ };

The base template can have the arguments T = std::list<int>::iterator (as specified) and SFINAE = void (as default). The specialization will have T = std::list<int>::iterator (as specified), and the void_t<...> part is yet to be evaluated. However, the evaluation will fail, as the list iterator has no iterator + int operation. Therefore the specialization is discarded, and the base template is used: the value is false, which is the right answer.

Secondly, for std::vector<int>::iterator:

template <>
class IsRandomAccessIterator<std::vector<int>::iterator, void> { /* ... */ };

template <>
class IsRandomAccessIterator<std::vector<int>::iterator, void_t<decltype(std::vector<int>::iterator() + 0)>> { /* ... */ };

The base version is the same, and evaluation is also required for the specialization. The compiler will look up if it can find the operator that could be used for the std::vector<int>::iterator() + 0 expression. This will be successful, as this iterator has the desired iterator + int operation. The decltype keyword will cause the compiler to figure out what the type of this expression is. The type is std::vector<int>::iterator (iterator + integer is an iterator itself), so the actual meaning of this part is void_t<std::vector<int>::iterator>, which is equivalent to void. So the final form of the base template and the specialization is:

template <>
class IsRandomAccessIterator<std::vector<int>::iterator, void> { /* ... */ };

template <>
class IsRandomAccessIterator<std::vector<int>::iterator, void> { /* ... */ };

A strange thing has happened: the template arguments of the base template and its user-defined specialization are the same. So the compiler cannot make a choice based on just the arguments. However, specializations are always preferred to base templates, and that means that in this case the specialization will be selected by the compiler, and the base template will be discarded. And the specialization is

template <>
class IsRandomAccessIterator<std::vector<int>::iterator, void> {
    public:
        static constexpr bool value = true;
};

Which is actually the correct answer: the vector iterator is definitely a random access iterator.

Remark 1. The sole purpose of using the decltype keyword here is to have a context in which the desired expressions can be specified. The expression ITER() + 0 won't be evaluated (the function won't be called), but the compiler will do the overload resolution to figure out the resulting type. The result of that overload resolution is not important either; it is used as a template argument for void_t, but whatever the argument is, void_t is always an alias for void. But to figure this out, the compiler had to do the overload resolution, so it had to check whether the expression can be evaluated.

Remark 2. Any number of expressions can be checked by a void_t<>, that's why it is defined as a variadic template. Just use void_t<decltype(expr1), decltype(expr2), ...>.

Remark 3. The operator+= of the iterator in advance(), but the metafunction checks for operator+. The code assumes that whenever operator+ exists, operator+= will be implemented as well - which is expected. One could also write something like decltype(*(T*)nullptr += 0) to check for the availability of operator+=. This looks like a null pointer dereference, but it is not: remember that the expression in decltype is not evaluated.

The IsRandomAccessIterator function is then used with std::enable_if as usual. That will be another SFINAE. For any iterator, one of the functions will be called, because either IsRandomAccessIterator or !IsRandomAccessIterator will be true.

#include <iostream>
#include <vector>
#include <list>


template <typename T, typename SFINAE = void>
class IsRandomAccessIterator {
    public:
        static constexpr bool value = false;
};

template <typename T>
class IsRandomAccessIterator<T, void_t<decltype(T() + 0)>> {
    public:
        static constexpr bool value = true;
};

template <typename ITER>
void advance(ITER & it, int n, typename std::enable_if<IsRandomAccessIterator<ITER>::value>::type * = nullptr) {
    std::cout << "O(1) advance with +=" << std::endl;
    it += n;
}

template <typename ITER>
void advance(ITER & it, int n, typename std::enable_if<!IsRandomAccessIterator<ITER>::value>::type * = nullptr) {
    std::cout << "O(n) advance with for(...) ++" << std::endl;
    for (int i = 0; i < n; ++i)
        it++;
}

int main() {
    std::vector<int> v = { 1, 2, 3, 4, 5 };
    auto v_it = v.begin();
    advance(v_it, 3);
    
    std::list<int> l = { 1, 2, 3, 4, 5 };
    auto l_it = l.begin();
    advance(l_it, 3);
}