Skip to content

Commit

Permalink
meta: fix is_compatible/constructible traits (#3020)
Browse files Browse the repository at this point in the history
The previous version relied on the existence of an 'iterator' type.

As mentioned in comments, this is not the proper way to do it and
causes issues with certain types (e.g. views from range-v3).

Add a 'is_range' trait that properly detects the return type of
'begin'/'end', and use it in instead.
  • Loading branch information
theodelrieu committed Oct 7, 2021
1 parent 62f2997 commit 80df5e8
Show file tree
Hide file tree
Showing 7 changed files with 313 additions and 124 deletions.
42 changes: 41 additions & 1 deletion include/nlohmann/detail/macro_scope.hpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#pragma once

#include <utility> // pair
#include <utility> // declval, pair
#include <nlohmann/thirdparty/hedley/hedley.hpp>
#include <nlohmann/detail/meta/detected.hpp>

// This file contains all internal macro definitions
// You MUST include macro_unscope.hpp at the end of json.hpp to undef all of them
Expand Down Expand Up @@ -292,6 +293,45 @@
inline void to_json(nlohmann::json& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \
inline void from_json(const nlohmann::json& nlohmann_json_j, Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) }


// inspired from https://stackoverflow.com/a/26745591
// allows to call any std function as if (e.g. with begin):
// using std::begin; begin(x);
//
// it allows using the detected idiom to retrieve the return type
// of such an expression
#define NLOHMANN_CAN_CALL_STD_FUNC_IMPL(std_name) \
namespace detail { \
using std::std_name; \
\
template<typename... T> \
using result_of_##std_name = decltype(std_name(std::declval<T>()...)); \
} \
\
namespace detail2 { \
struct std_name##_tag \
{ \
}; \
\
template<typename... T> \
std_name##_tag std_name(T&&...); \
\
template<typename... T> \
using result_of_##std_name = decltype(std_name(std::declval<T>()...)); \
\
template<typename... T> \
struct would_call_std_##std_name \
{ \
static constexpr auto const value = ::nlohmann::detail:: \
is_detected_exact<std_name##_tag, result_of_##std_name, T...>::value; \
}; \
} /* namespace detail2 */ \
\
template<typename... T> \
struct would_call_std_##std_name : detail2::would_call_std_##std_name<T...> \
{ \
}

#ifndef JSON_USE_IMPLICIT_CONVERSIONS
#define JSON_USE_IMPLICIT_CONVERSIONS 1
#endif
Expand Down
1 change: 1 addition & 0 deletions include/nlohmann/detail/macro_unscope.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@
#undef NLOHMANN_BASIC_JSON_TPL_DECLARATION
#undef NLOHMANN_BASIC_JSON_TPL
#undef JSON_EXPLICIT
#undef NLOHMANN_CAN_CALL_STD_FUNC_IMPL

#include <nlohmann/thirdparty/hedley/hedley_undef.hpp>
8 changes: 8 additions & 0 deletions include/nlohmann/detail/meta/call_std/begin.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#pragma once

#include <nlohmann/detail/macro_scope.hpp>

namespace nlohmann
{
NLOHMANN_CAN_CALL_STD_FUNC_IMPL(begin);
} // namespace nlohmann
8 changes: 8 additions & 0 deletions include/nlohmann/detail/meta/call_std/end.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#pragma once

#include <nlohmann/detail/macro_scope.hpp>

namespace nlohmann
{
NLOHMANN_CAN_CALL_STD_FUNC_IMPL(end);
} // namespace nlohmann
76 changes: 48 additions & 28 deletions include/nlohmann/detail/meta/type_traits.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
#include <utility> // declval
#include <tuple> // tuple

#include <nlohmann/detail/iterators/iterator_traits.hpp>
#include <nlohmann/detail/macro_scope.hpp>

#include <nlohmann/detail/iterators/iterator_traits.hpp>
#include <nlohmann/detail/meta/call_std/begin.hpp>
#include <nlohmann/detail/meta/call_std/end.hpp>
#include <nlohmann/detail/meta/cpp_future.hpp>
#include <nlohmann/detail/meta/detected.hpp>
#include <nlohmann/json_fwd.hpp>
Expand Down Expand Up @@ -79,9 +82,6 @@ using reference_t = typename T::reference;
template<typename T>
using iterator_category_t = typename T::iterator_category;

template<typename T>
using iterator_t = typename T::iterator;

template<typename T, typename... Args>
using to_json_function = decltype(T::to_json(std::declval<Args>()...));

Expand Down Expand Up @@ -217,6 +217,31 @@ struct is_iterator_traits<iterator_traits<T>>
is_detected<reference_t, traits>::value;
};

template<typename T>
struct is_range
{
private:
using t_ref = typename std::add_lvalue_reference<T>::type;

using iterator = detected_t<result_of_begin, t_ref>;
using sentinel = detected_t<result_of_end, t_ref>;

// to be 100% correct, it should use https://en.cppreference.com/w/cpp/iterator/input_or_output_iterator
// and https://en.cppreference.com/w/cpp/iterator/sentinel_for
// but reimplementing these would be too much work, as a lot of other concepts are used underneath
static constexpr auto is_iterator_begin =
is_iterator_traits<iterator_traits<iterator>>::value;

public:
static constexpr bool value = !std::is_same<iterator, nonesuch>::value && !std::is_same<sentinel, nonesuch>::value && is_iterator_begin;
};

template<typename R>
using iterator_t = enable_if_t<is_range<R>::value, result_of_begin<decltype(std::declval<R&>())>>;

template<typename T>
using range_value_t = value_type_t<iterator_traits<iterator_t<T>>>;

// The following implementation of is_complete_type is taken from
// https://blogs.msdn.microsoft.com/vcblog/2015/12/02/partial-support-for-expression-sfinae-in-vs-2015-update-1/
// and is written by Xiang Fan who agreed to using it in this library.
Expand Down Expand Up @@ -291,8 +316,9 @@ struct is_compatible_string_type_impl : std::false_type {};
template<typename BasicJsonType, typename CompatibleStringType>
struct is_compatible_string_type_impl <
BasicJsonType, CompatibleStringType,
enable_if_t<is_detected_exact<typename BasicJsonType::string_t::value_type,
value_type_t, CompatibleStringType>::value >>
enable_if_t<is_detected_convertible<typename BasicJsonType::string_t::value_type,
range_value_t,
CompatibleStringType>::value >>
{
static constexpr auto value =
is_constructible<typename BasicJsonType::string_t, CompatibleStringType>::value;
Expand Down Expand Up @@ -327,17 +353,13 @@ struct is_compatible_array_type_impl : std::false_type {};
template<typename BasicJsonType, typename CompatibleArrayType>
struct is_compatible_array_type_impl <
BasicJsonType, CompatibleArrayType,
enable_if_t < is_detected<value_type_t, CompatibleArrayType>::value&&
enable_if_t <
is_detected<iterator_t, CompatibleArrayType>::value&&
// This is needed because json_reverse_iterator has a ::iterator type...
// Therefore it is detected as a CompatibleArrayType.
// The real fix would be to have an Iterable concept.
!is_iterator_traits <
iterator_traits<CompatibleArrayType >>::value >>
is_iterator_traits<iterator_traits<detected_t<iterator_t, CompatibleArrayType>>>::value >>
{
static constexpr bool value =
is_constructible<BasicJsonType,
typename CompatibleArrayType::value_type>::value;
range_value_t<CompatibleArrayType>>::value;
};

template<typename BasicJsonType, typename CompatibleArrayType>
Expand All @@ -359,28 +381,26 @@ struct is_constructible_array_type_impl <
BasicJsonType, ConstructibleArrayType,
enable_if_t < !std::is_same<ConstructibleArrayType,
typename BasicJsonType::value_type>::value&&
!is_compatible_string_type<BasicJsonType, ConstructibleArrayType>::value&&
is_default_constructible<ConstructibleArrayType>::value&&
(std::is_move_assignable<ConstructibleArrayType>::value ||
std::is_copy_assignable<ConstructibleArrayType>::value)&&
is_detected<value_type_t, ConstructibleArrayType>::value&&
is_detected<iterator_t, ConstructibleArrayType>::value&&
is_iterator_traits<iterator_traits<detected_t<iterator_t, ConstructibleArrayType>>>::value&&
is_detected<range_value_t, ConstructibleArrayType>::value&&
is_complete_type <
detected_t<value_type_t, ConstructibleArrayType >>::value >>
detected_t<range_value_t, ConstructibleArrayType >>::value >>
{
using value_type = range_value_t<ConstructibleArrayType>;

static constexpr bool value =
// This is needed because json_reverse_iterator has a ::iterator type,
// furthermore, std::back_insert_iterator (and other iterators) have a
// base class `iterator`... Therefore it is detected as a
// ConstructibleArrayType. The real fix would be to have an Iterable
// concept.
!is_iterator_traits<iterator_traits<ConstructibleArrayType>>::value &&

(std::is_same<typename ConstructibleArrayType::value_type,
typename BasicJsonType::array_t::value_type>::value ||
has_from_json<BasicJsonType,
typename ConstructibleArrayType::value_type>::value ||
has_non_default_from_json <
BasicJsonType, typename ConstructibleArrayType::value_type >::value);
std::is_same<value_type,
typename BasicJsonType::array_t::value_type>::value ||
has_from_json<BasicJsonType,
value_type>::value ||
has_non_default_from_json <
BasicJsonType,
value_type >::value;
};

template<typename BasicJsonType, typename ConstructibleArrayType>
Expand Down
Loading

0 comments on commit 80df5e8

Please sign in to comment.