As I’ve started coding in C++ again instead of Rust, I’ve found myself missing Rust’s trait system a lot. I love static type checking; in my opinion, the more bugs the compiler finds, the fewer bugs I have to track down by hand.
I was writing a basic JSON serializer and ran into exactly this problem. I wanted three types – a JsonValue
that would hold either numbers or quoted strings, a JsonObject
and JsonArray
. These would form a sort of tree, with JsonValue
as the leaf nodes and the objects and arrays as internal nodes.
For valid type checking, I wanted to ensure that the only values that could be inserted into a JsonObject
or JsonArray
were valid JSON data types. My first instinct was to reach for polymorphism, constructing a JsonBase
interface:
class JsonBase {
virtual std::string str() = 0;
};
Then, all my JSON data types could inherit from JsonBase
, and when I wanted to insert a JSON data type into an object or array, all I’d have to do is downcast to a JsonBase
.
The problem was that when data is added to an array or object, the string representation (that str()
function) is retrieved. That means I have an additional v-table lookup every single time anything gets added to a JSON object or array…and since this serialization code is used in logging, it’s entirely possible that these v-table lookups will find their way into a hot path and have a noticeable impact.
I don’t really need v-table lookups, though – I always know exactly what type is being added to an array or object! I have no functions that return a general data type, so the real power and usefulness of dynamic polymorphism and RTTI is wasted.
Deciding against polymorphism for now, I turned my gaze towards templates. C++’s templating engine is fairly powerful (and reminiscent of how I would solve this problem in Rust, using traits) – so I figured it would do fine. And it does! There’s no v-table lookups, just three copies of the functions, which is a negligible impact.
The problem is that now there’s no type-checking. I could construct an unrelated class with a str()
method, and it could be added to my arrays and objects!
Instead, I stumbled upon a pattern I may end up re-using elsewhere. For now I’m calling it polymorphic templating, and although it’s not quite as concise as true polymorphism, it’s a good approximation.
I create a private templated function that contains all the logic for adding elements to JSON arrays/objects. Then, I write public, inline functions that simply wrap the templated function. These wrapper functions are overloaded for each possible JSON data type. Since I always know the type of data that I’m adding to an array or object, the compiler knows to call the appropriate function – and if someone tries to add an invalid data type type to an object or array, they run into an error.
class JsonArray
{
private:
// ...
template<typename T> JsonArray*
push_(T const& value)
{
// ...
}
public:
inline JsonArray*
push(JsonValue const& value)
{
return push_(value);
}
inline JsonArray*
push(JsonObject const& value)
{
return push_(value);
}
inline JsonArray*
push(JsonArray const& value)
{
return push_(value);
}
};
This isn’t the most concise solution – it’s got more LOC than either the templated function or the polymorphic solution. However, it does give me type-safe template restrictions, so until I find a better solution (aside from “convert your entire codebase to Rust), I think it’s the best available option.
That said, I’d love to hear any alternative ideas!