[Из песочницы] Включаем периферию контроллера за 1 такт или магия 500 строк кода

m65v6nk2ktzzuhm0wofwz8bff4o.png

Как часто, при разработке прошивки для микроконтроллера, во время отладки, когда байтики не бегают по UART, вы восклицаете: «Ааа, точно! Не включил тактирование!». Или, при смене ножки светодиода, забывали «подать питание» на новый порт? Думаю, что довольно часто. Я, по крайней мере, — уж точно.

На первый взгляд может показаться, что управление тактированием периферии тривиально: записал 1 — включил, 0 — выключил.

Но «просто», — не всегда оказывается эффективно…

Постановка задачи


Прежде, чем писать код, необходимо определить критерии, по которым возможна его оценка. В случае с системой тактирования периферии контроллера список может выглядеть следующим образом:

  • Во встраиваемых системах, один из самых главных критериев — это минимально-возможный результирующий код, исполняемый за минимальное время
  • Легкая масштабируемость. Добавление или изменение в проекте какой-либо периферии не должно сопровождаться code review всех исходников, чтобы удалить строчки включения/отключения тактирования
  • Пользователь должен быть лишен возможности совершить ошибку, либо, по крайней мере эта возможность должна быть сведена к минимуму
  • Нет необходимости работы с отдельными битами и регистрами
  • Удобство и однообразность использования независимо от микроконтроллера
  • Помимо основных возможностей включения и выключения тактирования периферии необходим расширенный функционал (о нем речь пойдет далее)


После выяснения критериев оценки, поставим конкретную задачу, попутно определив условия и «окружение» для реализации:

Компилятор: GCC 10.1.1 + Make
Язык: C++17
Среда: Visual Studio Code
Контроллер: stm32f103c8t6 (cortex-m3)
Задача: включение тактирования SPI2, USART1 (оба интерфейса с использованием DMA)

Выбор данного контроллера обусловлен, естественно, его распространённостью, особенно, благодаря одному из китайских народных промыслов — производству плат Blue Pill.

dykrtr_rya_ezallc1rlr01dpak.png

С точки зрения идеологии, совершенно неважно, какой именно контроллер выбран: stmf1, stmf4 или lpc, т.к. работа с системой тактирования периферии сводится лишь к записи в определенный бит либо 0 для выключения, либо 1 для включения.

В stm32f103c8t6 имеется 3 регистра, которые ответственны за включение тактирования периферии: AHBENR, APB1ENR, APB2ENR.

Аппаратные интерфейсы передачи данных SPI2 и USART1 выбраны неслучайно, потому что для их полноценного функционирования необходимо включить биты тактирования, расположенные во всех перечисленных регистрах — биты самих интерфейсов, DMA1, а также биты портов ввода-вывода (GPIOB для SPI2 и GPIOA для USART1).

3nx3cyivj--xfv3mtaujrdvebmy.pngzfzskqrorhzx8nx3iknpk5ivrjy.png
g1vehnaxqiq9aoebwrmw9qqsk5q.png

Следует отметить, что для оптимальной работы с тактированием, необходимо учитывать — AHBENR содержит разделяемый ресурс, используемые для функционирования как SPI2, так и USART1. То есть, отключение DMA сразу приведет к неработоспособности обоих интерфейсов, вместе с тем, КПД повторного включения будет даже не нулевым, а отрицательным, ведь эта операция займет память программ и приведет к дополнительному расходу тактов на чтение-модификацию-запись volatile регистра.

Разобравшись с целями, условиями и особенностями задачи, перейдем к поиску решений.

Основные подходы


В этом разделе собраны типовые способы включения тактирования периферии, которые мне встречались и, наверняка, Вы их также видели и/или используете. От более простых, — реализуемых на C, до fold expression из C++17. Рассмотрены присущие им достоинства и недостатки.

Если Вы хотите перейти непосредственно к метапрограммированию, то этот раздел можно пропустить и перейти к следующему.

Прямая запись в регистры


Классический способ, «доступный из коробки» и для С и для C++. Вендор, чаще всего, представляет заголовочные файлы для контроллера, в которых задефайнены все регистры и их биты, что дает возможность сразу начать работу с периферией:

int main(){
  RCC->AHBENR  |= RCC_AHBENR_DMA1EN;
  RCC->APB2ENR |= RCC_APB2ENR_IOPAEN
               |  RCC_APB2ENR_IOPBEN
               |  RCC_APB2ENR_USART1EN;
  RCC->APB2ENR |= RCC_APB1ENR_SPI2EN;
…
}


Листинг
    // AHBENR(Включение DMA1)
  ldr     r3, .L3
  ldr     r2, [r3, #20]
  orr     r2, r2, #1
  str     r2, [r3, #20]
    // APB2ENR(Включение GPIOA, GPIOB, USART1)
  ldr     r2, [r3, #24]
  orr     r2, r2, #16384
  orr     r2, r2, #12
  str     r2, [r3, #24]
    // APB1ENR(Включение SPI2)
  ldr     r2, [r3, #28]
  orr     r2, r2, #16384
  str     r2, [r3, #28]


Размер кода: 36 байт. Посмотреть

Плюсы:

  • Минимальный размер кода и скорость выполнения
  • Самый простой и очевидный способ


Минусы:

  • Необходимо помнить и названия регистров и названия битов, либо постоянно обращаться к мануалу
  • Легко допустить ошибку в коде. Читатель, наверняка, заметил, что вместо SPI2 был повторно включен USART1
  • Для работы некоторых периферийных блоков требуется также включать другую периферию, например, GPIO и DMA для интерфейсов
  • Полное отсутствие переносимости. При выборе другого контроллера этот код теряет смысл


При всех недостатках, этот способ остается весьма востребованным, по крайней мере тогда, когда нужно «пощупать» новый контроллер, написав очередной «Hello, World!» мигнув светодиодом.

Функции инициализации


Давайте попробуем абстрагироваться и спрятать работу с регистрами от пользователя. И в этом нам поможет обыкновенная C-функция:

void UART1_Init(){
 RCC->AHBENR  |= RCC_AHBENR_DMA1EN;
 RCC->APB2ENR |= RCC_APB2ENR_IOPAEN
              |  RCC_APB2ENR_USART1EN;
  // Остальная инициализация
}

void SPI2_Init(){
 RCC->AHBENR  |= RCC_AHBENR_DMA1EN;
 RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
 RCC->APB1ENR |= RCC_APB1ENR_SPI2EN;
  // Остальная инициализация
}

int main(){
  UART1_Init();
  SPI2_Init();
…
}


Размер кода: 72 байта. Посмотреть

Листинг
UART1_Init():
    // AHBENR(Включение DMA1)
  ldr     r2, .L2
  ldr     r3, [r2, #20]
  orr     r3, r3, #1
  str     r3, [r2, #20]
    // APB2ENR(Включение GPIOA, USART1)
  ldr     r3, [r2, #24]
  orr     r3, r3, #16384
  orr     r3, r3, #4
  str     r3, [r2, #24]
  bx      lr
SPI2_Init():
    //Повторно (!) AHBENR(Включение DMA1)
  ldr     r3, .L5
  ldr     r2, [r3, #20]
  orr     r2, r2, #1
  str     r2, [r3, #20]
    //Повторно (!) APB2ENR(Включение GPIOB)
  ldr     r2, [r3, #24]
  orr     r2, r2, #8
  str     r2, [r3, #24]
    //Запись в APB1ENR(Включение SPI2)
  ldr     r2, [r3, #28]
  orr     r2, r2, #16384
  str     r2, [r3, #28]
  bx      lr
main:
   push    {r3, lr}
   bl      UART1_Init()
   bl      SPI2_Init()


Плюсы:

  • Можно не заглядывать в мануал по каждому поводу
  • Ошибки локализованы на этапе написания драйвера периферии
  • Пользовательский код легко воспринимать


Минусы:

  • Количество необходимых инструкций возросло кратно количеству задействованной периферии
  • Очень много дублирования кода — для каждого номера UART и SPI он будет фактически идентичен


Хоть мы и избавились от прямой записи в регистры в пользовательском коде, но какой ценой? Требуемый размер памяти и время исполнения для включения увеличились в 2 раза и продолжат расти, при большем количестве задействованной периферии.

Функция включения тактирования


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

void PowerEnable(uint32_t ahb, uint32_t apb2, uint32_t apb1){
    RCC->AHBENR  |= ahb;
    RCC->APB2ENR |= apb2;
    RCC->APB1ENR |= apb1;
}

void UART_Init(int identifier){
    uint32_t ahb = RCC_AHBENR_DMA1EN, apb1 = 0U, apb2 = 0U;
    if (identifier == 1){
      apb2 = RCC_APB2ENR_IOPAEN | RCC_APB2ENR_USART1EN;
    } 
    else if (identifier == 2){…}
    PowerEnable(ahb, apb2, apb1);
  // Остальная инициализация
}

void SPI_Init(int identifier){
    uint32_t ahb = RCC_AHBENR_DMA1EN, apb1 = 0U, apb2 = 0U;
    if (identifier == 1){…} 
    else if (identifier == 2){
      apb2 = RCC_APB2ENR_IOPBEN;
      apb1 = RCC_APB1ENR_SPI2EN;
    }
    PowerEnable(ahb, apb2, apb1);
  // Остальная инициализация
}

int main(){
  UART_Init(1);
  SPI_Init(2);
…
}


Размер кода: 92 байта. Посмотреть

Листинг
PowerEnable(unsigned long, unsigned long, unsigned long):
  push    {r4}
  ldr     r3, .L3
  ldr     r4, [r3, #20]
  orrs    r4, r4, r0
  str     r4, [r3, #20]
  ldr     r0, [r3, #24]
  orrs    r0, r0, r1
  str     r0, [r3, #24]
  ldr     r1, [r3, #28]
  orrs    r1, r1, r2
  str     r1, [r3, #28]
  pop     {r4}
  bx      lr
UART_Init(int):
  push    {r3, lr}
  cmp     r0, #1
  mov     r2, #0
  movw    r1, #16388
  it      ne
  movne   r1, r2
  movs    r0, #1
  bl      PowerEnable(unsigned long, unsigned long, unsigned long)
  pop     {r3, pc}
SPI_Init(int):
  push    {r3, lr}
  cmp     r0, #2
  ittee   eq
  moveq   r1, #8
  moveq   r1, #16384
  movne   r1, #0
  movne   r2, r1
  movs    r0, #1
  bl      PowerEnable(unsigned long, unsigned long, unsigned long)
  pop     {r3, pc}
main:
   push    {r3, lr}
   movs    r0, #1
   bl      UART_Init(int)
   movs    r0, #2
   bl      SPI_Init(int)


Плюсы:

  • Удалось сократить код описания драйверов микроконтроллера
  • Результирующее количество инструкций сократилось*


Минусы:

  • Увеличилось время выполнения


*Да, в данном случае размер исполняемого кода возрос, по сравнению с предыдущим вариантом, но это связано с появлением условных операторов, влияние которых можно нивелировать, если задействовать хотя бы по 2 экземпляра каждого вида периферии.

Т.к. функция включения принимает параметры, то в ассемблере появились операции работы со стеком, что также негативно сказывается на производительности.

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

Свойства-значения и шаблоны


Начиная рассматривать плюсовый подход, сразу пропустим вариант включения тактирования в конструкторе класса, т.к. этот метод фактически не отличается от инициализирующих функций в стиле C.

Поскольку на этапе компиляции нам известны все значения, которые нужно записать в регистры, то избавимся от операций со стеком. Для этого создадим отдельный класс с шаблонным методом, а классы периферии наделим свойствами (value trait), которые будут хранить значения для соответствующих регистров.

struct Power{
template< uint32_t valueAHBENR, uint32_t valueAPB2ENR, uint32_t valueAPB1ENR>
    static void Enable(){
// Если значение = 0, то в результирующем коде операций с регистром не будет
        if constexpr (valueAHBENR)
            RCC->AHBENR |= valueAHBENR;
        if constexpr (valueAPB2ENR)
            RCC->APB2ENR |= valueAPB2ENR;
        if constexpr (valueAPB1ENR)
            RCC->APB1ENR |= valueAPB1ENR;
    };

};

template
struct UART{
// С помощью identifier на этапе компиляции можно выбрать значения для периферии
  static constexpr auto valueAHBENR = RCC_AHBENR_DMA1EN;
  static constexpr auto valueAPB1ENR = identifier == 1 ? 0U : RCC_APB1ENR_USART2EN;
  static constexpr auto valueAPB2ENR = RCC_APB2ENR_IOPAEN
                                    |  (identifier == 1 ? RCC_APB2ENR_USART1EN : 0U);
    // Остальная реализация
};

template
struct SPI{
  static constexpr auto valueAHBENR = RCC_AHBENR_DMA1EN;
  static constexpr auto valueAPB1ENR = identifier == 1 ? 0U : RCC_APB1ENR_SPI2EN;
  static constexpr auto valueAPB2ENR = RCC_APB2ENR_IOPBEN
                                    |  (identifier == 1 ? RCC_APB2ENR_SPI1EN : 0U);
    // Остальная реализация
};

int main(){
    // Необязательные псевдонимы для используемой периферии
  using uart = UART<1>;
  using spi = SPI<2>;

  Power::Enable<
                uart::valueAHBENR  | spi::valueAHBENR,
                uart::valueAPB2ENR | spi::valueAPB2ENR,
                uart::valueAPB1ENR | spi::valueAPB1ENR
                >();
…
}


Размер кода: 36 байт. Посмотреть

Листинг
main:
    // AHBENR(Включение DMA1)
  ldr     r3, .L3
  ldr     r2, [r3, #20]
  orr     r2, r2, #1
  str     r2, [r3, #20]
    // APB2ENR(Включение GPIOA, GPIOB, USART1)
  ldr     r2, [r3, #24]
  orr     r2, r2, #16384
  orr     r2, r2, #12
  str     r2, [r3, #24]
    // APB1ENR(Включение SPI2)
  ldr     r2, [r3, #28]
  orr     r2, r2, #16384
  str     r2, [r3, #28]


Плюсы:

  • Размер и время выполнения получились такими же, как и в эталонном варианте с прямой записью в регистры
  • Довольно просто масштабировать проект — достаточно добавить воды соответствующее свойство-значение периферии


Минусы:

  • Можно совершить ошибку, поставив свойство-значение не в тот параметр
  • Как и в случае с прямой записью в регистры — страдает переносимость
  • «Перегруженность» конструкции


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

Идеальный вариант… почти


Чтобы уменьшить количество пользовательского кода и возможностей для ошибок воспользуемся parameter pack, который уберет обращение к свойствам классов периферии в пользовательском коде. При этом изменится только метод включения тактирования:

struct Power{
template
  static void Enable(){
      // Для всех параметров пакета будет применена операция | 
      // В нашем случае value = uart::valueAHBENR | spi::valueAHBENR и т.д.
    if constexpr (constexpr auto value = (Peripherals::valueAHBENR | ... ); value)
      RCC->AHBENR |= value;
    if constexpr (constexpr auto value = (Peripherals::valueAPB2ENR | ... ); value)
      RCC->APB2ENR |= value;
    if constexpr (constexpr auto value = (Peripherals::valueAPB1ENR | ... ); value)
      RCC->APB1ENR |= value;
  };
};
    …
int main(){
    // Необязательные псевдонимы для используемой периферии
  using uart = UART<1>;
  using spi = SPI<2>;

  Power::Enable();
…
}


Размер кода: 36 байт. Посмотреть

Листинг
main:
    // AHBENR(Включение DMA1)
  ldr     r3, .L3
  ldr     r2, [r3, #20]
  orr     r2, r2, #1
  str     r2, [r3, #20]
    // APB2ENR(Включение GPIOA, GPIOB, USART1)
  ldr     r2, [r3, #24]
  orr     r2, r2, #16384
  orr     r2, r2, #12
  str     r2, [r3, #24]
    // APB1ENR(Включение SPI2)
  ldr     r2, [r3, #28]
  orr     r2, r2, #16384
  str     r2, [r3, #28]


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

И, вроде бы, можно на этом остановиться, но…

Расширяем функционал


Обратимся к одной из поставленных целей:

Помимо основных возможностей включения и выключения тактирования периферии необходим расширенный функционал


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

В контексте условий, озвученных в начале статьи, будем считать, что генератором события пробуждения будет USART1, а SPI2 и соответствующий ему порт GPIOB — необходимо отключать. При этом, общиий ресурс DMA1 должен оставаться включенным.

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

int main(){
  using uart = UART<1>;
  using spi = SPI<2>;
…
    // Включаем USART, SPI, DMA, GPIOA, GPIOB
  Power::Enable();

    // Some code

    // Выключаем SPI и GPIOB вместе (!) с DMA
  Power::Disable();
    
    // Включаем обратно DMA вместе(!) с USART и GPIOA
  Power::Enable();
    
    // Sleep();

    // Включаем SPI и GPIOB вместе(!) с DMA
  Power::Enable();
…
}


Размер кода: 100 байт. Посмотреть

Листинг
main:
        // AHBENR(Включение DMA1)
        ldr     r3, .L3
        ldr     r2, [r3, #20]
        orr     r2, r2, #1
        str     r2, [r3, #20]
       // APB2ENR(Включение GPIOA, GPIOB, USART1)
        ldr     r2, [r3, #24]
        orr     r2, r2, #16384
        orr     r2, r2, #12
        str     r2, [r3, #24]
       // APB1ENR(Включение SPI2)
        ldr     r2, [r3, #28]
        orr     r2, r2, #16384
        str     r2, [r3, #28]
        // Выключение SPI2
       // AHBENR(Выключение DMA1)
        ldr     r2, [r3, #20]
        bic     r2, r2, #1
        str     r2, [r3, #20]
       // APB2ENR(Выключение GPIOB)
        ldr     r2, [r3, #24]
        bic     r2, r2, #8
        str     r2, [r3, #24]
       // APB1ENR(Выключение SPI2)
        ldr     r2, [r3, #28]
        bic     r2, r2, #16384
        str     r2, [r3, #28]
        // Повторное (!) включение USART1
        // AHBENR(Включение DMA1)
        ldr     r2, [r3, #20]
        orr     r2, r2, #1
        str     r2, [r3, #20]
       // APB2ENR(Включение GPIOA, USART1)
        ldr     r2, [r3, #24]
        orr     r2, r2, #16384
        orr     r2, r2, #4
        str     r2, [r3, #24]
        // Sleep();
        // AHBENR(Включение DMA1)
        ldr     r2, [r3, #20]
        orr     r2, r2, #1
        str     r2, [r3, #20]
       // APB2ENR(Включение GPIOB)
        ldr     r2, [r3, #24]
        orr     r2, r2, #8
        str     r2, [r3, #24]
       // APB1ENR(Включение SPI2)
        ldr     r2, [r3, #28]
        orr     r2, r2, #16384
        str     r2, [r3, #28]



В это же время эталонный код на регистрах занял 68 байт. Посмотреть

Очевидно, что для подобных задач, камнем преткновения станут разделяемые ресурсы, такие как DMA. Кроме того, конкретно в этом случае возникнет момент, когда оба интерфейса станут неработоспособными и, фактически, произойдет аварийная ситуация.

Давайте попробуем найти решение…

Структура


Для упрощения понимания и разработки — изобразим общую структуру тактирования, какой мы ее хотим видеть:

xhwi8gnq8hjxiajczumohxt_qy4.png

Она состоит всего из четырех блоков:

Независимые:

  • IPower — интерфейс взаимодействия с пользователем, подготавливающий данные для записи в регистры
  • Hardware— запись значений в регистры контроллера


Аппаратно-зависимые:

  • Peripherals — периферия, которая используется в проекте и сообщает интерфейсу, какие устройства надо включить или выключить
  • Adapter — передает значения для записи в Hardware, указывая в какие именно регистры их следует записать


Интерфейс IPower


С учетом всех требований, определим методы, необходимые в интерфейсе:

template
Enable();

template
EnableExcept();

template
Keep();


Enable — включение периферии, указанной в параметре шаблона.

EnableExcept — включение периферии, указанной в параметре EnableList, за исключением той, что указана в ExceptList.

Пояснение
Таблица истинности
Например, вызов:
EnableExcept();

должен установить бит SPI2EN и бит IOPBEN. В то время, как общий DMA1EN, а также USART1EN и IOPAEN останутся в исходном состоянии.

Чтобы получить соответствующую таблицу истинности, необходимо произвести следующие операции:

resultEnable = (enable ^ except) & enable


К ним в дополнение также идут комплементарные методы Disable, выполняющие противоположные действия.

Keep — включение периферии из EnableList, выключение периферии из DisableList, при этом, если периферия присутствует в обоих списках, то она сохраняет свое состояние.

Пояснение
Таблица истинности
Например, при вызове:
Keep();

установятся SPI2EN и IOPBEN, при этом USART1EN и IOPAEN сбросятся, а DMA1EN останется неизменным.

Чтобы получить соответствующую таблицу истинности, необходимо произвести следующие операции:

resultEnable = (enable ^ disable) & enable
resultDisable = (enable ^ disable) & disable


Методы включения/выключения уже реализованы довольно неплохо с помощью fold expression, но как быть с остальными?

Если ограничиться использованием 2 видов периферии, как это сделано в пояснении, то никаких сложностей не возникнет. Однако, когда в проекте используется много различных периферийных устройств, появляется проблема — в шаблоне нельзя явно использовать более одного parameter pack, т.к. компилятор не сможет определить где заканчивается один и начинается второй:

template
EnableExcept(){…};
  // Невозможно определить где заканчивается EnableList и начинается ExceptList
EnableExcept();


Можно было бы создать отдельный класс-обертку для периферии и передавать его в метод:

template
PowerWrap{
  static constexpr auto valueAHBENR = (Peripherals::valueAHBENR | …);
  static constexpr auto valueAPB1ENR = (Peripherals:: valueAPB1ENR | …);
  static constexpr auto valueAPB2ENR = (Peripherals:: valueAPB2ENR | …);
};

using EnableList = PowerWrap;
using ExceptList = PowerWrap;

EnableExcept();


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

Поскольку вся используемая периферия и регистры тактирования известны на этапе компиляции, то поставленную задачу возможно решить с использованием метапрограммирования.

Метапрограммирование


Из-за того, что в основе метапрограммирования лежит работа не с обычными типами, а с их списками, то определим две сущности, которые будут оперировать типовыми и нетиповыми параметрами:

template
struct Typelist{};

template
struct Valuelist{};
…
using listT = Typelist ;// Список из последовательности типов char и int
…
using listV = Valuelist<8,9,5,11> ;// Список из 4 нетиповых параметров


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

1. Извлечение первого элемента из списка

front
  // Прототип функции
template
struct front;

  // Специализация для списка типов
  // Разделение списка в пакете параметров на заглавный и оставшиеся
template
struct front>{ 
    // Возвращение заглавного типа
  using type = Head; 
};

 // Специализация для списка нетиповых параметров
template
struct front> {
  // Возвращение заглавного значения
  static constexpr auto value = Head;
};

  // Псевдонимы для простоты использования
template
using front_t = typename front::type;

template
static constexpr auto front_v = front::value;

  // Примеры
using listT = Typelist;
using type = front_t; // type = char

using listV = Valuelist<9,8,7>;
constexpr auto value = front_v; //value = 9


2. Удаление первого элемента из списка

pop_front
template
struct pop_front;

  // Специализация для списка типов
  // Разделение списка в пакете параметров на заглавный и оставшиеся
template
struct pop_front> {
  // Возвращение списка, содержащего оставшиеся типы
  using type = Typelist;
};

template
struct pop_front> {
  using type = Valuelist;
};

template
using pop_front_t = typename pop_front::type;

 // Примеры
using listT = Typelist;
using typeT = pop_front_t; // type = Typelist

using listV = Valuelist<9,8,7>;
using typeV = pop_front_t; // type = Valuelist<8,7>


3. Добавление элемента в начало списка

push_front
template
struct push_front;

template
struct push_front, NewElement> {
  using type = Typelist;
};

template
using push_front_t = typename push_front::type;

  // Пример
using listT = Typelist;
using typeT = push_front_t; // type = Typelist



4. Добавление нетипового параметра в конец списка

push_back_value
template
struct push_back;

template
struct push_back, NewElement>{
  using type = Valuelist;
};

template
using push_back_t = typename push_back::type;

  // Пример
using listV = Valuelist<9,8,7>;
using typeV = push_back_t; // typeV = Valuelist<9,8,7,6>



5. Проверка списка на пустоту

is_empty
template
struct is_empty{
    static constexpr auto value = false;
};

 // Специализация для базового случая, когда список пуст
template<>
struct is_empty>{
    static constexpr auto value = true;
};

template
static constexpr auto is_empty_v = is_empty::value;

 // Пример
using listT = Typelist;
constexpr auto value = is_empty_v; // value = false


6. Нахождение количества элементов в списке

size_of_list
  // Функция рекурсивно извлекает по одному элементу из списка,
  // инкрементируя счетчик count, пока не дойдет до одного из 2 базовых случаев
template
struct size_of_list : public size_of_list, count + 1>{};

  // Базовый случай для пустого списка типов
template
struct size_of_list, count>{
  static constexpr std::size_t value = count;
};

  // Базовый случай для пустого списка нетиповых параметров 
template
struct size_of_list, count>{
  static constexpr std::size_t value = count;
};

template
static constexpr std::size_t size_of_list_v = size_of_list::value;

  // Пример
using listT = Typelist;
constexpr auto value = size_of_list_v ; // value = 3


Теперь, когда определены все базовые действия, можно переходить к написанию метафункций для битовых операций: or, and, xor, необходимых для методов интерфейса.

Поскольку эти битовые преобразования однотипны, то попытаемся сделать реализацию максимально обобщенно, чтобы избежать дублирования кода.

Функция, выполняющая абстрактную операцию над списком

lists_operation
template class operation,
         typename Lists, bool isEnd = size_of_list_v == 1>
class lists_operation{

  using first = front_t; // (3)
  using second = front_t>; // (4)
  using next = pop_front_t>; // (5)
  using result = operation; // (6)

public:

  using type = typename 
      lists_operation>::type; // (7)

};

template class operation, typename List>
class lists_operation{ // (1)
public:
  using type = front_t; // (2)
};

Lists — список, состоящий из типов или списков, над которым необходимо провести некоторое действие.
operation — функциональный адаптер, который принимает 2 первых элемента Lists и возвращает результирующий тип после операции.
isEnd — граничное условие метафункции, которое проверяет количество типов в Lists.

В базовом случае (1) Lists состоит из 1 элемента, поэтому результатом работы функции станет его извлечение (2).

Для остальных случаев — определяют первый (3) и второй (4) элементы из Lists, к которым применяется операция (6). Для получения результирующего типа (7) происходит рекурсивный вызов метафункции с новым списком типов, на первом месте которого стоит (6), за которым следуют оставшиеся типы (5) исходного Lists. Окончанием рекурсии становиться вызов специализации (1).


Далее, реализуем операцию для предыдущей метафункции, которая почленно будет производить абстрактные действия над нетиповыми параметрами из двух списков:

valuelists_operation
template