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

Add support for deserialization of STL containers of non-default constructable types (fixes #2574). #2576

Merged
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 33 additions & 9 deletions include/nlohmann/adl_serializer.hpp
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
#pragma once

#include <type_traits>
#include <utility>

#include <nlohmann/detail/conversions/from_json.hpp>
#include <nlohmann/detail/conversions/to_json.hpp>
#include <nlohmann/detail/meta/identity_tag.hpp>
#include <nlohmann/detail/meta/type_traits.hpp>

namespace nlohmann
{

template<typename, typename>
template<typename ValueType, typename>
struct adl_serializer
{
/*!
Expand All @@ -17,17 +20,39 @@ struct adl_serializer
This function is usually called by the `get()` function of the
@ref basic_json class (either explicit or via conversion operators).

@note This function is chosen for value types which can be default constructed.
AnthonyVH marked this conversation as resolved.
Show resolved Hide resolved

@param[in] j JSON value to read from
@param[in,out] val value to write to
*/
template<typename BasicJsonType, typename ValueType>
static auto from_json(BasicJsonType&& j, ValueType& val) noexcept(
template<typename BasicJsonType, typename TargetType = ValueType>
static auto from_json(BasicJsonType && j, TargetType& val) noexcept(
noexcept(::nlohmann::from_json(std::forward<BasicJsonType>(j), val)))
-> decltype(::nlohmann::from_json(std::forward<BasicJsonType>(j), val), void())
{
::nlohmann::from_json(std::forward<BasicJsonType>(j), val);
}

/*!
@brief convert a JSON value to any value type

This function is usually called by the `get()` function of the
@ref basic_json class (either explicit or via conversion operators).

@note This function is chosen for value types which can not be default constructed.
AnthonyVH marked this conversation as resolved.
Show resolved Hide resolved

@param[in] j JSON value to read from

@return copy of the JSON value, converted to @a ValueType
*/
template<typename BasicJsonType, typename TargetType = ValueType>
static auto from_json(BasicJsonType && j) noexcept(
noexcept(::nlohmann::from_json(std::forward<BasicJsonType>(j), detail::identity_tag<TargetType> {})))
-> decltype(::nlohmann::from_json(std::forward<BasicJsonType>(j), detail::identity_tag<TargetType> {}))
{
return ::nlohmann::from_json(std::forward<BasicJsonType>(j), detail::identity_tag<TargetType> {});
}

/*!
@brief convert any value type to a JSON value

Expand All @@ -37,13 +62,12 @@ struct adl_serializer
@param[in,out] j JSON value to write to
@param[in] val value to read from
*/
template<typename BasicJsonType, typename ValueType>
static auto to_json(BasicJsonType& j, ValueType&& val) noexcept(
noexcept(::nlohmann::to_json(j, std::forward<ValueType>(val))))
-> decltype(::nlohmann::to_json(j, std::forward<ValueType>(val)), void())
template<typename BasicJsonType, typename TargetType = ValueType>
static auto to_json(BasicJsonType& j, TargetType && val) noexcept(
noexcept(::nlohmann::to_json(j, std::forward<TargetType>(val))))
-> decltype(::nlohmann::to_json(j, std::forward<TargetType>(val)), void())
{
::nlohmann::to_json(j, std::forward<ValueType>(val));
::nlohmann::to_json(j, std::forward<TargetType>(val));
}
};

} // namespace nlohmann
99 changes: 88 additions & 11 deletions include/nlohmann/detail/conversions/from_json.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include <nlohmann/detail/exceptions.hpp>
#include <nlohmann/detail/macro_scope.hpp>
#include <nlohmann/detail/meta/cpp_future.hpp>
#include <nlohmann/detail/meta/identity_tag.hpp>
#include <nlohmann/detail/meta/type_traits.hpp>
#include <nlohmann/detail/value_t.hpp>

Expand Down Expand Up @@ -248,6 +249,33 @@ void())
from_json_array_impl(j, arr, priority_tag<3> {});
}

template < typename T, typename BasicJsonType, typename ArrayType, std::size_t... Idx>
ArrayType from_json_inplace_array_impl_base(BasicJsonType&& j, identity_tag<ArrayType> /*unused*/,
index_sequence<Idx...> /*unused*/)
{
return { std::forward<BasicJsonType>(j).at(Idx).template get<T>()... };
}

template < typename BasicJsonType, typename T, std::size_t N >
auto from_json_inplace_array_impl(BasicJsonType&& j, identity_tag<std::array<T, N>> tag, priority_tag<0> /*unused*/)
-> decltype(from_json_inplace_array_impl_base<T>(std::forward<BasicJsonType>(j), tag, make_index_sequence<N> {}))
{
return from_json_inplace_array_impl_base<T>(std::forward<BasicJsonType>(j), tag, make_index_sequence<N> {});
}

template < typename BasicJsonType, typename ArrayType >
auto from_json(BasicJsonType&& j, identity_tag<ArrayType> tag)
-> decltype(from_json_inplace_array_impl(std::forward<BasicJsonType>(j), tag, priority_tag<0> {}))
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 not how priority_tag is supposed to be used. You should have a single from_json overload and construct the tag with the highest value, like this:

template < typename BasicJsonType, typename ArrayType >
auto from_json(BasicJsonType&& j, ArrayType&& array)
-> decltype(from_json_inplace_array_impl(std::forward<BasicJsonType>(j),
                                         std::forward<ArrayType>(array), priority_tag<1> {}));

This gist shows quite nicely how to use it. Basically, it forces the overload resolution so you don't have to perform every SFINAE check in the world (like you had is_default_constructible and ! is_default_constructible)

Copy link
Contributor Author

@AnthonyVH AnthonyVH Jan 12, 2021

Choose a reason for hiding this comment

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

Note that there's only 1 possible overload for from_json_inplace_array_impl(), hence 0 is the highest value in that case. Or am I missing something else?

The from_json_inplace_array_impl_base() is there because there's no index_sequence argument for from_json_inplace_array_impl(). I could add that and call make_index_sequence() from from_json(), but then any future from_json_inplace_array_impl() overload should have that as an argument as well, which didn't seem ideal. Hence the extra indirection.

I also looked into making this part of the from_json_array_impl() overloads, but there's SFINAE on its "entry-point" from_json() which prevents it from being called for non-default constructable types.

Copy link
Contributor

Choose a reason for hiding this comment

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

Or am I missing something else?

No, it's me sorry. But in this case there is no need for priority_tag at all.

but there's SFINAE on its "entry-point" from_json() which prevents it from being called for non-default constructable types.

Just thought about it but non-default constructible types can very well use from_json(Json const&, T&) overloads, this happens when get_to is used for instance.

Therefore, the is_default_constructible check should just happen in get, and all functions that default-construct values before calling JSONSerializer<T>::from_json, no?

This would allow you to dispatch to from_json_array_impl and then use priority_tag.

It's been quite a while since I put my hands/eyes on the code, sorry for rusty review 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, it's me sorry. But in this case there is no need for priority_tag at all.

True, I didn't have that there originally, but I gathered from one of your previous comments that you preferred this to make adding future overloads easier. I can remove it again if you prefer.

Therefore, the is_default_constructible check should just happen in get, and all functions that default-construct values before calling JSONSerializer::from_json, no?

Yes, that's true. Checking for default constructability in detail::from_json (e.g. the array version) shouldn't be necessary, since at that point you already have a reference to such an object, so it's already constructed.

Note that get() already has some checks using e.g. has_from_json and has_non_default_from_json. And those currently depend on default constructability of a type, since they depend the presence of certain signatures of from_json existing, and some of those in turn exist based on constructability of a type. Which as noted above actually isn't necessary...

So I guess the solution would be to get rid of constructability checks in from_json() and add an explicit std::is_default_constructible to get()?

I can take a look at it later this week. Although that might be overextending the scope of this PR. Or is that not an issue?

Copy link
Contributor

Choose a reason for hiding this comment

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

True, I didn't have that there originally, but I gathered from one of your previous comments that you preferred this to make adding future overloads easier.

When there is at least two overloads, yes 😄

Although that might be overextending the scope of this PR.

I think it's a part of it, then you adding a get overload with that deals with non-default constructible types that do not have has_non_default_from_json should do the trick (it would check if from_json(json, identity_tag<> is valid).

Icing on the cake would be to refactor all those get overloads to use priority_tag.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

When there is at least two overloads, yes 😄

Ok, I got ride of the redundant priority_tag and simplified some more code.

The LWG 2367 but mentioned below should also be fixed. Let's see what the regression says.

Also added some icing 😉.

{
if (JSON_HEDLEY_UNLIKELY(!j.is_array()))
{
JSON_THROW(type_error::create(302, "type must be array, but is " +
std::string(j.type_name())));
}

return from_json_inplace_array_impl(std::forward<BasicJsonType>(j), tag, priority_tag<0> {});
}

template<typename BasicJsonType>
void from_json(const BasicJsonType& j, typename BasicJsonType::binary_t& bin)
{
Expand Down Expand Up @@ -323,22 +351,71 @@ void from_json(const BasicJsonType& j, ArithmeticType& val)
}
}

template<typename BasicJsonType, typename A1, typename A2>
void from_json(const BasicJsonType& j, std::pair<A1, A2>& p)
template < typename BasicJsonType, class A1, class A2 >
std::pair<A1, A2> from_json_pair_impl(BasicJsonType&& j, identity_tag<std::pair<A1, A2>> /*unused*/, priority_tag<0> /*unused*/)
{
p = {j.at(0).template get<A1>(), j.at(1).template get<A2>()};
return {std::forward<BasicJsonType>(j).at(0).template get<A1>(),
std::forward<BasicJsonType>(j).at(1).template get<A2>()};
}

template<typename BasicJsonType, typename A1, typename A2,
enable_if_t<std::is_default_constructible<std::pair<A1, A2>>::value, int> = 0>
void from_json_pair_impl(BasicJsonType && j, std::pair<A1, A2>& p, priority_tag<1> /*unused*/)
{
p = from_json_pair_impl(std::forward<BasicJsonType>(j), identity_tag<std::pair<A1, A2>> {}, priority_tag<0> {});
}

template<typename BasicJsonType, typename PairRelatedType>
auto from_json(BasicJsonType&& j, PairRelatedType&& p)
-> decltype(from_json_pair_impl(std::forward<BasicJsonType>(j), std::forward<PairRelatedType>(p), priority_tag<1> {}))
{
if (JSON_HEDLEY_UNLIKELY(!j.is_array()))
{
JSON_THROW(type_error::create(302, "type must be array, but is " +
std::string(j.type_name())));
}

return from_json_pair_impl(std::forward<BasicJsonType>(j), std::forward<PairRelatedType>(p), priority_tag<1> {});
}

template<typename BasicJsonType, typename Tuple, std::size_t... Idx>
void from_json_tuple_impl(const BasicJsonType& j, Tuple& t, index_sequence<Idx...> /*unused*/)
Tuple from_json_tuple_impl(BasicJsonType&& j, identity_tag<Tuple> /*unused*/, index_sequence<Idx...> /*unused*/, priority_tag<0> /*unused*/)
{
return std::make_tuple(std::forward<BasicJsonType>(j).at(Idx).template get<typename std::tuple_element<Idx, Tuple>::type>()...);
}

template<typename BasicJsonType, typename Tuple, std::size_t... Idx,
enable_if_t<std::is_default_constructible<Tuple>::value, int> = 0>
void from_json_tuple_impl(BasicJsonType && j, Tuple& t, index_sequence<Idx...> /*unused*/, priority_tag<1> /*unused*/)
{
t = from_json_tuple_impl(std::forward<BasicJsonType>(j), identity_tag<Tuple> {}, priority_tag<0> {});
}

template<typename BasicJsonType, typename TupleRelated, std::size_t... Idx>
auto from_json_tuple(BasicJsonType&& j, TupleRelated&& t, index_sequence<Idx...> idx)
-> decltype(from_json_tuple_impl(std::forward<BasicJsonType>(j), std::forward<TupleRelated>(t), idx, priority_tag<1> {}))
{
if (JSON_HEDLEY_UNLIKELY(!j.is_array()))
{
JSON_THROW(type_error::create(302, "type must be array, but is " +
std::string(j.type_name())));
}

return from_json_tuple_impl(std::forward<BasicJsonType>(j), std::forward<TupleRelated>(t), idx, priority_tag<1> {});
}

template<typename BasicJsonType, typename... Args>
auto from_json(BasicJsonType&& j, std::tuple<Args...>& t)
-> decltype(from_json_tuple(std::forward<BasicJsonType>(j), t, index_sequence_for<Args...> {}))
{
t = std::make_tuple(j.at(Idx).template get<typename std::tuple_element<Idx, Tuple>::type>()...);
from_json_tuple(std::forward<BasicJsonType>(j), t, index_sequence_for<Args...> {});
}

template<typename BasicJsonType, typename... Args>
void from_json(const BasicJsonType& j, std::tuple<Args...>& t)
auto from_json(BasicJsonType&& j, identity_tag<std::tuple<Args...>> tag)
-> decltype(from_json_tuple(std::forward<BasicJsonType>(j), std::move(tag), index_sequence_for<Args...> {}))
{
from_json_tuple_impl(j, t, index_sequence_for<Args...> {});
return from_json_tuple(std::forward<BasicJsonType>(j), std::move(tag), index_sequence_for<Args...> {});
}

template < typename BasicJsonType, typename Key, typename Value, typename Compare, typename Allocator,
Expand Down Expand Up @@ -384,11 +461,11 @@ void from_json(const BasicJsonType& j, std::unordered_map<Key, Value, Hash, KeyE
struct from_json_fn
{
template<typename BasicJsonType, typename T>
auto operator()(const BasicJsonType& j, T& val) const
noexcept(noexcept(from_json(j, val)))
-> decltype(from_json(j, val), void())
auto operator()(const BasicJsonType& j, T&& val) const
noexcept(noexcept(from_json(j, std::forward<T>(val))))
-> decltype(from_json(j, std::forward<T>(val)))
{
return from_json(j, val);
return from_json(j, std::forward<T>(val));
}
AnthonyVH marked this conversation as resolved.
Show resolved Hide resolved
};
} // namespace detail
Expand Down
10 changes: 10 additions & 0 deletions include/nlohmann/detail/meta/identity_tag.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#pragma once

namespace nlohmann
{
namespace detail
{
// dispatching helper struct
template <class T> struct identity_tag {};
} // namespace detail
} // namespace nlohmann
3 changes: 1 addition & 2 deletions include/nlohmann/detail/meta/type_traits.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,7 @@ struct is_getable
};

template<typename BasicJsonType, typename T>
struct has_from_json < BasicJsonType, T,
enable_if_t < !is_basic_json<T>::value >>
struct has_from_json < BasicJsonType, T, enable_if_t < !is_basic_json<T>::value >>
{
using serializer = typename BasicJsonType::template json_serializer<T, void>;

Expand Down
Loading