Понимание объявления, определения и специализации шаблонов - PullRequest
11 голосов
/ 11 марта 2019

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

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

Кроме того, почему в первом объявлении шаблона + класса отсутствует "<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

Ответы [ 3 ]

9 голосов
/ 11 марта 2019

Первый объявляет шаблон 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;
};

И последняя определяет полную специализацию для типа нет-arguments:

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

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

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

Конкретный используемый вами экземпляр 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;
};

Или с C ++ 17 выражениями фолда:

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

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

7 голосов
/ 11 марта 2019

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

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

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

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

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

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

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

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

4 голосов
/ 11 марта 2019

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

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

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

template<typename... S>
struct Example; 

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

template <typename...>
struct Example; 

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

На данный момент компилятор знает, что существует структура шаблона variadic 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) (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 или, что еще лучше, ИМХО, снова используя std::integral_constant

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;

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

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