Skip to content

“Polymorphic” Templates

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!

Leave a Reply

Your email address will not be published. Required fields are marked *