web-dev-qa-db-fra.com

Struct avec des membres optionnels en C++ moderne

Nous avons hérité de l'ancien code que nous convertissons en C++ moderne pour améliorer la sécurité des types, l'abstraction et autres avantages. Nous avons un certain nombre de structures avec de nombreux membres facultatifs, par exemple:

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

Le point important est que tous les membres sont facultatifs. Au moins un sera présent dans une instance donnée de Location, mais pas nécessairement tous. Plus de combinaisons sont possibles que le concepteur original a apparemment trouvé commode d'exprimer avec des structures séparées pour chacune.

Les structures sont désérialisées de cette manière (pseudocode):

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.

Dans l'ancien code, ces indicateurs sont stockés de manière permanente dans la structure et les fonctions getter de chaque membre de la structure les testent au moment de l'exécution pour s'assurer que le membre demandé est présent dans l'instance Location donnée.

Nous n'aimons pas ce système de vérification de l'exécution. Demander à un membre qui n'est pas présent est une grave erreur de logique que nous aimerions trouver au moment de la compilation. Cela devrait être possible, car chaque fois qu’un emplacement est lu, on sait quelle combinaison de variables membres doit être présente.

Au début, nous avons pensé à utiliser std :: optional:

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

Cette solution modernise le défaut de conception plutôt que de le réparer.

Nous avons pensé à utiliser std :: variant comme ceci:

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;
};

Cette solution rend impossible la représentation d'états illégaux, mais ne s'adapte pas correctement lorsque plusieurs combinaisons de variables membres sont possibles. De plus, nous ne voudrions pas y accéder en spécifiant Has_Area_and_Coarse , mais quelque chose de plus proche de loc.fine_position .

Existe-t-il une solution standard à ce problème que nous n’avons pas envisagée?

7
replete

Qu'en est-il des mixins?

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
Yola

Vous pourriez avoir une version de la structure qui fait que la bitmap compile le temps et la vérifie là. Je suppose que pour un morceau de code particulier, vous supposez ce qui est présent. Dans ce code, vous pouvez prendre la version avec le bitmap de temps de compilation. Afin de convertir avec succès une version à mappage au moment de l'exécution en fichier à mapper au moment de la compilation, le mappage est validé.

#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
nate

Si vous savez toujours en lecture ou en construction quels champs seront présents, alors le bit de validité deviendra un argument de modèle et la vérification avec static_assert fonctionnera.

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

Cela permet également aux modèles de traiter les champs manquants à l'aide de 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
nate

Je dirai d’abord que j’ai aussi parfois voulu avoir une "faculté" d’un cours, où tous les membres deviennent facultatifs. Je pense que cela pourrait peut-être être possible sans une métaprogrammation appropriée utilisant un code similaire à celui d'Antony Polukhin magic_get .

Mais quoi qu’il en soit ... Vous pourriez avoir une mappe d’attributs partiellement sécurisée avec des valeurs de type arbitraire:

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

std::any peut contenir n'importe quel type (quelque chose en allouant de l'espace sur la pile, parfois en interne). Face à l'extérieur du type est effacé, mais vous pouvez le restaurer avec une méthode get<T>(). C'est safe dans le sens où vous obtiendrez une exception si vous avez stocké un objet d'un type et essayez de get() un autre type, mais c'est unsafe dans lequel vous n'obtiendrez pas une erreur renvoyée lors de la compilation.

Cela peut être adapté au cas d'attributs arbitraires, au-delà de ceux que vous aviez initialement prévus, par exemple:

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;
}

L'utilisation des attributs pourrait impliquer des fonctions libres qui vérifient la présence d'attributs pertinents.

En pratique, un std::vector serait probablement plus rapide à rechercher que le std::unordered_map.

Mise en garde: Cette solution ne vous donne pas beaucoup du type de sécurité que vous désirez.

1
einpoklum