Понимание объявления, определения и специализации шаблонов

Я пытаюсь понять приведенный ниже пример, но меня немного смущают три различных объявления шаблона и структуры.

Не могли бы вы описать, что произойдет для звонка ниже? какой из шаблонов будет использоваться и когда?

Кроме того, почему в первом объявлении шаблона + класса отсутствует " <S...> " сразу после объявления структуры? (Посмотрите, что закомментировано)? когда это правильно добавить, а когда нет?

#include <iostream>
#include <stdio.h>
using namespace std;


template<typename... S>
struct Example /* <S...> */ ; 

template<typename H, typename... T>
struct Example<H, T...>
{
    static const size_t value = sizeof(H) + Example<T...>::value;
};

template<>
struct Example<>
{
    static const size_t value = 0;
};


int main(){
    cout << Example<long, int, char>::value << endl;
    return 0;
}

Выход: 13

Ответы

Ответ 1

Кроме того, почему в первом объявлении шаблона + класса отсутствует "<S...>" сразу после объявления структуры? (Посмотрите, что закомментировано)? когда это правильно добавить, а когда нет?

Мне кажется, что лучше начать с этого момента.

Прежде всего, следующее (удалено с комментарием <S...>) является объявление (внимание: только объявление, а не определение) структуры шаблона. Example который получает список переменных типа шаблона.

template<typename... S>
struct Example; 

Вы также можете избежать использования S и писать просто

template <typename...>
struct Example; 

потому что имя списка переменных не используется в этом контексте.

На данный момент компилятор знает, что существует переменная шаблонная структура Example но не знает, как это делается.

Затем мы добавляем определение специализации Example которая получает один или несколько параметров шаблона (обратите внимание, что Example определена так, чтобы получить ноль или более параметров, поэтому специализация, которая получает один или несколько параметров, является частным случаем Example)

//....... one --> V          VVVVV <- or more template parameter
template<typename H, typename... T>
struct Example<H, T...>
{ // .........^^^^^^^^^  <- this is a specialization
    static const size_t value = sizeof(H) + Example<T...>::value;
};

Часть <H, T...> после Example идентифицирует специализацию (как сказано).

Эта специализация определить static const size_t переменная инициализирована с суммой sizeof(H) (The sizeof() первого параметра шаблона типа) с value, определенным в другом Example класса: Example<T...>.

Итак, вы наблюдаете рекурсивное определение: значение - это сумма sizeof() первого параметра (типа) с суммой sizeof() следующих типов.

Предложение: если вы используете шаблоны с переменными значениями, вы также можете использовать constexpr, поэтому лучше определить value как constexpr

 static constexpr std::size_t value = sizeof(H) + Example<T...>::value;

Или, лучше, вы можете наследовать от std::integral_constant

template <typename H, typename... T>
struct Example <H, T...> 
   : public std::integral_constant<std::size_t, sizeof(H) + Example<T...>{}>
{ };

таким образом, вы наследуете value от std::integral_constant с дополнительными полезными средствами (например: автоматическое преобразование в std::size_t в контексте, где требуется std::size_t)

Каждая рекурсия нуждается в наземном обосновании, поэтому у вас есть

template<>
struct Example<>
{
    static const size_t value = 0;
};

объявление другой специализации Example; на этот раз случай с точно нулевым параметром шаблона (Example<>). В этом случае у вас есть определение value, которая равна нулю для завершения рекурсии.

Как и раньше, вы можете определить value как constexpr или, лучше IMHO, снова используя std::integral_constant constexpr

template <>
struct Example<> : public std::integral_constant<std::size_t, 0u>
 { };

Теперь вы определили две специализации для Example: одну для случаев с одним или несколькими параметрами, одну для случая с нулевыми параметрами. Итак, вы рассмотрели все случаи для Example который объявлен получающим ноль или более параметров; нет необходимости объявлять универсальную (не специализированную версию) Example.

Как отмечает Deduplicator, вы можете определить общий случай и только одну специализацию: если вы пишете

template <typename...>
struct Example : public std::integral_constant<std::size_t, 0u>
 { };

template <typename T, typename ... Ts>
struct Example<T, Ts...>
 : public std::integral_constant<std::size_t, sizeof(T)+Example<Ts...>{}>
 { };

Сначала вы объявляете Example получающий ноль или более параметров, и определяете общий случай со value ноль (основной случай), затем вы определяете одну или более специализацию.

Учитывая, что компилятор выбирает более специализированную версию (когда подходит больше версий), компилятор выбирает специализацию при наличии одного или нескольких параметров (версии ботов совпадают, но специализация более специализированная) и универсальную версию при нулевых параметрах (потому что специализация не совпадает).

Этот способ немного более синтетичен, но может быть менее понятным.

Не могли бы вы описать, что произойдет для звонка ниже? какой из шаблонов будет использоваться и когда?

Теперь должно быть просто понять.

Когда ты пишешь

Example<long, int, char>::value

вы запрашиваете value Example<long, int, char>.

Три параметра, так что выбирается одна или несколько специализаций, то есть

value = sizeof(long) + Example<int, char>::value;

по той же причине value в Example<int, char> равно

value = sizeof(int) + Example<char>::value;

и value в Example<char>

value = sizeof(char) + Example<>::value;

Теперь для Example<>::value выбрана специализация нулевых параметров, а Example<>::value равна нулю.

Итак, у нас есть value в Example<long, int, char> инициализируется

 value = sizeof(long) + sizeof(int) + sizeof(char) + 0;

Вы пометили С++ 11, поэтому жаль, что вы не можете использовать С++ 17 (свертывание шаблонов), где вы можете вообще избежать рекурсии и определить Example как using

template <typename ... Ts>
using Example = std::integral_constant<std::size_t, (... + sizeof(Ts))>;

Ответ 2

Первый объявляет шаблон struct именем Example, принимая любое количество типов:

template<typename... S>
struct Example /* <S...> */ ;

Если за именем недавно объявленного шаблона следует <> с аргументами или без них, это будет специализация!

Второй определяет частичную специализацию по крайней мере для одного аргумента типа:

template<typename H, typename... T>
struct Example<H, T...>
{
    static const size_t value = sizeof(H) + Example<T...>::value;
};

И последний определяет полную специализацию без аргументов типа:

template<>
struct Example<>
{
    static const size_t value = 0;
};

Обратите внимание, что за template следует пустое <> -brackets.

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

Конкретный экземпляр, который вы используете, Example<long,int,char>::value, зависит от Example<int, char>::value, который зависит от Example<char>, что приводит к базовому случаю:

Example<long, int, char>::value = sizeof(long) + Example<int, char>::value; // sizeof(long) + sizeof(int) + 1 + 0
Example<int, char>::value = sizeof(int) + Example<char>::value; // sizeof(int) + 1 + 0
Example<char>::value = sizeof(char) + Example<>::value; // 1 + 0
Example<>::value = 0;

Конечно, пример можно упростить:

template <class... T>
struct Example {
    static const size_t value = 0;
    static_assert(!sizeof...(T), "The base-template only handles no template arguments.");
};
template <class H, class... T>
struct Example {
    static const size_t value = sizeof(H) + Example<T...>::example;
};

Или с С++ 17 сложенных выражений:

template <class... T>
struct Example {
    static const size_t value = 0 + ... + sizeof(T);
};

Кроме того, есть веские причины никогда не использовать using namespace std; Мне интересно, почему вы #include <stdio.h> и return 0; избыточно для main().

Ответ 3

Только отвечая на эту часть вашего вопроса:

Кроме того, почему в первом объявлении шаблона + класса отсутствует < S...> сразу после объявления структуры? (Посмотрите, что закомментировано)? когда это правильно добавить, а когда нет?

  • Когда вы делаете (общее) объявление шаблонной функции/класса/структуры/типа, вы используете угловую скобку < > только один раз перед объявлением:

    template <typename T> 
    void foo(T x);
    
  • Когда вы объявляете конкретное создание экземпляра общего шаблона, вы используете < > дважды, один раз пусто до объявления, затем снова с конкретными параметрами шаблона, для которых вы запускаете:

    template <>
    void foo<int>(int& x);
    
  • Когда вы объявляете конкретную специализацию общего шаблона, вы используете < > один раз с конкретными параметрами шаблона, для которых вы создаете экземпляр:

    template 
    void foo<int>(int& x);
    

Подробнее о последних двух пунктах (и как они отличаются):

Разница между реализацией и специализацией в шаблонах c++