Here's one way to solve the serialization problem in c++20, using a json library:
#include <functional>
#include <unordered_map>
#include "json.hpp"
namespace nlm = nlohmann;
class properties
{
struct property_info
{
std::function<void(nlm::json const&)> deserializor;
std::function<nlm::json()> serializor;
};
std::unordered_map<std::string_view, property_info> reg_;
public:
//
nlm::json state() const;
void state(nlm::json const&);
//
template <typename U, typename ...A>
auto register_property(std::string_view const& k, U& v, A&& ...a)
{
static_assert(!(sizeof...(a) % 2));
static_assert(!(std::is_const_v<U>));
reg_.try_emplace(k,
[&v](nlm::json const& j){v = j.get<U>();},
[&v]{return nlm::json(v);}
);
if constexpr (sizeof...(a))
{
register_property(std::forward<A>(a)...);
}
return [this](auto&& ...a)
{
return register_property(std::forward<decltype(a)>(a)...);
};
}
auto get(std::string_view const& k)
{
return reg_.find(k)->second.serializor();
}
template <typename U>
void set(std::string_view const& k, U&& v)
{
reg_.find(k)->second.deserializor(std::forward<U>(v));
}
};
nlm::json properties::state() const
{
nlm::json r(nlm::json::object());
for (auto i(reg_.cbegin()), cend(reg_.cend()); cend != i; i = std::next(i))
{
r.emplace(i->first, i->second.serializor());
}
return r;
}
void properties::state(nlm::json const& e)
{
assert(e.is_object());
auto const cend(reg_.cend());
for (auto i(e.cbegin()), ecend(e.cend()); ecend != i; i = std::next(i))
{
auto& key(i.key());
if (auto const j(std::as_const(reg_).find(key)); cend != j)
{
j->second.deserializor(i.value());
}
}
}
Example:
int main()
{
struct S: properties
{
bool b{};
int i{};
S()
{
register_property("b", b)("i", i);
}
} s;
s.set("b", true);
s.set("i", 11.1);
std::cout << s.get("b") << std::endl;
std::cout << s.state() << std::endl;
}
2 functors for (de)serializing are generated for each registered property. If state is requested or set, these are executed accordingly. Obvious improvements are certain checks, getters/setters, instead of references. I think this is a nice quick solution for simple cases.
-
\$\begingroup\$ The text says C++17, which contradicts with the C++20 tag. Which one is intended? \$\endgroup\$L. F.– L. F.2020年09月13日 12:26:43 +00:00Commented Sep 13, 2020 at 12:26
-
\$\begingroup\$ Please do not add new solutions to your question, doing so goes against the Question + Answer style of Code Review. This is not a forum where you should keep the most updated version in your question. As such I have rolled back your latest edit. Please see what you may and may not do after receiving answers . \$\endgroup\$Peilonrayz– Peilonrayz ♦2020年09月13日 22:12:38 +00:00Commented Sep 13, 2020 at 22:12
-
\$\begingroup\$ github.com/user1095108/properties \$\endgroup\$user1095108– user10951082020年09月14日 20:32:12 +00:00Commented Sep 14, 2020 at 20:32
2 Answers 2
Avoid creating namespace aliases in header files
I assume that at least the declaration of class properties
would be put in a header file. In that case, consider that users of that header file might not expect namespace nlm
to be declared, so I recommend just writing out nlohmann
fully.
Spelling
A minor issue: it is serializer
, not serializor
.
Function names
Avoid overloading state()
to mean either setting the state or getting the state. While related, these are different operations, and it is much better to make that explicit by giving them different function names. An obvious modification is to name them get_state()
and set_state()
, but that sounds quite generic. I would also make it explicit that you are converting to or from JSON, so consider naming them to_json()
and from_json()
.
Overhead
Your serialization method introduces a huge overhead. Every instance of a serializable struct now has to contain a std::unordered_map
, which is filled in in the constructor. So this costs time and memory. It would be much nicer if you could build this only once per type that derives from properties
. Perhaps it can be done using static variables and CRTP, something like:
template<typename T>
struct properties
{
struct registry
{
// keeps the actual mapping
...
};
template <typename U>
void set(str::string_view const& k, U&& v) {
// forward it to the registry object, along with a pointer to the object
auto self = static_cast<T *>(this);
self->registry.set(self, k, v);
}
...
};
struct S: properties<S>
{
bool b{};
int i{};
static properties::registry reg_;
public:
...
};
S::properties::registry S::reg_ = {{"b", &S::b}, {"i", &S::i}};
But I struggle myself with how to create a constructor for properties::registry
that would allow the above code (especially the last line) to work.
Make get()
const
You should make the get()
member function const
, as it should not modify the state, and this will allow that functions to be used on const
instances of classes that inherit from properties
.
Use range-for
where possible
You can simplify the code in some places by using range-for
. For example, in properties::state()
, where you can also combine it with structured binding:
for (auto &[name, variable]: reg_)
{
r.emplace(name, variable.serializer());
}
It is unfortunate that the iterator of nlm::json
doesn't work the same way; you can only access the value in a range-for
, not the key.
Crash at runtime when accessing a non-existing property
If in main()
, you call s.get("x")
, the program crashes with a segmentation fault. Even if you never expect this function to be called with a user-supplied name, it still makes it hard to debug programming errors. Check the return value of calls to find()
before trying to dereference the result. You could throw a std::runtime_error
if find()
return nulltpr
, or if you don't want to use exceptions or pay for the performance cost in production builds, at least use assert()
to help with debug builds.
-
\$\begingroup\$ The "or" not "er" is there because we're dealing with functors. Yeah,
const
s can be placed to. even more places, as far as the constructor goes, no, that's not possible like you've had in mind. The overhead is huge, but the approach is also very convenient, for a gui it might be ok. \$\endgroup\$user1095108– user10951082020年09月13日 15:31:25 +00:00Commented Sep 13, 2020 at 15:31 -
1\$\begingroup\$ @user1095108 Neither
set()
norget()
areconst
functions in the code you posted. Andset()
should of course not beconst
at all. \$\endgroup\$G. Sliepen– G. Sliepen2020年09月13日 16:03:21 +00:00Commented Sep 13, 2020 at 16:03 -
\$\begingroup\$
set()
could be qualified asconst
, that was my point. It just looks up a functor and forwards into it. \$\endgroup\$user1095108– user10951082020年09月13日 16:59:57 +00:00Commented Sep 13, 2020 at 16:59
Now without a map:
#include <cassert>
#include <functional>
namespace nlm = nlohmann;
class properties
{
using serializor_t = std::function<nlm::json()>;
using deserializor_t = std::function<void(nlm::json)>;
struct property_info
{
std::string_view k;
serializor_t serializor;
deserializor_t deserializor;
};
std::function<property_info const*(
std::function<bool(property_info const&)>
)> visitor_;
public:
virtual ~properties() = default;
//
nlm::json state() const;
void state(nlm::json const&) const;
//
template <std::size_t I = 0, typename A = std::array<property_info, I>, typename U>
auto register_property(std::string_view k, U&& u, A&& a = {})
{
std::array<property_info, I + 1> b;
std::move(a.begin(), a.end(), b.begin());
if constexpr (std::is_invocable_v<U>)
{
*b.rbegin() = {
std::move(k),
[=]()noexcept(noexcept(u()))->decltype(auto){return u();},
{}
};
}
else if constexpr (std::is_lvalue_reference_v<U>)
{
if constexpr (std::is_const_v<std::remove_reference_t<U>>)
{
*b.rbegin() = {
std::move(k),
[&]()noexcept->decltype(auto){return u;},
{}
};
}
else
{
*b.rbegin() = {
std::move(k),
[&]()noexcept->decltype(auto){return u;},
[&](auto&& j){u = j.template get<std::remove_cvref_t<U>>();}
};
}
}
return [this, b(std::move(b))](auto&& ...a) mutable
{
if constexpr (bool(sizeof...(a)))
{
return register_property<I + 1>(std::forward<decltype(a)>(a)...,
std::move(b));
}
else
{
visitor_ = [b(std::move(b)), c(std::move(visitor_))](auto f)
noexcept(noexcept(f({})))
{
for (auto& i: b)
{
if (f(i))
{
return &i;
}
}
return c ? c(std::move(f)) : typename A::const_pointer{};
};
}
};
}
template <std::size_t I = 0, typename A = std::array<property_info, I>,
typename U, typename V,
std::enable_if_t<
std::is_invocable_v<U> &&
std::is_invocable_v<V, decltype(std::declval<U>()())>,
int
> = 0
>
auto register_property(std::string_view k, U&& u, V&& v, A&& a = {})
{
std::array<property_info, I + 1> b;
std::move(a.begin(), a.end(), b.begin());
*b.rbegin() = {
std::move(k),
[=]()noexcept(noexcept(u()))->decltype(auto){return u();},
[=](auto&& j){v(std::forward<decltype(j)>(j));}
};
return [this, b(std::move(b))](auto&& ...a) mutable
{
if constexpr (bool(sizeof...(a)))
{
return register_property<I + 1>(std::forward<decltype(a)>(a)...,
std::move(b));
}
else
{
visitor_ = [b(std::move(b)), c(std::move(visitor_))](auto f)
noexcept(noexcept(f({})))
{
for (auto& i: b)
{
if (f(i))
{
return &i;
}
}
return c ? c(std::move(f)) : typename A::const_pointer{};
};
}
};
}
//
nlm::json get(std::string_view const&) const;
template <typename U>
auto set(std::string_view const& k, U&& u) const
{
if (auto const pi(visitor_([&](auto& pi) noexcept
{
return pi.k == k;
})); pi && pi->deserializor)
{
pi->deserializor(std::forward<U>(u));
}
return [&](auto&& ...a)
{
return set(std::forward<decltype(a)>(a)...);
};
}
};
nlm::json properties::get(std::string_view const& k) const
{
if (auto const pi(visitor_([&](auto& pi) noexcept
{
return pi.k == k;
})); pi)
{
return pi->serializor();
}
else
{
return nlm::json();
}
}
nlm::json properties::state() const
{
nlm::json r(nlm::json::object());
visitor_([&](auto& pi)
{
r.emplace(pi.k, pi.serializor());
return false;
}
);
return r;
}
void properties::state(nlm::json const& e) const
{
assert(e.is_object());
for (auto i(e.cbegin()), ecend(e.cend()); ecend != i; i = std::next(i))
{
auto&& k(i.key());
if (auto const pi(visitor_([&](auto& pi) noexcept
{
return pi.k == k;
})); pi && pi->deserializor)
{
pi->deserializor(i.value());
}
}
}
int main()
{
struct S: properties
{
bool b{};
int i{};
S()
{
register_property("b", b)("i", i)("joke",[]{return "just a joke";})();
}
} s;
s.set("b", true)("i", 11.1);
std::cout << s.get("b") << std::endl;
std::cout << s.state() << std::endl;
}
This is generative programming in action. We generate a lambda for traversing over all property infos. We could just as well have generated a data structure (such as an array, a tuple, ...), but the type of these is unknown in advance, so we would need some type-erasure approach to interpret and store this data. This means we would not be able to avoid generating a functor, that would "know", what the generated data-structure was and how/where it was stored.