We have inherited old code which we are converting to modern C++ to gain better type safety, abstraction, and other goodies. We have a number of structs 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 of the members are optional. At least one will be present in any given instance of Location, but not necessarily all. More combinations are possible than the original designer apparently found convenient to express with separate structs for each.
The structs are deserialized in this manner (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.
In the old code, these flags are stored in the struct permanently, and getter functions for each struct member test these flags at runtime to ensure the requested member is present in the given instance of Location.
We don't like this system of runtime checks. Requesting a member that isn't present is a serious logic error that we would like to find at compile time. This should be possible because whenever a Location is read, it is known which combination of member variables should be present.
At first, we thought of using std::optional:
struct Location {
std::optional<int> area;
std::optional<QPoint> coarse_location;
// etc.
};
This solution modernizes the design flaw rather than fixing it.
We thought of using std::variant like this:
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;
};
This solution makes illegal states impossible to represent, but doesn't scale well, when more than a few combinations of member variables are possible. Furthermore, we would not want to access by specifying Has_Area_and_Coarse, but by something closer to loc.fine_position.
Is there a standard solution to this problem that we haven't considered?