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_ptr
s, 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:
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