In Loksim a rail track can have a lot of associated properties. Properties like maximum speed, signals, illumination characteristics, name, stops and so on, which can be seen in the following screenshot:
So far the internal implementation of this system heavily depends on constants. For example, if you want to get the maximum speed on a track ‘t’ at position ‘pos’ you would use a function call similar to this:
auto& propSpeed = t.GetProperty(PROPERTY_MAXIMUM_SPEED, pos);
However, ‘p’ does not contain the actual maximum speed: A maximum speed is not a single value, it e.g. contains information if this maximum speed is different for trains with tilt technology or if it is shown in the timetable of the train operator. So a second query – again using a constant – is needed:
float maxSpeed = propSpeed.GetPropertyFloat(PROPERTY_MAXIMUM_SPEED_NORMAL_TRAIN);
This also shows, that one has to know the data type of the actual property when retrieving it. In fact, at the moment all these properties are stored as strings and converted to the requested type when they are queried by the GetPropertyType functions.
This system has two main advantages:
- It can be extended quite easily: One just has to introduce a new constant PROPERTY_XXX
- It can be serialized and deserialized to XML in a straightforward way: The values are just written out using the PROPERTY_XXX constants as attribute names
However, working with this system for some years also showed some disadvantages:
- There does not exist any static check if the property that is queried really makes sense. E.g. in the example above, one could easily write the line propSpeed.GetPropertyString(PROPERTY_STOP_NAME). The compiler can never recognize this error and you would just get an empty property (string) back
- It can be hard to find the appropriate constant for a property of a rail track. In fact I often end up checking the name in the GUI, searching for it in GUI resource file, than searching for the resource identifier in the code of the editor. There you find the connection between the constant and the GUI name, since there exists some kind of data binding between the GUI and the properties store
- There exists no IntelliSense that could help to find the existing properties.
- The data values are converted when they are retrieved and not stored in the original format
One additional characteristic of the system is, that there exist properties which are the same for both directions (e.g. illumination of the rail track) and other ones which are different depending if you are driving forwards or backwards on the track (e.g. stops and names). So far the developer has to know which properties are valid for both directions and which ones depend on the direction on the track. The GetProperty funktion takes an additional argument which determines if the property for the forward or for the backward direction is retrieved.
In the process of rewriting the system for generation of 3D models for the rail tracks I need to access some of these properties. I decided this would be a good occasion to experiment with a new system for such properties. My main design goals were the following:
- Design system in a way, that the compiler can check for invalid usage of properties and allow the IDE to offer IntelliSense
- Available properties and the types of the properties should be represented in some way in the source code
- Static differentation between properties that are the same in both directions and ones where different properties for both directions exist
- Values should be stored in a typesafe way and not needed to be converted each time properties are accessed
- Allow fast access and filtering of properties, which means the property lists should store the properties by value and not allocate space on the heap for each single property
First of all I decided to use structures to store the different types of properties. Since every property has at least a position (relative to the track it is associated with), I created a base class for all different kind of properties:
template <bool TBothDirections> struct RailProperty { static const bool BothDirections = TBothDirections; RailProperty() : RailProperty(0.0f * units::meter) { } explicit RailProperty(units::distance_m position) : position_(position) { } units::distance_m position_; };
The template parameter TBothDirections is used to differentiate between properties that are valid for both directions and one where different properties for forward and backward exist.
Each type of property is a subclass of the RailProperty struct. In the following two examplary properties are shown:
struct PropLimit : public RailProperty<true> { PropLimit(units::distance_m position, l3d::units::speed_kph limitNormal = units::speed_kph::from_value(100.0f)); PropLimit(units::distance_m position, l3d::units::speed_kph limitNormal, l3d::units::speed_kph limitNeigetechnik); units::speed_kph limitNormal_; units::speed_kph limitNeigetechnik_; }; struct PropUeberhoehung : public RailProperty<false> { PropUeberhoehung(/* ctor parameters */); // Properties };
So how can these properties now be accessed? The user of the interface accesses them through a RailDescription class, which stores information about one rail. Each type of property is stored in an own custom list class. The lists itself are organized in a map[1], where std::type_index is used as a key. The type_index class is new in C++11, can be constructed from std::type_info (which is returned by the typeid operator) and can be used in (hash) maps.
The RailDescription class contains a Properties() function which provides access to a list of properties of the specified type. Since the type of the property is given as a template parameter, the concrete type of the return value is determined at compile time and can be used by IDEs for IntelliSense.
class RailDescription { public: /* other stuff */ template <class Prop> railprops::PositionedPropertyList<Prop>& Properties() { auto& l = propLists_[typeid(Prop)]; if (l.get() == nullptr) { l = std::make_unique<railprops::PositionedPropertyList<Prop>>(); } return static_cast<railprops::PositionedPropertyList<Prop>&>(*l); } private: std::map<std::type_index, std::unique_ptr<railprops::PositionedPropertyListBase>> propLists_; /* other stuff */ };
So what about the PositionedPropertyList class?
template <class Prop> class PositionedPropertyList : public std::conditional<Prop::BothDirections, PositionedPropertyListWithDir<Prop>, PositionedPropertyListWithoutDir<Prop>>::type { };
This class has only one purpose: If the properties that are stored in the list are handled differently for the two directions, PositionedPropertyList is a subclass of PositionedPropertyListWithDir, otherwise of PositionedPropertyListWithoutDir
The PositionedPropertyListWithDir class is responsible to provide two lists itself: One for the forward and one for the backward direction:
template <class Prop> class PositionedPropertyListWithDir : public PositionedPropertyListBase { public: PositionedPropertyListWithDir(); // forward PositionedPropertyListWithoutDir<Prop>& Fwd() { return *forwards_; } // backwards PositionedPropertyListWithoutDir<Prop>& Bwd() { return *backwards_; } private: std::unique_ptr<PositionedPropertyListWithoutDir<Prop>> forwards_; std::unique_ptr<PositionedPropertyListWithoutDir<Prop>> backwards_; };
The class in which the properties are actually stored is the PositionedPropertyListWithoutDir class:
template <class Prop> class PositionedPropertyListWithoutDir : public PositionedPropertyListBase { public: // Property with position_ <= position // Returns nullptr if there does not exist such property const Prop* At(units::distance_m position) const { auto it = std::upper_bound(cbegin(orderedProps_), cend(orderedProps_), position); if (it != cbegin(orderedProps_)) { return &(*(it - 1)); } else { return nullptr; } } /* Implementation of functions omitted */ Prop* At(units::distance_m position); Prop& Add(const Prop& v); Prop& Emplace(units::distance_m position); template<class... Args> Prop& Emplace(units::distance_m position, Args&&... args); bool DeleteAt(units::distance_m position); private: std::vector<Prop> orderedProps_; };
In this class the properties are stored in a std::vector and a handful of methods to retrieve, add new and delete properties exist. For some use cases probably more advanced methods to retrieve a range of properties are needed, which are not yet implemented.
In the end, the user can access the properties in the following way:
RailDescription rd; rd.Properties<PropLimit>().Fwd().Add(PropLimit(10.0f * l3d::units::meter, 100.0f * units::kilometer_per_hour)); rd.Properties<PropLimit>().Fwd().Emplace(50.6f * l3d::units::meter, 100.0f * units::kilometer_per_hour); auto& p1 = rd.Properties<PropLimit>().Fwd().At(4.0f * l3d::units::meter) rd.Properties<PropUeberhoehung>().Emplace(100.0f * units::meter, UeberhoehungType::FesterWert, 45.0f * units::degrees); auto& a = rps.Properties<PropUeberhoehung>().At(100.0f * l3d::units::meter)->angle_;
Everything is type checked at compile time and the IDE can provide full IntelliSense. I always knew that C++ templates are a real nice technique to work with. However, I really had a lot of fun creating this system for storing properties. I still think that it offers a nice interface forusers of this part of the code and can be easily used even by people who do not know anything about the way it is implemented. The main reason for this is, that it is statically typed and therefore prevents a lot of careless mistakes.
So far I did not think a lot about serialization of this new property store. I have a few options in mind, but at the moment I do not need it and writing code that is not needed at the moment is… unnecessary 😉
[1] Accoring to a talk by Chandler Carruth both std::map and std::unordered_map are not really optimal data structures. Maybe I will exchange the usage of std::map with a better map sometime, but this is not really relevant for this post.