Как работает int *** в этой метапрограмме шаблона?

Как работает метапрограммирование шаблонов (static const int value = 1 + StarCounter<\U>::value;) для печати 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;
}

Ответ 1

Первый шаблон создает структуру, которая всегда будет возвращать 0 при вызове StarCounter<U>::value.

Второй шаблон специализируется на первом случае для случаев, когда используется указатель. Поэтому, когда вы вызываете его с помощью StarCounter<U*>::value, используется второй шаблон, а не первый, и он возвращает StarCounter<U>::value + 1. Обратите внимание, что он удаляет указатель на каждом этапе рекурсии.

Таким образом, вызов StarCounter<int***>::value будет затрачен на:

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

Ответ 2

StarCounter<int>::value

равно 0, поскольку он соответствует первому экземпляру шаблона, где value явно определено.

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

равно 1, потому что StarCounter<int*> соответствует StarCounter<U*>. Да, StarCounter<T> также можно рассматривать как совпадение, но StarCounter<U*> является более конкретным и почему этот вариант является предпочтительным.

Аналогично,

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

равно 2 и

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

равно 3.

Ответ 3

Я нахожу, что это помогает думать о эквивалентах времени исполнения, когда дело доходит до метапрограммирования. В метапрограммировании шаблонов мы используем частичную специализацию, как при программировании во время выполнения, мы используем рекурсию. Основной шаблон функционирует как базовый случай, а функция специализаций - как рекурсивные случаи.

Рассмотрим следующую рекурсивную версию определения размера контейнера:

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

Это эквивалент кода шаблона, который вы представляете. Основной шаблон, StarCounter<T>, является базовым. Пустой случай. Он имеет размер (value) ноль. Специализация StarCounter<U*> - это рекурсивный случай. Он имеет размер (value) 1 плюс размер рекурсии с хвостом (StarCounter<U>).

В С++ 17 мы можем еще более явно сделать версию метапрограммирования эквивалентной рекурсивной версии runtime (это представлено исключительно как пример, а не как способ написания этого кода):

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();
};

Ответ 4

Существует шаблон StarCounter, который в нем более общий вид имеет константу value, равную 0. Поэтому, когда вы используете этот шаблон для большинства типов и запрашиваете value, вы получите 0.

Этот шаблон также имеет специализированную версию, которая принимает указатели. Реализация также имеет константу value, которая равна 1 (поскольку у нас есть указатель, который означает, что у нас есть хотя бы одна звезда) плюс значение value типа, на который указывает этот указатель.

В случае трех звезд имеем:

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