Вступление.
На прошлом занятии мы рассмотрели управление портами и отдельными линиями ввода-вывода и подытожили тем фактом, что на практике было бы удобно иметь возможность "собрать" порт из отдельных линий, необязательно идущих подряд, необязательно одного физического порта. Такая возможность требуется в целях упрощения разработки печатной платы, а также в случае активного использования периферии, когда те или иные линии портов заняты.
Применение шаблонов C++ для создания виртуальных портов.
Представим, что мы хотим объединить линии PA0, PB4, PA3, PB2 в один четырехбитный порт. Сразу же напрашивается вполне очевидное объявление шаблона класса, в котором конкретные линии являются шаблонными аргументами:
template<typename... _Pins>
class VirtualPort{};
Использование этого класса (объявление нового виртуального порта) представляет собой инстанцирование шаблона (что в некотором смысле является аналогом создания объекта обычного класса):
using ExampleVirtualPort = VirtualPort<Pa0, Pb4, Pa3, Pb2>;
Теперь попробуем предложить возможную реализацию методов этого шаблона (в перспективе интерфейс класса должен в точности совпадать с интерфейсом класса обычного порта, который мы рассматривали на предыдущем занятии).
Рассмотрим метод включения тактирования порта: void Enable(). Несложно догадаться, что для корректной реализации достаточно выполнения двух действий:
- Определить, какие физические порты участвуют в качестве части виртуального.
- Включить тактирование всех физических портов.
Самая примитивная, но неоптимальная, реализация этого метода может выглядеть так:
static void Enable()
{
(_Pins::Port::Enable(), ...);
}
На этапе компиляции для предложенного нами примера свертка (_Pins::Port::Enable(), ...) преобразуется в последовательный вызов четырех методов:
Pa0::Port::Enable(); // Включение тактирования порта A; Pb4::Port::Enable(); // Включение тактирования порта B; Pa3::Port::Enable(); // Включение тактирования порта A (повторно!); Pb2::Port::Enable(); // Включение тактирования порта B (повторно!);
Недостатком является то, что в такой реализации для одного и того же физического порта тактирование будет включаться столько раз, сколько линий этого порта участвуют в составном виртуальном. Однако, как правило, данный метод вызывается единожды, поэтому этот факт можно проигнорировать. В библиотеке эта проблема решена, однако рассмотрение решения выходит за рамки этой статьи (интересующиеся могут посмотреть в исходниках). Если вкратце, то для любого списка типов существует возможность исключить дублирование, то есть получить элементы без повтора, и уже к ним применить вызов метода.
Теперь рассмотрим любой метод управления портом, потому что их реализация похожа: в целом нет разницы между настройкой конфигурации, режима виртуального порта или записи в него числового значения. Остановимся на методе установки битов. Прототип метода возьмем из класса обычного порта: void Set(DataT value);
К сожалению, очевидного решения, как в случае с методом включения тактирования, здесь не существует, поскольку параметр метода (значение для записи) шаблонным не является (это было бы странным), соответственно задачу невозможно разрешить шаблонными методами в compile-time.
Решим несколько вспомогательных задач.
Определение принадлежности линий виртуального порта физическому.
В первую очередь, хочется научиться определять, какие именно линии виртуального порта принадлежат тому или иному физическому. Это можно сделать во время компиляции, что позволяет задействовать шаблонную магию.
В составе библиотеки представлен заголовочный файл type_list.h, содержащий определение списка типов а также инструменты для работы с такими списками. Данная тема не относится напрямую к разработке программ для микроконтроллеров, поэтому предлагаю вынести реализацию этих инструментов за рамки статьи, ограничившись описанием некоторых из них. Однако, если будет запрос на углубленное объяснение действия шаблонов C++, в частности понятия списка типов, я подготовлю отдельный материал на эту тему.
Само понятие списка типов очень простое (стоит отметить, что простым оно стало со стандарта C++17, а до этого использовалось рекурсивное определение, которое предложил Андрей Александреску в книге "Современное проектирование на С++" и реализации своих идей в виде библиотеки LOKI):
template<typename... Types>
class TypeList{};
// Использование
using MyTypeList = TypeList<int, int, double, std::vector>;
// Применительно к нашему случаю
using MyPins = TypeList<Pa0, Pb4, Pa3, Pb2>;
Внимательный читатель скажет, что это то же самое, что std::tuplestd::tuple и будет абсолютно прав!
Сам по себе список типов никакой пользы не несет, пока мы не определили возможные операции над ним. К базовым можно отнести следующие:
- Взятие типа по его индексу;
- Определение индекса типа в списке;
- Определение принадлежности типа списку типов.
Для общего понимания работы со списками типа разберем первую операцию из представленных: взятие типа по его индексу. Как правило, для работы со списками типов применяется рекурсивный подход, потому что для самого понятия списка можно предложить рекурсивное определение: список - это первый элемент и хвост, который в свою очередь тоже является списком.
// Базовый класс для дальнейшей специализации
// Первый шаблонный параметр - номер типа
// Второй (variadic) - список типов
template<int, typename...>
class GetType {};
// Специализация дна рекурсии, когда из списка <Head, Tail>
// нужно извлечь начальный (нулевой) тип.
template<typename Head, typename... Tail>
class GetType<0, TypeList<Head, Tail...>>
{
public:
using type = Head;
};
// Общий случай: если Index > 0, то берем тип с номером
// Index - 1 из хвоста списка
template<int Index, typename Head, typename... Tail>
class GetType<Index, TypeList<Head , Tail...>>
{
static_assert(Index < Length<TypeList<Head, Tail...>>::value);
public:
using type = typename GetType<Index - 1, TypeList<Tail...>>::type;
};
Но есть и более продвинутые задачи, например:
- Удаление дубликатов из списка;
- Сортировка по предикату;
- Поиск (фильтрация) типов по предикату.
Последний пункт нам подходит! Допустим, что для произвольного списка типов доступен следующий функционал:
template<template <typename> class Predic