Мы унаследовали старый код, который мы конвертируем в современный 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.
Есть ли стандартное решение этой проблемы, которое мы не рассматривали?
Не совсем понятно, почему вы считаете это недостатком дизайна. Можете ли вы показать на конкретных примерах проблем, которые возникают из-за этого дизайна?
Если вы хотите предотвратить запрос неинициализированного члена, самым простым решением было бы иметь конструктор, который инициализирует все члены. Есть ли причина, по которой он не используется? Если вам нужен более современный подход, кортежи также можно использовать в сочетании с std :: get для доступа к членам, который развертывается во время выполнения.
Если вы не хотите «запрашивать член, которого нет», вы должны определить отдельные классы для каждой возможной комбинации полей (возможно, используя иерархию классов, если это имеет смысл), определить необходимые операции в базовом классе ((чистый) virtual) и реализовать их в производных классах. тогда вам больше не нужны никакие проверки во время выполнения. Но вызовы виртуальных функций могут быть немного медленнее, чем просто использование optional.
это очень похоже на то, как работает google protobuf v2, с его необязательными полями и битовой картой десериализованных полей со значениями по умолчанию для других.
Вариант, вероятно, самый безопасный вариант, но его дизайн выглядит подозрительно.
Вы хотите сказать, что любое использование, скажем, «местоположение с областью» должно быть статически отделено от любого использования «местоположение без области»?
Я не могу найти существенной разницы между дизайном, использующим std ::: option, и дизайном std :: optional. Если вы сгенерируете, возможно, автоматически, все необходимые варианты, вы получите что-то эквивалентное дизайну std :: optional, но более неудобное в использовании.
@ n.m. Плюсы, которые я могу придумать: если вариант потребляется через посетителей, то если заставляет рассматривать все типы. Кроме того, он позволяет использовать отдельные, лучше спроектированные типы вместо гибридного типа. Но это действительно зависит от того, как реальный вариант использования.
@juanchopanza Есть варианты O (2 ^ N), не то, что я бы хотел применить ...
Если структуры десериализованы, как можно проверить это во время компиляции? Если вы хотите удалить избыточные проверки, либо измените геттеры, либо напишите что-нибудь вроде __builtin_unreachable





А как насчет миксинов?
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> не связаны, хотя последний, возможно, является первым из типа.
Сначала я скажу, что иногда мне хотелось иметь "необязательную" опцию класса, когда все члены становятся необязательными. Я думаю, возможно, это было бы возможно без правильного метапрограммирования с использованием кода, подобного 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";
}
Я чувствую, что что-то не так с дизайном этого кода. Сами поля класса не могут быть необязательными, и даже если все они каким-то образом обернуты, весь код, использующий такие структуры, будет загроможден проверкой необязательности.