Stephen Horne

← Back to blog

Published on 08/19/2024 17:10 by Stephen Horne

JSON and the Golden Fleece, Pt 2

https://github.com/sjh87/json-parser/tree/main

To say I’ve been learning a lot would be a huge understatement. Yes, I’ve scrapped the code itself maybe 7 times now, but that all was invaluable in helping me to learn C++ 17 with no prior C++ experience and some C experience. Here’s what my testing code looks like now, capturing the behavior I want for the individual JSON tree node types. I struggled so much trying to figure out how make all the classes representing the JSON types (number, object, null, etc) able to occupy the same vector or map while still keeping their individual identities and member values. When I’d put them in a vector, for example, and tried to perform any kind of operation on them later on, they all would lose their individuality and become instances of the useless base class. I thought for sure I was missing something when pulling the values out and I’d find them useless, but it was how I was storing them that was the problem.

If I want to have a vector or map of types that inherit from a shared base class, I need to type the vector or map as a pointer type. I think this is because the pointers are all the same size, whereas all my derived JSON types all varied wildly in size, from Null, which didn’t have any value stored in it at all, to the Array and Object types which had a vector or map in its bowels of unknown size. With pointers, the vector can be of a predictable footprint regardless of what the pointers point to. I’m now thinking something like this scratch code

class ArrayNode : public ValueNodeBase {
    const Type type; // an enumeration type informing a caller how to cast `value`
    std::unique_ptr<std::vector<std::unique_ptr<ValueNodeBase>>> value;

public:
    ArrayNode() : type(Type::Array), value(std::make_unique<std::vector<std::unique_ptr<ValueNodeBase>>>()) {}

    Type getType() const override {
        return type;
    }

    void* getValue() const override {
        value.get();
    };

    void add(std::unique_ptr<ValueNodeBase> ptr&&) {
        // actually transfers ownership of what `ptr` points to to this classe's `value` pointer object
        // `ptr` will, afterward, point to `nullptr` and can be disposed of safely.
        value->push_back(std::move(ptr));
    }
};

and the base class:

class ValueNodeBase {
    const Type type; // so callers know how to cast getValue()'s returned pointer
    //  tried to make this `const` but the compiler hated it and there's no
    // interface to change it anyway so whatever
    void *value; 

public:
    ValueNodeBase() : type(Type::Empty), value(nullptr) {}

    virtual Type getType() const {
        return type;
    }

    virtual void* getValue() const {
        return value;
    };
};

Making the base classes’s methods virtual lets the correct version be called at runtime, once the pointer to one of the derived classes is dereferenced. Or, at least, I think that’s how it works.

tests.add({ "JSON::NumberNode::getValue() returns pointer to double = 0 when default constructor used", [](){
    const auto instance = JSON::NumberNode();
    if (*(static_cast<double*>(instance.getValue())) != 0) {
        return false;
    }

    return true;
}});

tests.add({ "JSON::NumberNode::getValue() returns pointer to double = 69 when constructor used to set value", [](){
    const auto instance = JSON::NumberNode(69);
    if (*(static_cast<double*>(instance.getValue())) != 69) {
        return false;
    }

    return true;
}});

I wrote these tests to make sure the original method, which always returns a nullptr, was being overridden correctly for the NumberNode class. My hope is that when I put a bunch of these in a vector or map, pointed to by unique_ptrs, and pull them back out and cast them to the type indicated by getType(), the pointer returned by getValue() will point to what I stored there, and not collapse into the original implementation and return nullptr. I guess we’ll see. I use a enum, rather than a series of tried casts, because I imagine it is much, much less expensive to simply check integer equality than to try a bunch of different type casts of the pointer, checking if the resuling pointer is null. This way, the program will only try to cast the pointer to what the enum tells it to. I’ll need to throw an error if that cast fails, too, as that is not a scenario I want to allow.

I am quite proud of my little testing class. It’s going to make this project so much easier:

Cute output from my test runner class

Isn’t that output adorable? It’s definitely my favorite part of this project so far. I hope that changes as I finish these data types and begin the fun part: implementing my parsing and storage algorithm. I think it will work, and be very fast and reliable. I’m very excited about it. I just need to make sure I have a nice data structure to put all those extracted values into.

Thanks for reading this. I had fun writing it. Yakında görüşürüz!

Written by Stephen Horne

← Back to blog