How does int*** in this template metaprogram work?
Asked Answered
M

4

10

How is template metaprogramming working here (static const int value = 1 + StarCounter<\U>::value;) to print out 3 ?

#include <iostream>

template <typename T>
struct StarCounter
{
    static const int value = 0;
};

template <typename U>
struct StarCounter<U*>
{
    static const int value = 1 + StarCounter<U>::value;
};

int main()
{
    std::cout << StarCounter<int***>::value << std::endl;//How is it printing 3?
    return 0;
}
Minesweeper answered 18/12, 2016 at 7:45 Comment(4)
When you create code samples, please don't omit include directives. It makes it a real pain to copy-paste and compile your codeCasares
That's a very basic example of how explicit specialization works. Did you read anything about explicit specialization of templates?Wyckoff
this is a simple enough case that you can deduce it yourself. StarCounter<int***> is the same thing as StarCounter<U*> where U is int** which then uses StarCounter<int**> and so on ...Lavonna
Could you remove the extra \ inside StarCounter<\U>::value? (I can't do single character edits)Cookbook
M
16

The first template creates a struct that will always return 0 when you call StarCounter<U>::value.

The second template specialises the first one for cases where a pointer is used. So when you call it with StarCounter<U*>::value, the second template is used, not the first and it will return StarCounter<U>::value + 1. Note that it removes the pointer at each recursion step.

So the call to StarCounter<int***>::value will expend to:

StarCounter<int***>::value // second template is used due to pointer
1 + StarCounter<int**>::value // second template is used due to pointer
1 + 1 + StarCounter<int*>::value // second template is used due to pointer
1 + 1 + 1 + StarCounter<int>::value // no pointer here, so first template is used
1 + 1 + 1 + 0
3
Mesoglea answered 18/12, 2016 at 7:54 Comment(2)
"will always return 0 when you call StarCounter<U>::value"—not really. There's no call taking place here. StarCounter<U>::value is a static field in the struct, and its value is 0. Writing StarCounter<U>::value simply reads that field.Intromission
@wchargin: well, considering that you cannot read "that field" without calculating it first (instantiating the template), it shouldn't be too hard to see where the "call" vocabulary comes from (generally, in TMP lingo you think of template instantiations as calls and template fields as results of said calls)Doited
P
10
StarCounter<int>::value

equals 0, because it's matched with first instantiation of the template, where value is explicitly defined.

StarCounter<int*>::value = 1 + StarCounter<int>::value

equals 1, because StarCounter<int*> is matched with StarCounter<U*>. Yes, StarCounter<T> can also be considered as a match, but StarCounter<U*> is more specific and that's why this one is preferred.

Similarly,

StarCounter<int**>::value = 1 + StarCounter<int*>::value

equals 2 and

StarCounter<int***>::value = 1 + StarCounter<int**>::value

equals 3.

Pacian answered 18/12, 2016 at 7:53 Comment(0)
I
1

I find it helps to think of runtime equivalents when it comes to metaprogramming. In template metaprogramming, we use partial specialization, as in runtime programming, we use recursion. The primary template functions as the base case and the specializations function as the recursive cases.

Consider the following recursive version of determining the size of a container:

def size(x):
   if empty(x):
       return 0
   else:
       return 1 + size(tail(x))

This is the equivalent of the template code you present. The primary template, StarCounter<T>, is the base case. The empty case. It has size (value) zero. The specialization, StarCounter<U*>, is the recursive case. It has size (value) 1 plus the size of recursing with the tail (StarCounter<U>).

In C++17, we can even more explicitly make the metaprogramming version equivalent to the runtime recursive version (this is presented solely as an example and not as a way that this code should be written):

template <class T>
struct StarCounter {
    static constexpr int calc_value() {
        if constexpr (!std::is_pointer<T>::value) {
            return 0;
        }
        else {
            return 1 + StarCounter<std::remove_pointer_t<T>>::value;
        }
    }

    static constexpr int value = calc_value();
};
Ingeborg answered 18/12, 2016 at 20:27 Comment(0)
A
0

There is a template StarCounter which in it's more general form has constant value equal to 0. So when you use this template for most of the types and ask for the value you will get 0.

This template has also a specialized version which accepts pointers. It's implementation also has constant value which is equal to 1 (as we have a pointer which means we have at least one star) plus value of value of type which this pointer points to.

In case of three stars we have:

StarCounter<int***>::value = 1 + StarCounter<int**>::value (1) + StarCounter<int*>::value (1) + StarCounter<int>::value (0)

Alfilaria answered 18/12, 2016 at 7:53 Comment(2)
StarCounter<int**>::value equals 2, not 1.Pacian
alexeykuzmin0: by (1) I meant the 1 which is used in pointer specialisation equation.Dole

© 2022 - 2024 — McMap. All rights reserved.