четверг, 22 января 2026 г.

Variadic темплейты в C++, tuple / Variadic template and tuple

 Когда я решил разобраться с вариадик темплейтами, то, как обычно, пошел искать статьи на хабре и нашел их великое множество, но почти все они либо написаны так, как будто ты уже собаку съел на всех этих эллипсисах и тебе ничего не надо пояснять, либо не содержали нужной мне информации. Но вот наткнулся на эту статью, где все вроде бы понятно, рассмотрена концепция "откусывания головы" у переменного списка параметров (эллипсис), потом специализацию "дна", когда параметров уже не осталось и создается пустая структура (что есть конец шаблонной рекурсии) и аналогично, когда мы перечисляем все поля, реализован шаблон для нулевого поля ("стукаемся о дно структуры"). И для демонстрации всего этого рассмотрен пример реализации tuple. Но когда попробовал реализовать все это на VS2022, то выяснилось, что пример из хабра какой-то, мягко говоря, неоптимальный, когда на три поля (int, double, int) уходит 56 байт, а на обычную структуру уходит всего лишь 24 байта (это, кстати, при сумме размеров полей 16 байт, но тут уже выравнивание по 8 байтам уже работает на 64-битной архитектуре), что тоже не идеально, но уже гораздо лучше. Я собрал пример из статьи на хабре и получил вот такой отутпут:

12 2.34 89 12 2.34 89 12 2.34 89 sizeof(int) + sizeof(double) + sizeof(int) = 16 sizeof(habr_tuple<int, double, int>) = 56 sizeof(plain_struct) = 24

Вот пример с вариадиком с хабра:

  1. template<typename... Args>
  2. struct habr_tuple;
  3. template<typename Head, typename... Tail>
  4. struct habr_tuple<Head, Tail...> : habr_tuple<Tail...>
  5. {
  6. habr_tuple(Head h, Tail... tail)
  7. : habr_tuple<Tail...>(tail...), head_(h)
  8. {
  9. }
  10. typedef habr_tuple<Tail...> base_type;
  11. typedef Head value_type;
  12. base_type base = static_cast<base_type&>(*this);
  13. Head head_;
  14. };
  15. template<>
  16. struct habr_tuple<>
  17. {
  18. };
  19. template<int I, typename Head, typename... Args>
  20. struct getter
  21. {
  22. typedef typename getter<I - 1, Args...>::return_type return_type;
  23. static return_type get(habr_tuple<Head, Args...> t)
  24. {
  25. return getter<I - 1, Args...>::get(t);
  26. }
  27. };
  28. template<typename Head, typename... Args>
  29. struct getter<0, Head, Args...>
  30. {
  31. typedef typename habr_tuple<Head, Args...>::value_type return_type;
  32. static return_type get(habr_tuple<Head, Args...> t)
  33. {
  34. return t.head_;
  35. }
  36. };
  37. template<int I, typename Head, typename... Args>
  38. typename getter<I, Head, Args...>::return_type
  39. get(habr_tuple<Head, Args...> t)
  40. {
  41. return getter<I, Head, Args...>::get(t);
  42. };

Мне тут очень многое было непонятно, особенно член base, который ссылка (то есть по сути указатель, и по размеру тоже) - зачем он тут нужен? Почему нельзя было обойтись без него? А почему просто нельзя рекурсивно вставлять структуры, без указателей? Я переделал этот пример следующим образом:

  1. template<typename... Args>
  2. struct my_tuple;
  3. // отделяем хвост от головы списка параметров
  4. template<typename Head, typename... Tail>
  5. struct my_tuple<Head, Tail...>
  6. {
  7. my_tuple(Head h, Tail... tail)
  8. : head_(h), tail_(tail...)
  9. {
  10. }
  11. typedef Head value_type;
  12. Head head_;
  13. my_tuple<Tail...> tail_;
  14. };
  15. // нет параметров - конец рекурсии (пустая структура)
  16. template<>
  17. struct my_tuple<>
  18. {
  19. };
  20. // вспомогательная функция для получения поля по номеру
  21. template<int I, typename Head, typename... Args>
  22. struct getter2
  23. {
  24. typedef typename getter2<I - 1, Args...>::return_type return_type;
  25. static return_type get(my_tuple<Head, Args...> t)
  26. {
  27. return getter2<I - 1, Args...>::get(t.tail_);
  28. }
  29. };
  30. // вспомогательная функция, специализация для 0 (конец рекурсии)
  31. template<typename Head, typename... Args>
  32. struct getter2<0, Head, Args...>
  33. {
  34. typedef typename my_tuple<Head, Args...>::value_type return_type;
  35. static return_type get(my_tuple<Head, Args...> t)
  36. {
  37. return t.head_;
  38. }
  39. };
  40. // вспомогательная ф-я
  41. template<int I, typename Head, typename... Args>
  42. typename getter2<I, Head, Args...>::return_type
  43. get(my_tuple<Head, Args...> t)
  44. {
  45. return getter2<I, Head, Args...>::get(t);
  46. };

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

12 2.34 89 12 2.34 89 12 2.34 89 sizeof(int) + sizeof(double) + sizeof(int) = 16 sizeof(habr_tuple<int, double, int>) = 56 12 2.34 89 12 2.34 89 sizeof(my_tuple<int, double, int>) = 24 sizeof(std::tuple<int, double, int>) = 24 sizeof(plain_struct) = 24