RavenDB C++ client: Laying the ground work
The core concept underlying the RavenDB client API is the notion of Unit of Work. This provide core features such as change tracking and identity map. In all our previous clients, that was pretty easy to deal with, because the GC solved memory ownership and reflection gave us a lot of stuff basically for free.
Right now, what I want to achieve is the following:
It seems pretty simple, right? Because both the session and the caller code are going to share ownership on the passed User. Notice that we modify the user after we call store() but before save_changes(). We expect to see the modification in the document that is being generated.
The memory ownership is handled here by using shared_ptr as the underlying mode in which we accept and return data to the session. Now, let’s see how we actually deal with serialization, shall we? I have chosen to use nlohmann’s json for the project, which means that the provided API is quite nice. As a consumer, you’ll need to write your JSON serialization code, but it is fairly obvious how to do so, check this out:
Given that C++ doesn’t have reflection, I think that this represent a really nice handling of the issue. Now, how does this play with everything else? Here is what the skeleton of the session looks like:
There is a whole bunch of stuff that is going on here that we need to deal with.
First, we have the IEntityDetails interface, which is non generic. It is implemented by the generic class EntityDetails, which has the actual type that we are using and can then use the json serialization we defined to convert the entity to JSON. The rest are just details, we need to use a vector of shared_ptr, instead of the abstract class, because the abstract class has no defined size.
The generic store() method just capture the generic type and store it, and the rest of the code can work with the non generic interface.
I’m not sure how idiomatic this code is, or how performant, but at least as a proof of concept, it works to show that we can get a really good interface to our users in C++.
Comments
It's always difficult to comment on parts of code without knowing the whole thing, but randomly assuming there's nothing in the other stuff that's material, I've noticed a few things you could improve. Idiomatically it's not bad, just a few missing details.
Notice the extra std::move - this eliminates a pointless copy of the string. There's quite a few other places. The general rule is that if you don't need the source object anymore, std::move the last usage. This is particularly good for functions which only reference things one time (as in the above).
For the entity details, you can make life considerably simpler on yourself by using
std::function
. One of the things people often miss when coming from other languages is how much C++ lambdas can really simplify things. Consider for example:In this case the
std::function
can perform small buffer optimisation, eliminating the need for a heap reference. Another benefit is that it's a lot less verbose and keeps everything by value, which is more idiomatic. It's a bit less pleasant if you're trying to keep multiple functions that way though. Even if you decide to stick with the interface (which is not an invalid decision; depends a lot on details not shown here) use aunique_ptr
, not ashared_ptr
. There's no need for refcounting here.In fact, the details of keeping a reference to the entity are arguably poor here as well. I've been thinking about how to handle figuring out how to handle serialising the entity, and I think it would be best to just take the serialisation function as the argument to store in the first place. Then the user doesn't need to keep their entities on the heap if they don't need to, and you don't have to worry about ADL or interfaces for figuring out how to serialise.
For example, class Session { std::vector<std::function<std::string()>> items;
Mark, Thank you very much for your feedback. The
std::function
is very nice, although I have to admit that coming from C#'s background, I'm very wary of capturing labmdas. This looks like it would be a really nice behavior here.With regards to the
shared_ptr
, I'm using it there explicitly because the lifetime of the entity is no scoped to either the session or the caller lifetime, but the larger of the two. Sinceunique_ptr
owns the reference, but I need at least two people to actually share ownership, I don't think it would be appropriate.Mark, Something like
void store(std::function<std::string()> serialise)
is explicitly not something that I want to do. In particular, because it mess up the interface that we expose to the user significantly. Consider:session.store([&] { return to_json(user); });
vs.session.store(user);
I would much rather have the later.If you used an interface for the contents of
items
, those objects being held there are not the lifetime of the entity, though. That is only the lifetime of the Session, sounique_ptr
can be appropriate there.The advantage of the approach I have suggested where you simply take the serialisation function directly is that the user can choose how the lifetime works. They can use a shared approach as you have used here, but they can also use a fixed lifetime approach as I have shown- or indeed, any other lifetime that works for them.
One of the things I found in the C++ community is that there is always a part who will go nuts over the slightest heap allocation or any perceived inefficiency, even if in reality it's totally meaningless (e.g. a couple extra allocations per entity). APIs that allow them to avoid allocating on the heap if possible tend to be more successful.
Mark, I'm actually fine with having overloads of this function. So you can either pass the entity, or pass the serialization function :-)
Mark, Actually, things are a bit more complex. Consider the following scenario:
In this case,
user
anduser2
must point to the same location. So we do need the actual instance.Comment preview