(des) serializando objeto c ++ quase de forma automática
Esta é uma maneira de resolver o problema de serialização em c ++ 20, usando uma biblioteca json :
#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());
}
}
}
Exemplo:
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 functores para (des) serialização são gerados para cada propriedade registrada. Se o estado for solicitado ou definido, eles serão executados de acordo. Melhorias óbvias são certas verificações, getters / setters, ao invés de referências. Eu acho que esta é uma boa solução rápida para casos simples.
https://wandbox.org/permlink/J0vdLOTp5x6xvF9s
Respostas
Evite criar apelidos de namespace em arquivos de cabeçalho
Presumo que pelo menos a declaração de class propertiesseria colocada em um arquivo de cabeçalho. Nesse caso, considere que os usuários desse arquivo de cabeçalho podem não esperar namespace nlmser declarados, portanto, recomendo apenas escrever por nlohmanncompleto.
Ortografia
Um problema menor: é serializer, não serializor.
Nomes de funções
Evite sobrecarregar state()para significar definir o estado ou obter o estado. Embora relacionadas, essas são operações diferentes e é muito melhor tornar isso explícito, dando-lhes nomes de função diferentes. Uma modificação óbvia é nomeá-los get_state()e set_state(), mas isso soa bastante genérico. Eu também deixaria explícito que você está convertendo de ou para JSON, então considere nomear to_json()e from_json().
A sobrecarga
Seu método de serialização apresenta uma grande sobrecarga. Cada instância de uma estrutura serializável agora deve conter um std::unordered_map, que é preenchido no construtor. Portanto, isso custa tempo e memória. Seria muito melhor se você pudesse construir isso apenas uma vez por tipo que deriva de properties. Talvez isso possa ser feito usando variáveis estáticas e CRTP , algo como:
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}};
Mas eu me esforço para criar um construtor properties::registryque permita que o código acima (especialmente a última linha) funcione.
Faço get() const
Você deve fazer a get()função de membro const, pois ela não deve modificar o estado, e isso permitirá que as funções sejam usadas em constinstâncias de classes que herdam de properties.
Use o alcance - forquando possível
Você pode simplificar o código em alguns lugares usando range- for. Por exemplo, em properties::state(), onde você também pode combiná-lo com vinculação estruturada:
for (auto &[name, variable]: reg_)
{
r.emplace(name, variable.serializer());
}
É uma pena que o iterador de nlm::jsonnão funcione da mesma maneira; você só pode acessar o valor em um intervalo for, não a chave.
Falha em tempo de execução ao acessar uma propriedade não existente
Se em main(), você liga s.get("x"), o programa falha com uma falha de segmentação. Mesmo que você nunca espere que essa função seja chamada com um nome fornecido pelo usuário, ainda assim será difícil depurar erros de programação. Verifique o valor de retorno das chamadas para find()antes de tentar cancelar a referência do resultado. Você pode lançar um std::runtime_errorif find()return nulltpr, ou se não quiser usar exceções ou pagar pelo custo de desempenho em compilações de produção, pelo menos use assert()para ajudar com compilações de depuração.
Agora sem um mapa:
#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;
}
Esta é a programação generativa em ação. Geramos um lambda para percorrer todas as informações de propriedade. Poderíamos muito bem ter gerado uma estrutura de dados (como uma matriz, uma tupla, ...), mas o tipo delas é desconhecido de antemão, portanto, precisaríamos de alguma abordagem de eliminação de tipo para interpretar e armazenar esses dados. Isso significa que não poderíamos evitar a geração de um functor, que "saberia" qual era a estrutura de dados gerada e como / onde foi armazenada.