5
\$\begingroup\$

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.

https://wandbox.org/permlink/J0vdLOTp5x6xvF9s

\$\endgroup\$
3
  • \$\begingroup\$ The text says C++17, which contradicts with the C++20 tag. Which one is intended? \$\endgroup\$
    – L. F.
    Commented 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
    Commented Sep 13, 2020 at 22:12
  • \$\begingroup\$ github.com/user1095108/properties \$\endgroup\$ Commented Sep 14, 2020 at 20:32

2 Answers 2

3
\$\begingroup\$

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.

\$\endgroup\$
3
  • \$\begingroup\$ The "or" not "er" is there because we're dealing with functors. Yeah, consts 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\$ Commented Sep 13, 2020 at 15:31
  • 1
    \$\begingroup\$ @user1095108 Neither set() nor get() are const functions in the code you posted. And set() should of course not be const at all. \$\endgroup\$
    – G. Sliepen
    Commented Sep 13, 2020 at 16:03
  • \$\begingroup\$ set() could be qualified as const, that was my point. It just looks up a functor and forwards into it. \$\endgroup\$ Commented Sep 13, 2020 at 16:59
0
\$\begingroup\$

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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.