Skip to content
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

Feature/user defined types bis #355

Closed

Conversation

theodelrieu
Copy link
Contributor

WIP: This PR is NOT ready to be merged, its purpose is to facilitate review and discussion.

@theodelrieu
Copy link
Contributor Author

This is a first version of the JSONSerializer template argument, (alternate implementation).
I didn't make a custom serializer in the tests yet. But I already feel that I like the first implementation more.

I find the need to have another basic_json typedef for types we want to serialize differently quite problematic.

A use case in my own code is partial specialization on template types (e.g. boost::optional, boost::containers, ...).

In order to rely on ADL, one would need to write a from_json method in the boost namespace, which is undesirable.

@gnzlbg I know you mentioned something about providing some "c++11 concepts" (e.g. Optional Concept), however I think we should provide UDT support first, then enhance it if users demand it.

What do you think about this ?

@coveralls
Copy link

Coverage Status

Coverage remained the same at 100.0% when pulling 280cd36 on theodelrieu:feature/user_defined_types_bis into 9ca00e4 on nlohmann:develop.

@gnzlbg
Copy link

gnzlbg commented Nov 8, 2016

In order to rely on ADL, one would need to write a from_json method in the boost namespace, which is undesirable.

That would be undesirable but doing so is not necessary. If the customization point is implemented as suggested in the paper, then you have something like this (from the top of my head using my brain's imperfect C++ compiler):

namespace json {

// here you can define from_json for any type 

template <typename JSON, typename T, std::size_t N>
void from_json(JSON const& j, T (t&)[N]) {
  // implementation for C-Arrays
}

template <typename JSON, typename T>
void from_json(JSON const& j, boost::optional<T>& o) {
  // implementation for boost::optional
}

// if you have a type trait to select "Optional-like objects" you could have a 
// generic implementation for those here as well, but you just want it to
// be picked if a concrete implementation is not found

struct from_json_fn {
  template <typename JSON, typename T>
  constexpr auto operator()(JSON const& j, T& t) const noexcept(...) 
  -> decltype(from_json(j, t)) {
    return from_json(j, t);
  }
};

namespace { 
  static constexpr auto const& from_json = static_const<from_json_fn>::value;
}  // namespace 

}  // namespace json

The only thing to take into account is that you don't want nlohmann::json to depend on boost::optional, so this means you should put those overloads in the json namespace in a separate header, e.g., <json/extensions/boost/optional.hpp>, that people can then opt into including. IIRC the only condition is that those extension headers must be included before including json.

EDIT: to make it more clear, when from_json is called unqualified within from_json_fn::operator(), it will try to find a suitable overload both in scope (the global and the json namespaces), and the associated namespaces of the types involved, i.e., the associated namespace of JSON (which in this case should be json), and the associated namespace of T, which in case of boost::optional will be the boost namespace.

@theodelrieu
Copy link
Contributor Author

It makes sense to have extensions separately, even if I believe we should add them later, if users show the need.

By the way, I re-checked one of your comments in issue #328 and I realized that I forgot to add template arguments to the JSONSerializer class. Which enables the use of partial specialization and ADL if both are wanted.

This solves the main issue I had with that solution!

@coveralls
Copy link

Coverage Status

Coverage remained the same at 100.0% when pulling 4fae04c on theodelrieu:feature/user_defined_types_bis into 9ca00e4 on nlohmann:develop.

static auto from_json(Json&& j, T& val) -> decltype(::nlohmann::from_json(std::forward<Json>(j), val))
{
::nlohmann::from_json(std::forward<Json>(j), val);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is missing a 'return' and maybe you should have a unit test on a from_json type that does return a value.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC from_json should never return a value, that is, its signature should be (JSON const& input, T& output) -> void, so maybe it would be better to omit the return value but put a void in the decltype like this:

static auto from_json(Json&& j, T& val) -> 
decltype(::nlohmann::from_json(std::forward<Json>(j), val), void)
{
    ::nlohmann::from_json(std::forward<Json>(j), val);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I came to the same conclusion, right now, the code is quite messy, I thought at first it would be a nice idea to return the type directly.

However, we cannot know which basic_json type will be used by the user in its own method, hence the need to pass it as an argument.

I will remove the overloads in a next commit

@coveralls
Copy link

Coverage Status

Coverage remained the same at 100.0% when pulling 2530767 on theodelrieu:feature/user_defined_types_bis into 9ca00e4 on nlohmann:develop.

@coveralls
Copy link

Coverage Status

Coverage remained the same at 100.0% when pulling 9ca5d9c on theodelrieu:feature/user_defined_types_bis into 9ca00e4 on nlohmann:develop.

@theodelrieu
Copy link
Contributor Author

@nlohmann there seems to be a problem in a test.

I had an out of memory error once, in unit-constructor1.cpp, when constructing a JSON from a std::ifstream. Simply launching the executable from elsewhere solved it for me.

I don't know if the Travis' segfault is related though, do you have this problem on other PR?
It didn't trigger on each of my commit, that's why I'm wondering

@nlohmann
Copy link
Owner

Travis seems to have problems every now and then - I restarted the build and it succeeded.

@theodelrieu
Copy link
Contributor Author

In the last commit I faced an issue when providing a user-defined JSONSerializer.

Since I did not change the way base types are handled, such as int or std::string, the following code breaks because of ambiguity, when doing enable_if checks:

// when not specialized, I want to use ADL as well
template <typename T, typename = void>
struct my_serializer
{
  template <typename Json>
  static void from_json(Json const& j, T& val)
  {
    nlohmann::from_json(j, val);
  }
};

There are some ways to solve this:

  • specialize the serializer for every type
  • do some SFINAE to avoid already handled types
  • handle basic types in nlohmann with to_json / from_json functions

The first two are quite annoying for users, and the latter requires a bit of refactoring (which will be needed someday if this feature is merged IMHO).

I would rather choose the latter, which doesn't impact users
@gnzlbg @nlohmann Do you see another way to solve this issue?

@coveralls
Copy link

Coverage Status

Coverage remained the same at 100.0% when pulling 738157a on theodelrieu:feature/user_defined_types_bis into 9ca00e4 on nlohmann:develop.

@gnzlbg
Copy link

gnzlbg commented Nov 15, 2016

Do you see another way to solve this issue?

Not really. From the point of view of the new serialization/deserialization mechanism, primitive types and standard library types look like user-defined types :/

@nlohmann
Copy link
Owner

and the latter requires a bit of refactoring (which will be needed someday if this feature is merged IMHO).

About what kind of refactoring are we talking?

@gnzlbg
Copy link

gnzlbg commented Nov 15, 2016

The constructors / assignments of basic_json for int, float, std::string, ranges(begin, end, value_type) ... would be extracted to from_json/to_json free functions. The only "special" constructor / assignment that would remain would be a template that is constrained on the type T being JSONSerializable.

@theodelrieu
Copy link
Contributor Author

I am working on a fourth solution, that does not require refactoring. It should be ready around tonight.

@theodelrieu
Copy link
Contributor Author

After a lot of rewrites, I came to this implementation.

I managed to remove constraints on nlohmann::adl_serializer and the custom one in the tests, without breaking anything.

I wanted to have your feedback on it, I'm sorry I didn't write comments, so do not hesitate to comment on the diff!

This fourth solution has the advantages of the former discussed one, without its main drawback: the need to rewrite almost of the lib...

// taken from ranges-v3
// TODO add doc
template <typename T>
struct _static_const
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: the leading underscore might be undefined behavior (I always forget the rules...). Worth checking.

Copy link
Contributor Author

@theodelrieu theodelrieu Nov 16, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to http://en.cppreference.com/w/c/language/identifier#Reserved_identifiers, it seems that you're right, since this qualify as an external identifier (never seen that term before).
Anyway, removing the underscore seems a good idea

Copy link

@gnzlbg gnzlbg Nov 16, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I rechecked and IIUC this usage is fine because the underscore:

  • is not followed by another underscore,
  • is not followed by a capital letter,

and the identifier is not in the global namespace.

I am still not sure that I understood the rules correctly though... Avoiding identifiers that start with an underscore was always my pragmatic solution (Life is too short, etc.).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still confused by the "external identifier" thing, but it would surprise me if identifiers beginning with an underscore (except the global namespace) were to be reserved.

I would be an outlaw with all my prefixed private variables...

detail::is_compatible_unsigned_integer_type<number_unsigned_t, uncvref_t<T>>::value or
std::is_constructible<string_t, uncvref_t<T>>::value or
std::is_base_of<std::istream, uncvref_t<T>>::value or
std::is_same<boolean_t, uncvref_t<T>>::value), int> = 0>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might want to abstract all these checks into an detail::is_basic_json_compatible<uncvref_t<T>> or something like that. It might make the function definition clearer, and it also might make it easier to explain what is going on.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes indeed, I pushed as soon as I made it work, I also forgot to format the relevant parts, sorry for that

Copy link

@gnzlbg gnzlbg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The overall approach looks good to me.

The whole thing still needs to be cleaned up, documented, I haven't reviewed the tests, etc., but a solution in this spirit should work for all situations discussed in the issue.

std::numeric_limits<CompatibleNumberUnsignedType>::is_integer and
not std::numeric_limits<CompatibleNumberUnsignedType>::is_signed,
CompatibleNumberUnsignedType>::type = 0>
template<typename CompatibleNumberUnsignedType, enable_if_t<detail::is_compatible_unsigned_integer_type<number_unsigned_t, CompatibleNumberUnsignedType>::value, int> = 0>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you need to pass number_unsigned_t to detail::is_compatible_unsigned_integer_type ? Shouldn't passing it CompatibleNumberUnsignedType be enough? (as in, can't we simplify this a bit?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can, I simply translated the existing checks, to avoid spending time on it (before making the whole thing work)

template<typename CompatibleNumberFloatType, typename = typename std::enable_if<
std::is_constructible<number_float_t, CompatibleNumberFloatType>::value and
std::is_floating_point<CompatibleNumberFloatType>::value>::type>
template<typename CompatibleNumberFloatType, enable_if_t<detail::is_compatible_float_type<number_float_t, CompatibleNumberFloatType>::value, int> = 0>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as a bove

detail::is_compatible_array_type<basic_json_t, uncvref_t<T>>::value or
detail::is_compatible_float_type<number_float_t, uncvref_t<T>>::value or
detail::is_compatible_integer_type<number_integer_t, uncvref_t<T>>::value or
detail::is_compatible_unsigned_integer_type<number_unsigned_t, uncvref_t<T>>::value or
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, define a detail::is_compatible_basic_json_type (or similar) that abstracts all this and makes it more readable.

};

void to_json();
void from_json();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need these two here? are they really necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to add a comment on that, this is the MSVC workaround taken from Microsoft range implementation

Copy link

@gnzlbg gnzlbg Nov 16, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While reading all the has_xxxx classes I was wondering why you did just not use expression SFINAE for these, and actually wondered if it might had something to do with MSVC.

EDIT: Not that I think expression SFINAE is better, but it would be the laziest solution and what I would have tried at first.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was more because of clarity at first. I try to avoid expression SFINAE when it becomes too demanding on MSVC too

@@ -122,6 +135,186 @@ struct has_mapped_type
std::is_integral<decltype(detect(std::declval<T>()))>::value;
};

template <template <typename...> class T>
struct wrapper{};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this used somewhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope I forgot that

@coveralls
Copy link

Coverage Status

Coverage remained the same at 100.0% when pulling c4be803 on theodelrieu:feature/user_defined_types_bis into 9ca00e4 on nlohmann:develop.

@theodelrieu
Copy link
Contributor Author

theodelrieu commented Nov 17, 2016

uuh? Is there a problem with travis? I dont see conflicts

EDIT: nevermind, I see them now

@@ -220,20 +220,20 @@ TEST_CASE("const_iterator class")
{
json j(json::value_t::null);
json::const_iterator it = j.cbegin();
CHECK(it.m_it.primitive_iterator == 1);
CHECK(it.m_it.primitive_iterator.m_it == 1);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just out of curiosity: why is the .m_it suffix necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well this a consequence of removing the explicit keyword on the new constructor.

As I mentioned a few days ago:

In the second [example], the converting struct has template arguments, which prevents the sneaky implicit conversion, since the compiler cannot deduce T when trying to convert int to basic_json.

Although code outside the basic_json class is protected from this sneaky implicit conversion, the code inside is not!

Every implicit conversion inside basic_json will try to use the T && constructor, if it goes trough the enable_ifs.

It happened with primitive_iterator because there was an implicit conversion operator to difference_type, that's why I reimplemented the iterator operators (copy-pasted until I refactor, sorry about that).

That's also the reason why I made the .m_it suffix public, it was also needed in the tests, because Catch does fancy capture stuff, and seems to be able to call the doomed constructor (through a call to operator<<(ostream&, primitive_iterator) if I remember correctly)

I should have added comments explaining this, sorry about that too!

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation. Sorry for not having seen this in the discussion above. Great work!

@coveralls
Copy link

Coverage Status

Coverage remained the same at 100.0% when pulling 13082c1 on theodelrieu:feature/user_defined_types_bis into bc28942 on nlohmann:develop.

@coveralls
Copy link

Coverage Status

Coverage remained the same at 100.0% when pulling 13082c1 on theodelrieu:feature/user_defined_types_bis into bc28942 on nlohmann:develop.

@coveralls
Copy link

Coverage Status

Coverage remained the same at 100.0% when pulling b6ef4f7 on theodelrieu:feature/user_defined_types_bis into bc28942 on nlohmann:develop.

@coveralls
Copy link

coveralls commented Dec 15, 2016

Coverage Status

Coverage remained the same at 100.0% when pulling 59ffbab on theodelrieu:feature/user_defined_types_bis into bc28942 on nlohmann:develop.

@theodelrieu
Copy link
Contributor Author

Hi everyone, we're almost done!

@nlohmann, @gnzlbg and I will have a Slack meeting shortly, to discuss the design decisions, and decide where we go from here.

If you want to join, feel free to send me an email, I'll invite you to the Slack team!

I'd like to plan it for this weekend, if it fits everyone's agenda. Let me know and I'll post the meeting date on Slack and Github.

Thanks

@nlohmann
Copy link
Owner

Sunday or Friday evening (CET) would fit for me.

@gnzlbg
Copy link

gnzlbg commented Dec 16, 2016 via email

@theodelrieu
Copy link
Contributor Author

Sunday is fine for me, I can't make it today unfortunately

@nlohmann
Copy link
Owner

How about 19:00 (Berlin time) today?

@theodelrieu
Copy link
Contributor Author

No problem for me

@theodelrieu
Copy link
Contributor Author

Hi, I was thinking about what we talked about yesterday, i.e. replacing existing basic_json constructors by to_json/from_json overloads.

This is not only about cleaning code, but this enables a feature:

In the code, we call JSONSerializer<T>::to_json in the new constructor, but there is a lot of enable_ifs to avoid chosing the others (when T = int for example).

The issue with those constraints is that users cannot override the default behaviour for vector, array or unscoped enums (and all types already handled by the library)

But if we change constructors to free functions, those functions would be called via the default adl_serializer<T>::from_json, thanks to ADL.

Which means, if a user specialized adl_serializer<std::vector<T>>, that his specialization will be called.
This allows to support unscoped enums, and to give full control to the users that need it.

Also, they would still be able to call the default behaviour:

std::vector<int> v{1, 2, 3};

// this is also called by the default adl_serializer too
auto j = nlohmann::to_json(v);

This might have been mentioned by @gnzlbg yesterday, I don't remember, but I thought it would be useful to share that thought.

@gnzlbg
Copy link

gnzlbg commented Dec 19, 2016

No, I didn't mention it (and hadn't really thought about it), but yes, you are correct, if everything goes through the trait system then the trait system would allow this kind of "override" (note: this kind of override is not possible with just ADL because adding overloads to std:: is undefined behavior).

@theodelrieu
Copy link
Contributor Author

Indeed, but if we add the overloads in the detail namespace, it should work:

namespace nlohmann::detail {

// this should be found via unqualified call
template <typename JSON, typename T, typename Allocator>
void to_json(JSON&, std::vector<T, Allocator> const&) {}

struct to_json_fn {
template <typename JSON, typename T>
void operator()(JSON& j, T&& val) {
  // for T = std::vector<U>, this should call the overload ahead
  to_json(j, std::forward<T>(val));
}
};
} // nlohmann::detail

I haven't tried to compile this, but I think it's valid

@gnzlbg
Copy link

gnzlbg commented Dec 20, 2016

Yes that would be the way to move the "constructors" out of basic_json. What I meant was only that the only way for users to override this would be to either specialize the adl_serializer trait or pass basic_json a different serializer trait since they cannot add overloads to the namespaces associated with those types. I guess this should be documented.

@nlohmann
Copy link
Owner

This can be closed, right? After all, we have #423 now.

@qis
Copy link

qis commented Jan 23, 2017

Looks like it. Thank you very much for caring about this topic and not rushing things.

@theodelrieu
Copy link
Contributor Author

Yes, this can be closed

@nlohmann nlohmann closed this Jan 23, 2017
@theodelrieu theodelrieu deleted the feature/user_defined_types_bis branch January 23, 2017 10:27
nlohmann added a commit that referenced this pull request Jan 27, 2017
conversion from/to user-defined types
@dota17 dota17 mentioned this pull request Feb 5, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants