Мапим значения на типы. Ч2
#опытным
Напомню проблему:
enum class DataType: uint8_t {
kInt32,
kDouble,
kUint64
};
template
T create_from_buffer(const void* buffer) {
static_assert(std::is_trivially_copyable_v,
"Type T must be trivially copyable");
T obj;
std::memcpy(&obj, buffer, sizeof(T));
return obj;
}
Как на основе DataType создать объект соответствующего типа?
Compile-time мапа из массива, где по индексу подкапотного числа, стоящего за каждым перечислителем, позволяет получить соответствующий нужный тип и выбрать правильный обработчик. И все это за О(1) с тратами на сдвиг элемента в массиве
Если же перечислителям явно присвоены числа, то этот номер не прокатит и надо думать дальше.
Без шаблонной магии нам не обойтись, ведь мы хотим только добавлять маппинги, но не менять самого кода обработчиков и логику их выбора.
И давайте заодно попробуем уйти от полной специализации шаблонов, как было в предыдущей части.
Сделаем структуру аля ноду маппинга:
template
struct Node { static constexpr DataType key = D; using type = T; };
В структуре хранится конкретный перечислитель, получаемый из шаблонного параметра, и соответствующий ему тип.
И в качестве пака шаблонных параметров нашей шаблонной функции мы будем передавать список нод Node, Node, Node.
В функции мы должны сделать примерно следующее. Перебираем рантайм значение enum'а на соответствие значению Key из нод пака параметров. Перебираем, пока не найдем нужный и после используем правильную ноду для получения замаппленого типа.
И вот этот перебор можно делать через fold expression и короткозамкнутый оператор ||. Как только условие истинно, вычисления прекращаются.
template
DataVariant ConvertImpl(const void* buffer, DataType type) {
DataVariant result;
bool found = ((Es::key == type
? (result = create_from_buffer(buffer), true)
: false) || ...);
if (!found) throw std::runtime_error("Unknown type");
return result;
}
DataVariant Convert(const void* buffer, DataType type) {
return ConvertImpl<
Node,
Node,
Node
>(buffer, type);
}
Также тут используется фишка оператора запятой, что результатом выражения является только самый крайний справа операнд.
Теперь вместо поиска элемента в массиве по индексу мы занимаемся линейным проходом выполнения логического ИЛИ для типов.
На первый взгляд это может быть сильно дольше, но компиляторы хорошо умеют оптимизировать fold expression, даже в худшем случае большой разницы не будет.
Код кстати по размеру уже не сильно больше изначального свитча.
Вот примерчик с рабочим кодом, можете поиграться.
Use tricks. Stay cool.
#template #cpp17
#опытным
Напомню проблему:
enum class DataType: uint8_t {
kInt32,
kDouble,
kUint64
};
template
T create_from_buffer(const void* buffer) {
static_assert(std::is_trivially_copyable_v,
"Type T must be trivially copyable");
T obj;
std::memcpy(&obj, buffer, sizeof(T));
return obj;
}
Как на основе DataType создать объект соответствующего типа?
Compile-time мапа из массива, где по индексу подкапотного числа, стоящего за каждым перечислителем, позволяет получить соответствующий нужный тип и выбрать правильный обработчик. И все это за О(1) с тратами на сдвиг элемента в массиве
Если же перечислителям явно присвоены числа, то этот номер не прокатит и надо думать дальше.
Без шаблонной магии нам не обойтись, ведь мы хотим только добавлять маппинги, но не менять самого кода обработчиков и логику их выбора.
И давайте заодно попробуем уйти от полной специализации шаблонов, как было в предыдущей части.
Сделаем структуру аля ноду маппинга:
template
struct Node { static constexpr DataType key = D; using type = T; };
В структуре хранится конкретный перечислитель, получаемый из шаблонного параметра, и соответствующий ему тип.
И в качестве пака шаблонных параметров нашей шаблонной функции мы будем передавать список нод Node, Node, Node.
В функции мы должны сделать примерно следующее. Перебираем рантайм значение enum'а на соответствие значению Key из нод пака параметров. Перебираем, пока не найдем нужный и после используем правильную ноду для получения замаппленого типа.
И вот этот перебор можно делать через fold expression и короткозамкнутый оператор ||. Как только условие истинно, вычисления прекращаются.
template
DataVariant ConvertImpl(const void* buffer, DataType type) {
DataVariant result;
bool found = ((Es::key == type
? (result = create_from_buffer(buffer), true)
: false) || ...);
if (!found) throw std::runtime_error("Unknown type");
return result;
}
DataVariant Convert(const void* buffer, DataType type) {
return ConvertImpl<
Node,
Node,
Node
>(buffer, type);
}
Также тут используется фишка оператора запятой, что результатом выражения является только самый крайний справа операнд.
Теперь вместо поиска элемента в массиве по индексу мы занимаемся линейным проходом выполнения логического ИЛИ для типов.
На первый взгляд это может быть сильно дольше, но компиляторы хорошо умеют оптимизировать fold expression, даже в худшем случае большой разницы не будет.
Код кстати по размеру уже не сильно больше изначального свитча.
Вот примерчик с рабочим кодом, можете поиграться.
Use tricks. Stay cool.
#template #cpp17