Struct with optional members in modern C ++

We inherited the old code, which we translate into modern C ++, to get better security, abstraction and other useful properties. We have several structures with many optional members, for example:

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

The important point is that all members are optional. At least one will be present at any given location, but not necessarily all. More combinations are possible than the original developer, which is apparently convenient for expression with separate structures for each.

Structures are deserialized in this way (pseudo-code):

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.

, getter , , 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.

, ?

+6
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;
+4

, "" , . , , , , , Antony Polukhin 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.

. , .

+1

, . , , . . - - , - .

#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!");
    }
};
+1

If you always know the time of reading or building, which fields will be present, then make the bit of validity a template argument and working with static_assertwill work.

#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 ();
}

It also allows templates to handle missing fields with 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";
}
+1
source

All Articles