-
-
Notifications
You must be signed in to change notification settings - Fork 6.7k
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
Feature/user defined types bis #355
Conversation
This is a first version of the I find the need to have another 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 @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 ? |
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 EDIT: to make it more clear, when |
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 This solves the main issue I had with that solution! |
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); | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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);
}
There was a problem hiding this comment.
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
@nlohmann there seems to be a problem in a test. I had an out of memory error once, in I don't know if the Travis' segfault is related though, do you have this problem on other PR? |
Travis seems to have problems every now and then - I restarted the build and it succeeded. |
In the last commit I faced an issue when providing a user-defined Since I did not change the way base types are handled, such as // 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:
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 |
Not really. From the point of view of the new serialization/deserialization mechanism, primitive types and standard library types look like user-defined types :/ |
About what kind of refactoring are we talking? |
The constructors / assignments of |
I am working on a fourth solution, that does not require refactoring. It should be ready around tonight. |
After a lot of rewrites, I came to this implementation. I managed to remove constraints on 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.).
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this 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> |
There was a problem hiding this comment.
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?)
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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{}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this used somewhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nope I forgot that
2dcffbb
to
e7bdc2f
Compare
uuh? Is there a problem with travis? I dont see conflicts EDIT: nevermind, I see them now |
7c60907
to
d9a7493
Compare
0e8e9b6
to
75ae715
Compare
@@ -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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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!
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 |
Sunday or Friday evening (CET) would fit for me. |
Sunday fits me, today it depends on the time.
…On Thu, Dec 15, 2016 at 10:47 PM, Niels Lohmann ***@***.***> wrote:
Sunday or Friday evening (CET) would fit for me.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#355 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AA3NptoC7SJ1M8xoJHmsE8eRZbziN7y1ks5rIbVXgaJpZM4KrzJH>
.
|
Sunday is fine for me, I can't make it today unfortunately |
How about 19:00 (Berlin time) today? |
No problem for me |
Hi, I was thinking about what we talked about yesterday, i.e. replacing existing This is not only about cleaning code, but this enables a feature: In the code, we call The issue with those constraints is that users cannot override the default behaviour for But if we change constructors to free functions, those functions would be called via the default Which means, if a user specialized 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. |
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 |
Indeed, but if we add the overloads in the 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 |
Yes that would be the way to move the "constructors" out of |
This can be closed, right? After all, we have #423 now. |
Looks like it. Thank you very much for caring about this topic and not rushing things. |
Yes, this can be closed |
WIP: This PR is NOT ready to be merged, its purpose is to facilitate review and discussion.