Структура с необязательными членами в современном C++

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

struct Location {
    int area;
    QPoint coarse_position;
    int layer;
    QVector3D fine_position;
    QQuaternion rotation;
};

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

Структуры десериализуются таким образом (псевдокод):

Location loc;
// Bitfield expressing whether each member is present in this instance
uchar flags = read_byte();
// If _area_ is present, read it from the stream, else it is filled with garbage
if (flags & area_is_present)
    loc.area = read_byte();
if (flags & coarse_position_present)
    loc.coarse_position = read_QPoint();
etc.

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

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

Сначала мы подумали об использовании std :: optional:

struct Location {
    std::optional<int> area;
    std::optional<QPoint> coarse_location;
    // etc.
};

Это решение не устраняет недостаток конструкции, а модернизирует его.

Мы думали об использовании std :: variant вот так:

struct Location {
    struct Has_Area_and_Coarse {
        int area;
        QPoint coarse_location;
    };
    struct Has_Area_and_Coarse_and_Fine {
        int area;
        QPoint coarse_location;
        QVector3D fine_location;
    };
    // etc.
    std::variant<Has_Area_and_Coarse,
                 Has_Area_and_Coarse_and_Fine /*, etc.*/> data;
};

Это решение делает невозможным представление недопустимых состояний, но плохо масштабируется, когда возможно более нескольких комбинаций переменных-членов. Более того, мы не хотели бы получать доступ, задавая Has_Area_and_Coarse, но что-то более близкое к loc.fine_position.

Есть ли стандартное решение этой проблемы, которое мы не рассматривали?

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

user7860670 15.03.2018 07:33

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

n. 'pronouns' m. 15.03.2018 07:35

Если вы хотите предотвратить запрос неинициализированного члена, самым простым решением было бы иметь конструктор, который инициализирует все члены. Есть ли причина, по которой он не используется? Если вам нужен более современный подход, кортежи также можно использовать в сочетании с std :: get для доступа к членам, который развертывается во время выполнения.

Ryan Stonebraker 15.03.2018 07:35

Если вы не хотите «запрашивать член, которого нет», вы должны определить отдельные классы для каждой возможной комбинации полей (возможно, используя иерархию классов, если это имеет смысл), определить необходимые операции в базовом классе ((чистый) virtual) и реализовать их в производных классах. тогда вам больше не нужны никакие проверки во время выполнения. Но вызовы виртуальных функций могут быть немного медленнее, чем просто использование optional.

Rene 15.03.2018 07:36

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

dgsomerton 15.03.2018 07:37

Вариант, вероятно, самый безопасный вариант, но его дизайн выглядит подозрительно.

juanchopanza 15.03.2018 07:40

Вы хотите сказать, что любое использование, скажем, «местоположение с областью» должно быть статически отделено от любого использования «местоположение без области»?

n. 'pronouns' m. 15.03.2018 07:40

Я не могу найти существенной разницы между дизайном, использующим std ::: option, и дизайном std :: optional. Если вы сгенерируете, возможно, автоматически, все необходимые варианты, вы получите что-то эквивалентное дизайну std :: optional, но более неудобное в использовании.

n. 'pronouns' m. 15.03.2018 07:48

@ n.m. Плюсы, которые я могу придумать: если вариант потребляется через посетителей, то если заставляет рассматривать все типы. Кроме того, он позволяет использовать отдельные, лучше спроектированные типы вместо гибридного типа. Но это действительно зависит от того, как реальный вариант использования.

juanchopanza 15.03.2018 07:49

@juanchopanza Есть варианты O (2 ^ N), не то, что я бы хотел применить ...

n. 'pronouns' m. 15.03.2018 08:27

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

Passer By 15.03.2018 08:35
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
11
11
6 816
4

Ответы 4

А как насчет миксинов?

struct QPoint {};
struct QVector3D {};
struct Area {
    int area;
};
struct CoarsePosition {
    QPoint coarse_position;
};
struct FinePosition {
    QVector3D fine_position;
};
template <class ...Bases>
struct Location : Bases... {
};

Location<Area, CoarsePosition> l1;
Location<Area, FinePosition> l2;

Одна потенциальная проблема здесь заключается в том, что Location<Area> и Location<Area, CoarsePosition> не связаны, хотя последний, возможно, является первым из типа.

n. 'pronouns' m. 15.03.2018 08:14

Сначала я скажу, что иногда мне хотелось иметь "необязательную" опцию класса, когда все члены становятся необязательными. Я думаю, возможно, это было бы возможно без правильного метапрограммирования с использованием кода, подобного magic_get Антония Полухина.

Но как бы то ни было ... У вас может быть карта атрибутов с частичной типобезопасностью с произвольно типизированными значениями:

class Location {
    enum class Attribute { area, coarse_position, fine_position, layer };   
    std::unoredered_map<Attribute, std::any> attributes;
}

std::any может содержать любой тип (что-то, выделяя пространство в стеке, иногда внутренне). Облицовка наружу шрифт стирается, но вы можете восстановить его методом get<T>(). Это безопасный в том смысле, что вы получите исключение, если вы сохранили объект одного типа и пытаетесь get() другого типа, но это небезопасно в том смысле, что вы не получите ошибку во время компиляции.

Это можно адаптировать к случаю произвольных атрибутов, помимо тех, которые вы изначально планировали, например:

class Location {
    using AttributeCode = uint8_t;
    enum : AttributeCode { 
        area            = 12,
        coarse_position = 34,
        fine_position   = 56,
        layer           = 789
    };   
    std::unoredered_map<AttributeCode, std::any> attributes;
}

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

На практике, кстати, std::vector, вероятно, будет быстрее искать, чем std::unordered_map.

Предостережение: это решение не дает вам желаемой типобезопасности.

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

#include <stdexcept>

struct foo
{
    int a;
    float b;
    char c;
};

struct rt_foo : foo
{
    unsigned valid;
};

template <unsigned valid>
struct ct_foo : foo
{
    // cannnot default construct
    ct_foo () = delete;

    // cannot copy from version withouth validity flags
    ct_foo (foo const &) = delete;
    ct_foo & operator = (foo const &) = delete;

    // copying from self is ok
    ct_foo (ct_foo const &) = default;
    ct_foo & operator = (ct_foo const &) = default;

    // converting constructor and assignement verify the flags 
    ct_foo (rt_foo const & rtf) :
        foo (check (rtf))
    {
    }

    ct_foo & operator = (rt_foo const & rtf)
    {
        *static_cast <foo*> (this) = check (rtf);

        return *this;
    }

    // using a member that is not initialize will be a compile time error at when
    // instantiated, which will occur at the time of use

    auto & get_a () { static_assert (valid & 1); return a; }
    auto & get_b () { static_assert (valid & 2); return a; }
    auto & get_c () { static_assert (valid & 3); return a; }

    // helper to validate the runtime conversion

    static foo & check (rt_foo const & rtf)
    {
        if ((valid & rtf.valid) != 0)
            throw std::logic_error ("bad programmer!");
    }
};

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

#include <stdexcept>
#include <iostream>

struct stream {
    template <typename value> value read ();
    template <typename value> void read (value &);
};

template <unsigned valid>
struct foo
{
    int a;
    float b;
    char c;

    auto & get_a () { static_assert (valid & 1); return a; }
    auto & get_b () { static_assert (valid & 2); return b; }
    auto & get_c () { static_assert (valid & 4); return c; }
};

template <unsigned valid>
foo <valid> read_foo (stream & Stream)
{
    if (Stream.read <unsigned> () != valid)
        throw std::runtime_error ("unexpected input");

    foo <valid> Foo;

    if (valid & 1) Stream.read (Foo.a);
    if (valid & 2) Stream.read (Foo.b);
    if (valid & 4) Stream.read (Foo.c);
}

void do_something (stream & Stream)
{
    auto Foo = read_foo <3> (Stream);

    std::cout << Foo.get_a () << ", " << Foo.get_b () << "\n";

    // don't touch c cause it will fail here
    // Foo.get_c ();
}

Это также позволяет шаблонам обрабатывать отсутствующие поля с помощью if constexpr.

template <unsigned valid>
void print_foo (std::ostream & os, foo <valid> const & Foo)
{
    if constexpr (valid & 1)
        os << "a = " << Foo.get_a () << "\n";
    if constexpr (valid & 2)
        os << "b = " << Foo.get_b () << "\n";
    if constexpr (valid & 4)
        os << "c = " << Foo.get_c () << "\n";
}

Другие вопросы по теме