Проблема 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 + enum
  • std::visit — сопоставление с образцом по вариантам; компилятор проверяет исчерпывающность
  • std::optional<T> — nullable-значение; заменяет сторожевые значения и выходные параметры
  • Оба выделяются на стеке — без кучи, безопасны во встраиваемых и ISR-смежном коде
  • Используйте variant для разнородных очередей событий, диспетчеризации команд, типобезопасных возвращаемых типов
  • Используйте optional для возврата «может не иметь значения», безошибочных результатов «не найдено»

Что дальше