Проблема void*
C-код передаёт значения неизвестного типа через void* и целочисленный тег:
1typedef struct {
2 int type; // 0=int, 1=float, 2=string
3 void* data;
4} Value;
5
6// Приведение на основе тега — неопределённое поведение при ошибке
7float f = *(float*)val.data;
Ничто не мешает передать неправильный тип. Тег может рассинхронизироваться
с данными. void* требует явного управления памятью.
C++ улучшает это с помощью union + enum, но объединения по-прежнему
управляются вручную — можно забыть установить дискриминант, забыть уничтожить
активный член или прочитать не тот член.
std::variant — типобезопасное дискриминированное объединение. Активный тип
всегда отслеживается. Нельзя прочитать не тот член без исключения.
std::variant — типобезопасное объединение
std::variant<T1, T2, ...> хранит ровно один из перечисленных типов одновременно.
1#include <variant>
2
3using Value = std::variant<int, float, std::string>;
4
5Value v = 42; // хранит int
6v = 3.14f; // теперь хранит float
7v = std::string("hi"); // теперь хранит string
Доступ к значению
std::get<T> — извлекает по типу, бросает std::bad_variant_access при ошибке:
1float f = std::get<float>(v); // OK — v хранит float
2int i = std::get<int>(v); // исключение — v хранит float
std::get_if<T> — возвращает указатель, nullptr при неправильном типе:
1if (auto* f = std::get_if<float>(&v)) {
2 process(*f); // безопасно — указатель ненулевой
3}
std::holds_alternative<T> — проверить, какой тип активен:
1if (std::holds_alternative<std::string>(v)) {
2 auto& s = std::get<std::string>(v);
3 uart_send(s);
4}
std::visit — сопоставление с образцом по всем типам
std::visit применяет вызываемый объект к активной альтернативе, обрабатывая каждый случай:
1std::visit([](auto&& val) {
2 using T = std::decay_t<decltype(val)>;
3 if constexpr (std::is_same_v<T, int>)
4 printf("int: %d\n", val);
5 else if constexpr (std::is_same_v<T, float>)
6 printf("float: %.2f\n", val);
7 else if constexpr (std::is_same_v<T, std::string>)
8 printf("string: %s\n", val.c_str());
9}, v);
Или перегруженный посетитель с помощью вспомогательного типа:
1// Вспомогательный тип для построения посетителя из нескольких лямбд
2template<typename... Ts>
3struct overloaded : Ts... { using Ts::operator()...; };
4template<typename... Ts>
5overloaded(Ts...) -> overloaded<Ts...>;
6
7std::visit(overloaded{
8 [](int i) { printf("int: %d\n", i); },
9 [](float f) { printf("float: %.2f\n", f); },
10 [](const std::string& s){ printf("string: %s\n", s.c_str()); },
11}, v);
Компилятор проверяет, что обработаны все альтернативы. Добавьте новый тип в
variant — visit не скомпилируется, пока не добавите обработчик для него.
Это C++-версия исчерпывающего сопоставления с образцом.
Variant для диспетчеризации команд/событий
Распространённый встраиваемый паттерн: очередь разнородных событий из разных источников.
1struct TemperatureEvent { float celsius; };
2struct ButtonEvent { uint8_t id; bool pressed; };
3struct ErrorEvent { uint32_t code; };
4
5using Event = std::variant<TemperatureEvent, ButtonEvent, ErrorEvent>;
6
7// Очередь — без void*, без union, без ручного тега
8RingBuffer<Event, 32> eventQueue;
9
10// Производитель (ISR или задача)
11eventQueue.push(TemperatureEvent{23.5f});
12eventQueue.push(ButtonEvent{1, true});
13
14// Потребитель
15while (auto ev = eventQueue.pop()) {
16 std::visit(overloaded{
17 [](const TemperatureEvent& e) { updateDisplay(e.celsius); },
18 [](const ButtonEvent& e) { handleButton(e.id, e.pressed); },
19 [](const ErrorEvent& e) { logError(e.code); },
20 }, *ev);
21}
Сравните с эквивалентом на void*: нет приведений, нет ручного free, нет
рассинхронизации тегов. Добавление нового типа события вызывает ошибку компиляции
везде, где оно не обработано.
std::optional — nullable-значения без нулевых указателей
std::optional<T> либо содержит значение, либо пуст. Это правильное представление
для «операция может не вернуть значение».
1#include <optional>
2
3std::optional<float> readSensor() {
4 if (!sensorReady()) return std::nullopt; // нет значения
5 return readRaw(); // есть значение
6}
7
8// Использование
9auto result = readSensor();
10
11if (result) {
12 process(*result); // разыменование при непустом
13}
14
15// Или с value_or — указать значение по умолчанию
16float temp = readSensor().value_or(NAN);
17
18// value() бросает std::bad_optional_access при пустом
19float temp2 = readSensor().value(); // бросает, если датчик не готов
Замена сторожевых значений
1// До: сторожевое значение — -1 означает "не найдено"
2int findDevice(uint8_t addr) {
3 for (int i = 0; i < deviceCount; ++i)
4 if (devices[i].addr == addr) return i;
5 return -1; // вызывающий должен помнить о проверке
6}
7
8// После: optional — пустота явно выражена в типе
9std::optional<int> findDevice(uint8_t addr) {
10 for (int i = 0; i < deviceCount; ++i)
11 if (devices[i].addr == addr) return i;
12 return std::nullopt;
13}
14
15auto idx = findDevice(0x3C);
16if (idx) initDevice(devices[*idx]);
Никаких магических чисел, никакой неоднозначности «-1 — это ошибка или допустимый индекс?»
Замена выходных параметров
1// До: выходной параметр + возврат bool
2bool parseFrame(const uint8_t* buf, size_t len, Frame* out);
3
4// После: возврат optional
5std::optional<Frame> parseFrame(std::span<const uint8_t> buf) {
6 if (buf.size() < FRAME_MIN_LEN) return std::nullopt;
7 if (checksum(buf) != buf.back()) return std::nullopt;
8 return Frame::from(buf);
9}
10
11auto frame = parseFrame(rxBuf);
12if (frame) handle(*frame);
Более чистое место вызова, нет неинициализированного выходного параметра, случай ошибки очевиден.
std::optional во встраиваемых системах
std::optional<T> хранит T плюс флаг bool. Для тривиально копируемого T
это ровно sizeof(T) + 1 (с выравниванием). Никакого выделения на куче.
1// optional на стеке — без кучи, безопасен для ISR
2std::optional<SensorSample> latest = readAdc();
Это делает optional естественной заменой сторожевых значений во встраиваемом коде,
где куча запрещена.
Для variant размер — max(sizeof(T1), sizeof(T2), ...) + дискриминант.
Тоже на стеке. На Cortex-M4 std::variant<float, uint32_t, ErrorCode>
занимает 8 байт — столько же, сколько float + целочисленный тег вручную.
Обработка ошибок с variant
Распространённый паттерн: Result<T, E> — либо значение, либо ошибка. C++23 добавляет
std::expected именно для этого, но можно реализовать через variant в C++17:
1template <typename T, typename E>
2using Result = std::variant<T, E>;
3
4struct ParseError { std::string message; };
5
6Result<Frame, ParseError> parse(std::span<const uint8_t> data) {
7 if (data.size() < 4) return ParseError{"too short"};
8 if (!validateChecksum(data)) return ParseError{"bad checksum"};
9 return buildFrame(data);
10}
11
12auto result = parse(rxBuf);
13std::visit(overloaded{
14 [](const Frame& f) { processFrame(f); },
15 [](const ParseError& e) { logError(e.message); },
16}, result);
Это безопаснее исключений (нет накладных расходов на раскрутку стека, работает
на МК с -fno-exceptions) и более явно, чем коды ошибок.
C++23: std::expected
C++23 стандартизирует тип результата как std::expected<T, E>:
1#include <expected>
2
3std::expected<Frame, ParseError> parse(std::span<const uint8_t> data) {
4 if (data.size() < 4) return std::unexpected(ParseError{"too short"});
5 return buildFrame(data);
6}
7
8auto result = parse(rxBuf);
9if (result) processFrame(*result);
10else logError(result.error().message);
Та же семантика, что у версии с variant, но с выделенным API — .value(),
.error(), .value_or() и монадические операции .and_then(), .or_else().
Итоги
std::variant<T1, T2, ...>— типобезопасное дискриминированное объединение; заменяетvoid*+ тег иunion+ enumstd::visit— сопоставление с образцом по вариантам; компилятор проверяет исчерпывающностьstd::optional<T>— nullable-значение; заменяет сторожевые значения и выходные параметры- Оба выделяются на стеке — без кучи, безопасны во встраиваемых и ISR-смежном коде
- Используйте
variantдля разнородных очередей событий, диспетчеризации команд, типобезопасных возвращаемых типов - Используйте
optionalдля возврата «может не иметь значения», безошибочных результатов «не найдено»
Что дальше
- Руководство по выбору контейнера — выбор правильного контейнера для данных
- Умные указатели и владение — типобезопасное выделение на куче с владением
- Lock-free очереди — передача вариантов между потоками без выделения