(de) Serialisierung von c ++ - Objekten fast automatisch
Hier ist eine Möglichkeit, das Serialisierungsproblem in c ++ 20 mithilfe einer JSON- Bibliothek zu lösen :
#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());
}
}
}
Beispiel:
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;
}
Für jede registrierte Eigenschaft werden 2 Funktoren für die (De-) Serialisierung generiert. Wenn der Status angefordert oder festgelegt wird, werden diese entsprechend ausgeführt. Offensichtliche Verbesserungen sind bestimmte Überprüfungen, Getter / Setter anstelle von Referenzen. Ich denke, dies ist eine schöne schnelle Lösung für einfache Fälle.
https://wandbox.org/permlink/J0vdLOTp5x6xvF9s
Antworten
Vermeiden Sie das Erstellen von Namespace-Aliasen in Header-Dateien
Ich gehe davon aus, dass zumindest die Deklaration von class propertiesin eine Header-Datei eingefügt wird. In diesem Fall sollten Sie berücksichtigen, dass Benutzer dieser Header-Datei möglicherweise nicht damit rechnen namespace nlm, deklariert zu werden. Ich empfehle daher, nur nlohmannvollständig zu schreiben .
Rechtschreibung
Ein kleines Problem: ist es serializernicht serializor.
Funktionsnamen
Vermeiden Sie Überlastung state(), um entweder den Status festzulegen oder den Status abzurufen. Obwohl dies verwandt ist, handelt es sich um unterschiedliche Operationen, und es ist viel besser, dies explizit zu machen, indem Sie ihnen unterschiedliche Funktionsnamen geben. Eine offensichtliche Modifikation besteht darin, sie zu benennen get_state()und set_state(), aber das klingt ziemlich allgemein. Ich würde es auch deutlich machen , dass Sie zu oder von JSON konvertieren, so betrachten sie die Benennung to_json()und from_json().
Overhead
Ihre Serialisierungsmethode verursacht einen enormen Overhead. Jede Instanz einer serialisierbaren Struktur muss jetzt eine enthalten std::unordered_map, die im Konstruktor ausgefüllt ist. Das kostet also Zeit und Speicher. Es wäre viel schöner, wenn Sie dies nur einmal pro Typ erstellen könnten, der von abgeleitet ist properties. Vielleicht kann es mit statischen Variablen und CRTP gemacht werden , so etwas wie:
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}};
Aber ich habe Mühe, einen Konstruktor dafür zu erstellen properties::registry, damit der obige Code (insbesondere die letzte Zeile) funktioniert.
Machen get() const
Sie sollten die get()Member-Funktion festlegen const, da sie den Status nicht ändern sollte. Dadurch können diese Funktionen für constInstanzen von Klassen verwendet werden, von denen geerbt wird properties.
Verwenden Sie nach forMöglichkeit die Reichweite
Sie können den Code an einigen Stellen vereinfachen, indem Sie range- verwenden for. Zum Beispiel in properties::state(), wo Sie es auch mit strukturierter Bindung kombinieren können:
for (auto &[name, variable]: reg_)
{
r.emplace(name, variable.serializer());
}
Es ist bedauerlich, dass der Iterator von nlm::jsonnicht auf die gleiche Weise funktioniert. Sie können nur auf den Wert in einem Bereich zugreifen for, nicht auf den Schlüssel.
Absturz zur Laufzeit beim Zugriff auf eine nicht vorhandene Eigenschaft
Wenn main()Sie anrufen s.get("x"), stürzt das Programm mit einem Segmentierungsfehler ab. Selbst wenn Sie nie erwarten, dass diese Funktion mit einem vom Benutzer angegebenen Namen aufgerufen wird, ist es dennoch schwierig, Programmierfehler zu debuggen. Überprüfen Sie den Rückgabewert von Aufrufen an, find()bevor Sie versuchen, das Ergebnis zu dereferenzieren. Sie können eine std::runtime_errorif- find()Rückgabe nulltprauslösen oder, wenn Sie keine Ausnahmen verwenden oder die Leistungskosten in Produktions-Builds bezahlen möchten, zumindest assert()zur Unterstützung bei Debug-Builds verwenden.
Jetzt ohne Karte:
#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;
}
Dies ist generative Programmierung in Aktion. Wir generieren ein Lambda zum Durchlaufen aller Immobilieninfos. Wir hätten genauso gut eine Datenstruktur (wie ein Array, ein Tupel, ...) generieren können, aber der Typ dieser ist im Voraus unbekannt, sodass wir einen Typ-Lösch-Ansatz benötigen würden, um diese Daten zu interpretieren und zu speichern. Dies bedeutet, dass wir es nicht vermeiden können, einen Funktor zu generieren, der "weiß", wie die generierte Datenstruktur war und wie / wo sie gespeichert wurde.