-
-
Notifications
You must be signed in to change notification settings - Fork 6.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Custom type registration : instrusive API #2175
Comments
By the way, if you look at those implementations, they are doing some crazy macro expansions which i'm not brave enough to undertake. But the end result is great, as you can see above. Super clean and easy. It's almost providing compile time reflection in C++. I know this repo isn't a free code service, but if one is looking for suggestions, here is one. |
I understand the idea. Do I assume correctly that you would assume a "canonic" serialization to {
"name": "a name value",
"address": "an address value",
"age": 42
} |
I'm not sure what you mean by "canonic" serialization, but in case of JSON serialisation, then yes i would assume a 'person' object would serialise to your example above. What would be great is if there were casting functions like: |
With "canonic" I meant that
|
If I understand the C++ macro magic correctly, the library needs to make an assumption on the upper bound of member variables. Assuming three members, a possible implementation would be: #include <iostream>
#include "json.hpp"
using json = nlohmann::json;
struct person
{
std::string name;
std::string address;
int age;
};
#define YAS_DEFINE_INTRUSIVE_SERIALIZE(type, var1, var2, var3) \
void to_json(nlohmann::json& j, const type& t)\
{\
j[#var1] = t.var1;\
j[#var2] = t.var2;\
j[#var3] = t.var3;\
}\
void from_json(const nlohmann::json& j, type& t)\
{\
j.at(#var1).get_to(t.var1);\
j.at(#var2).get_to(t.var2);\
j.at(#var3).get_to(t.var3);\
}
YAS_DEFINE_INTRUSIVE_SERIALIZE(person, name, address, age)
int main() {
person p;
p.name = "Ned Flanders";
p.age = 65;
p.address = "Springfield";
std::cout << json(p) << std::endl;
json j;
j["name"] = "Homer Simpson";
j["address"] = "Springfield";
j["age"] = 42;
person s = j;
std::cout << json(s) << std::endl;
} Output: {"address":"Springfield","age":65,"name":"Ned Flanders"}
{"address":"Springfield","age":42,"name":"Homer Simpson"} Is this what you have in mind? |
Yes exactly. I hadn't realised the macro expansion had an upper bound on the number of arguments. I thought it used VA_ARGS and worked automagically :)
If that makes sense. |
|
I have no real opinions on function names. Whatever works and Whatever you think is best. I was just clarifying that conversion functions were to and from the json object, not a json serialised string. What do you think overall ? Do you like the macro expansion API for registering types ? |
One of the big drawback here is that since json, as a format, is meant to be human-readable, that syntax only looks nice if you have a 1-to-1 match between variable names and the serialized tag, which is inherently precluded by many organizations' coding standards. If such a feature went in, I would expect an immediate followup request to be able to remap names. At that point, it's worth pointing out that there is a middle ground possible between the current approach and the full macro-based approach: Something along these lines (with better names):
I think this strikes a good balance between not having to essentially write the same code twice, while remaining debugable and flexible. |
@FrancoisChabot I like your suggestion. It's a good middle ground. I would maybe go a tiny bit further and use something like https://github.com/Neargye/nameof so you don't have to specify the variable name (though please don't use something that depends on C++17 and beyond) |
I still think the macro api is awesome. But looking at how yas and msgpack-c implemented it, it doesn't look debug-friendly that's for sure. |
namespace detail{
constexpr size_t nArgs(std::string_view str) {
//constexpr count_if is C++20
size_t nargs = 1;
for (auto c : str) if (c == ',') ++nargs;
return nargs;
}
template<typename T, size_t... Is>
void to_json_impl(nlohmann::json::json& j, const T& t, std::index_sequence<Is...>) {
((j[std::get<Is>(t._memberNames)] = std::get<Is>(t.members())), ...);
}
template<typename T, size_t... Is>
void from_json_impl(const nlohmann::json::json& j, T& t, std::index_sequence<Is...>) {
(j.at(std::get<Is>(t._memberNames)).get_to(std::get<Is>(t.members())), ...);
}
}
#define NLOHMANN_DEFINE_STRUCT_SERIALIZE(Type, ...) \
constexpr inline decltype(auto) members() const { return std::tie(__VA_ARGS__); } \
constexpr inline decltype(auto) members() { return std::tie(__VA_ARGS__); } \
static constexpr std::string_view _typeName = #Type;\
static constexpr std::array<std::string_view, detail::nArgs(#__VA_ARGS__)> _memberNames = \
[](){ std::array<std::string_view, detail::nArgs(#__VA_ARGS__)> out{}; \
size_t off = 0, next = 0; std::string_view ini(#__VA_ARGS__); \
for(auto& strV : out){ next = ini.find_first_of(',', off); strV = ini.substr(off, next - off); off = next + 1;} return out;}();\
friend void to_json(nlohmann::json::json& j, const Type& t) { detail::to_json_impl(j, t, std::make_index_sequence<_memberNames.size()>{}); } \
friend void from_json(const nlohmann::json::json& j, const Type& t) { detail::from_json_impl(j, t, std::make_index_sequence<_memberNames.size()>{}); }
struct person{
std::string name;
std::string address;
int age;
NLOHMANN_DEFINE_STRUCT_SERIALIZE(person, name, address, age)
}; If you don't like macro heavy implementations, something along this lines might be an option (this is implemented using C++17, but should be easy to port to C++11). If something like this is viable then I could also try to rewrite it for C++11. |
Yeah that looks great but very much a C++17 implementation. std::string_view is available in the nonstd libraries, std::tie isn't constexpr in C++11 and std::index_sequence is C++14. Do you think it's possible as a C++11 implementation? |
As a user i don't care about the implementation, that's hidden away from me. So long as it works i'm happy. Personally though, i am restricted to C++11 due to what i develop for. But that's just me, maybe this API could be enabled in C++17 only. Tough if you can't use it. |
@KonanM Thanks for the code! It would be really nice to have a C++11 equivalent for this. |
@KonanM I see you've used the same api in your |
@pfeatherstone yeah I wrote that basically for my own library and I'm happy to share that code. @nlohmann I converted the code to C++11 and hope I have not overseen anything: namespace nlohmann {
namespace detail {
//#include <nlohmann/detail/meta/cpp_future.hpp> for index_sequence and make_index_sequence
template<size_t...I >
using index_sequence = std::index_sequence<I...>;
template <size_t N>
using make_index_sequence = std::make_index_sequence<N>;
constexpr size_t n_args(const char* str) {
size_t nargs = 1;
for (char const* c = str; *c; ++c) if (*c == ',') ++nargs;
return nargs;
}
constexpr size_t str_size(const char* str) {
size_t strSize = 1;
for (char const* c = str; *c; ++c, ++strSize);
return strSize;
}
template<typename T, size_t... Is>
void to_json_impl(nlohmann::json::json& j, const T& t, nlohmann::detail::index_sequence<Is...>) {
(void)std::initializer_list<int>{(j[std::get<Is>(t._memberNames)] = std::get<Is>(t.members()), 0)...};
}
template<typename T, size_t... Is>
void from_json_impl(const nlohmann::json::json& j, T& t, nlohmann::detail::index_sequence<Is...>) {
(void)std::initializer_list<int>{(j.at(std::get<Is>(t._memberNames)).get_to(std::get<Is>(t.members())), 0)...};
}
}
}
#define NLOHMANN_DEFINE_STRUCT_SERIALIZE(Type, ...) \
inline decltype(auto) members() const { return std::tie(__VA_ARGS__); } \
inline decltype(auto) members() { return std::tie(__VA_ARGS__); } \
static constexpr std::array<char, nlohmann::detail::str_size(#__VA_ARGS__)> _memberNameData = [](){ \
std::array<char, nlohmann::detail::str_size(#__VA_ARGS__)> chars{}; size_t idx = 0; constexpr auto* ini(#__VA_ARGS__); \
for (char const* c = ini; *c; ++c, ++idx) if(*c != ',') chars[idx] = *c; return chars;}(); \
static constexpr std::array<const char*, nlohmann::detail::n_args(#__VA_ARGS__)> _memberNames = \
[](){ std::array<const char*, nlohmann::detail::n_args(#__VA_ARGS__)> out{ {Type::_memberNameData.data()} }; \
for(size_t i = 0, nArgs = 0; i < Type::_memberNameData.size() - 1; ++i) \
if (Type::_memberNameData[i] == '\0'){++nArgs; out[nArgs] = &Type::_memberNameData[i] + 1;} return out;}(); \
friend void to_json(nlohmann::json::json& j, const Type& t) { \
nlohmann::detail::to_json_impl(j, t, nlohmann::detail::make_index_sequence<_memberNames.size()>{}); } \
friend void from_json(const nlohmann::json::json& j, const Type& t) { \
nlohmann::detail::from_json_impl(j, t, nlohmann::detail::make_index_sequence<_memberNames.size()>{}); } |
|
my compiler complains that the lambda for _memberNameData is not constexpr |
by the way, i'm using g++ 8.4.0 with CXXFLAGS -std=c++11 |
I tried to make the code work with C++11 and the current version of the library. I still struggle to find a namespace nlohmann {
namespace detail {
constexpr size_t n_args(const char* str) {
size_t nargs = 1;
for (char const* c = str; *c; ++c) if (*c == ',') ++nargs;
return nargs;
}
constexpr size_t str_size(const char* str) {
size_t strSize = 1;
for (char const* c = str; *c; ++c, ++strSize);
return strSize;
}
template<typename T, size_t... Is>
void to_json_impl(nlohmann::json& j, const T& t, nlohmann::detail::index_sequence<Is...>) {
(void)std::initializer_list<int>{(j[std::get<Is>(t._memberNames)] = std::get<Is>(t.members()), 0)...};
}
template<typename T, size_t... Is>
void from_json_impl(const nlohmann::json& j, T& t, nlohmann::detail::index_sequence<Is...>) {
(void)std::initializer_list<int>{(j.at(std::get<Is>(t._memberNames)).get_to(std::get<Is>(t.members())), 0)...};
}
}
}
#define NLOHMANN_DEFINE_STRUCT_SERIALIZE(Type, ...) \
inline auto members() const -> decltype(std::tie(__VA_ARGS__)) { return std::tie(__VA_ARGS__); } \
inline auto members() -> decltype(std::tie(__VA_ARGS__)) { return std::tie(__VA_ARGS__); } \
static constexpr std::array<char, nlohmann::detail::str_size(#__VA_ARGS__)> _memberNameData = [](){ \
std::array<char, nlohmann::detail::str_size(#__VA_ARGS__)> chars{}; size_t idx = 0; constexpr auto* ini(#__VA_ARGS__); \
for (char const* c = ini; *c; ++c, ++idx) if(*c != ',') chars[idx] = *c; return chars;}(); \
static constexpr std::array<const char*, nlohmann::detail::n_args(#__VA_ARGS__)> _memberNames = \
[](){ std::array<const char*, nlohmann::detail::n_args(#__VA_ARGS__)> out{ {Type::_memberNameData.data()} }; \
for(size_t i = 0, nArgs = 0; i < Type::_memberNameData.size() - 1; ++i) \
if (Type::_memberNameData[i] == '\0'){++nArgs; out[nArgs] = &Type::_memberNameData[i] + 1;} return out;}(); \
friend void to_json(nlohmann::json& j, const Type& t) { \
nlohmann::detail::to_json_impl(j, t, nlohmann::detail::make_index_sequence<_memberNames.size()>{}); } \
friend void from_json(const nlohmann::json& j, Type& t) { \
nlohmann::detail::from_json_impl(j, t, nlohmann::detail::make_index_sequence<_memberNames.size()>{}); } Any ideas? |
are constexpr lambdas allowed in C++11? |
No, that's C++17. |
So I don’t think the above will work. Unless implicit constexpr lambdas are ok in c++11 but I don’t think so. Maybe macro is the only way |
We can still write a normal struct with an constexpr operator() inside the macro. I will give it another try tomorrow. Otherwise we could still drop constexpr support for C++1 and replace it by const. |
The lambdas aren’t capturing anything so I don’t think you need the structs, just static functions. |
Ok I gave it another try and made it work with C++14 with msvc, gcc and clang, but I completely forgot that I can't use any control statements in constexpr functions in C++11... like you can't use a for loop at all. https://godbolt.org/z/Zdc9Mz That doesn't make the implementation impossible, but at that point the macro based approach is probably just easier (you would have to replace the for loop by index sequence template parameters...). I will give the macro based approach a try to see how that looks like |
Ok here is a super simply macro based solution, which works for up to 9 parameters (can be expanded) - that should work for basically every compiler. There might be more elegant solution, but this one is dead simple and doesn't require any great macro magic. |
I had a look at the msgpack-c implementation and it looks like a lot of the MACROS are inspired by boost.preprocessor. The number of arguments is limited to 256. I think that is reasonable. Could get it to work with boost.preprocessor, if it does then pinch all the macros we need. |
Actually one more thing. What i really like about this library as opposed to msgpack-c say, is that the intermediate representation between type and serialized buffer, i.e. the Now currently, if you have:
this won't work because of the line
But if this were replaced with:
then it would be fine.
with
Are there any performance issues? |
Really the problem with using
when j is a
Maybe the function |
Hmm, actually |
Maybe the solution is dealing with this error: |
I would not like to mix this issue with adding an intermediate value. I will prepare a merge request for #2175 (comment). |
@pfeatherstone @KonanM I finally had time to open a PR for this issue, see #2225. It would be great if you could provide some feedback. |
Thanks a lot everybody! |
Last thing: I want to do something like this:
Which spits out error:
Is there a way of dealing with this compiler error?
which opens Pandora's box of errors. Is there a way of doing this? |
That would be the icing on the cake |
So instead of :
I propose something like
where
I'm not a huge fan of this, but it avoids having to suppress the error:
Which could require some refactoring inside the |
It seems to be sufficient to add template<typename ValueType,
detail::enable_if_t <
detail::is_basic_json<ValueType>::value,
int> = 0>
ValueType & get_to(ValueType& v) const
{
v = *this;
return v;
} (the other function explicitly require Then the following code works: #include <iostream>
#include "json.hpp"
struct person
{
std::string name;
std::string address;
int age;
nlohmann::json j;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(person, name, address, age, j)
int main()
{
person p;
p.name = "Homer Simpson";
p.address = "Springfield";
p.age = 42;
p.j = {{"foo", "bar"}};
nlohmann::json j = p;
std::cout << j << std::endl;
} Output: {"address":"Springfield","age":42,"j":{"foo":"bar"},"name":"Homer Simpson"} Any thoughts on this? |
Yep that looks better than what I suggested. Ship it. Thanks! :) |
Sorry that I'm that late, but yeah the approach by @nlohmann is much better. The other approach would also fail when being used with more than one 'NLOHMANN_DEFINE_TYPE'. I was thinking about if it's possible to drop the SFINAE and only specialize 'nlohmann::json', but I guess not. |
#include <nlohmann/json.hpp>
#include <iostream>
namespace nl = nlohmann;
enum class Schema {
HTTP,
HTTPS
};
NLOHMANN_JSON_SERIALIZE_ENUM(Schema, {
{Schema::HTTP, "http"},
{Schema::HTTPS, "https"},
})
struct Foo {
int a;
NLOHMANN_DEFINE_TYPE_INTRUSIVE(Foo, a)
};
struct Request {
Schema schema;
Foo foo;
std::vector<int> bar;
NLOHMANN_DEFINE_TYPE_INTRUSIVE(Request, schema, foo, bar)
};
int main() {
std::string s = "{\"schema\":\"https\",\"x\":{\"a\":11},\"bar\":[1,2,3,4]}";
Request r = nl::json::parse(s);
} the example above fails with:
is it possible to just skip fields that are not present? |
Not when using the |
Hi guys, what about inheritance? struct parent
{
std::string surname;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(parent, surname);
struct child : public parent
{
std::string name;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(child, name); It would be great if something similar would be possible. |
Yep, that should be handled. |
Or maybe not. It would complicate |
For reference, here i discuss an alternative implementation of |
Currently, the API for registering custom types is as follows:
It would be great if there was a MACRO-style registration a bit like what msgpack-c uses:
or yas:
or
The text was updated successfully, but these errors were encountered: