diff --git a/docs/mkdocs/docs/api/basic_json/at.md b/docs/mkdocs/docs/api/basic_json/at.md index 2d1042cc8..fa412e39b 100644 --- a/docs/mkdocs/docs/api/basic_json/at.md +++ b/docs/mkdocs/docs/api/basic_json/at.md @@ -224,5 +224,7 @@ Strong exception safety: if an exception occurs, the original value stays intact 1. Added in version 1.0.0. 2. Added in version 1.0.0. -3. Added in version 3.11.0. +3. Added in version 3.11.0. Fixed in version 3.13.0 to consistently accept `std::string_view`-convertible keys, as + already supported by [`operator[]`](operator[].md), [`value`](value.md), [`find`](find.md), and other lookup + functions. 4. Added in version 2.0.0. diff --git a/docs/mkdocs/docs/api/basic_json/contains.md b/docs/mkdocs/docs/api/basic_json/contains.md index 7f865f83c..1145273bb 100644 --- a/docs/mkdocs/docs/api/basic_json/contains.md +++ b/docs/mkdocs/docs/api/basic_json/contains.md @@ -115,5 +115,7 @@ Logarithmic in the size of the JSON object. ## Version history 1. Added in version 3.11.0. -2. Added in version 3.6.0. Extended template `KeyType` to support comparable types in version 3.11.0. +2. Added in version 3.6.0. Extended template `KeyType` to support comparable types in version 3.11.0. Fixed in + version 3.13.0 to consistently accept `std::string_view`-convertible keys, as already supported by + [`operator[]`](operator[].md), [`at`](at.md), [`value`](value.md), and other lookup functions. 3. Added in version 3.7.0. diff --git a/docs/mkdocs/docs/api/basic_json/count.md b/docs/mkdocs/docs/api/basic_json/count.md index ce51addbd..58e2eb995 100644 --- a/docs/mkdocs/docs/api/basic_json/count.md +++ b/docs/mkdocs/docs/api/basic_json/count.md @@ -80,4 +80,6 @@ This method always returns `0` when executed on a JSON type that is not an objec ## Version history 1. Added in version 3.11.0. -2. Added in version 1.0.0. Changed parameter `key` type to `KeyType&&` in version 3.11.0. +2. Added in version 1.0.0. Changed parameter `key` type to `KeyType&&` in version 3.11.0. Fixed in version 3.13.0 to + consistently accept `std::string_view`-convertible keys, as already supported by [`operator[]`](operator[].md), + [`at`](at.md), [`value`](value.md), and other lookup functions. diff --git a/docs/mkdocs/docs/api/basic_json/erase.md b/docs/mkdocs/docs/api/basic_json/erase.md index d1e6d6d22..47531fed8 100644 --- a/docs/mkdocs/docs/api/basic_json/erase.md +++ b/docs/mkdocs/docs/api/basic_json/erase.md @@ -213,5 +213,7 @@ Strong exception safety: if an exception occurs, the original value stays intact 1. Added in version 1.0.0. Added support for binary types in version 3.8.0. 2. Added in version 1.0.0. Added support for binary types in version 3.8.0. 3. Added in version 1.0.0. -4. Added in version 3.11.0. +4. Added in version 3.11.0. Fixed in version 3.13.0 to consistently accept `std::string_view`-convertible keys, as + already supported by [`operator[]`](operator[].md), [`at`](at.md), [`value`](value.md), and other lookup + functions. 5. Added in version 1.0.0. diff --git a/docs/mkdocs/docs/api/basic_json/find.md b/docs/mkdocs/docs/api/basic_json/find.md index 35ff9dcb2..87f538454 100644 --- a/docs/mkdocs/docs/api/basic_json/find.md +++ b/docs/mkdocs/docs/api/basic_json/find.md @@ -84,4 +84,6 @@ This method always returns `end()` when executed on a JSON type that is not an o ## Version history 1. Added in version 3.11.0. -2. Added in version 1.0.0. Changed to support comparable types in version 3.11.0. +2. Added in version 1.0.0. Changed to support comparable types in version 3.11.0. Fixed in version 3.13.0 to + consistently accept `std::string_view`-convertible keys, as already supported by [`operator[]`](operator[].md), + [`at`](at.md), [`value`](value.md), and other lookup functions. diff --git a/docs/mkdocs/docs/api/basic_json/value.md b/docs/mkdocs/docs/api/basic_json/value.md index 2e9b85a2b..8aa8969ec 100644 --- a/docs/mkdocs/docs/api/basic_json/value.md +++ b/docs/mkdocs/docs/api/basic_json/value.md @@ -185,7 +185,9 @@ changes to any JSON value. ## Version history 1. Added in version 1.0.0. Changed parameter `default_value` type from `const ValueType&` to `ValueType&&` in version 3.11.0. -2. Added in version 3.11.0. Made `ValueType` the first template parameter in version 3.11.2. +2. Added in version 3.11.0. Made `ValueType` the first template parameter in version 3.11.2. Fixed in version 3.13.0 + to consistently accept `std::string_view`-convertible keys, as already supported by + [`operator[]`](operator[].md), [`at`](at.md), [`find`](find.md), and other lookup functions. 3. Added in version 2.0.2. Extended to work with arrays in version 3.13.0, including fixing an issue where resolving `ptr` through an array unexpectedly threw `out_of_range` instead of returning the resolved element (or `default_value`, as documented). diff --git a/include/nlohmann/detail/meta/type_traits.hpp b/include/nlohmann/detail/meta/type_traits.hpp index 6f8bf2a3d..ee344ee28 100644 --- a/include/nlohmann/detail/meta/type_traits.hpp +++ b/include/nlohmann/detail/meta/type_traits.hpp @@ -748,6 +748,30 @@ using is_usable_as_key_type = typename std::conditional < std::true_type, std::false_type >::type; +#ifdef JSON_HAS_CPP_17 +// type trait to check if KeyType can only be used as an object key after +// converting it to std::string_view: it is convertible to std::string_view, the +// object's comparator cannot compare it with object_t::key_type directly, but +// can compare a std::string_view. JSON pointers and JSON iterators are ruled out +// first, so that the conversion checks are never instantiated for them (a JSON +// pointer's deprecated conversion to string_t would be named otherwise). +template < typename BasicJsonType, typename KeyTypeCVRef, typename KeyType = uncvref_t, + bool = is_json_pointer::value || is_json_iterator_of::value > +struct is_string_view_convertible_key_type : std::false_type {}; + +template +struct is_string_view_convertible_key_type + : std::integral_constant < bool, + std::is_convertible::value + && !is_usable_as_key_type::value + && is_usable_as_key_type::value > {}; +#else +template +struct is_string_view_convertible_key_type : std::false_type {}; +#endif + // type trait to check if KeyType can be used as an object key // true if: // - KeyType is comparable with BasicJsonType::object_t::key_type @@ -761,9 +785,7 @@ using is_usable_as_basic_json_key_type = typename std::conditional < typename BasicJsonType::object_t::key_type, KeyTypeCVRef, RequireTransparentComparator, ExcludeObjectKeyType>::value && !is_json_iterator_of::value) -#ifdef JSON_HAS_CPP_17 - || std::is_convertible::value -#endif + || is_string_view_convertible_key_type::value , std::true_type, std::false_type >::type; diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index 500fcddf2..2df71d890 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -805,6 +805,24 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec return it; } + /// @brief the key to look up an object member with: the key itself, or its + /// std::string_view if the object can only be searched with that + template < typename KeyType, detail::enable_if_t < + !detail::is_string_view_convertible_key_type::value, int > = 0 > + static KeyType && lookup_key(KeyType && key) noexcept + { + return std::forward(key); + } + +#ifdef JSON_HAS_CPP_17 + template < typename KeyType, detail::enable_if_t < + detail::is_string_view_convertible_key_type::value, int > = 0 > + static std::string_view lookup_key(KeyType && key) + { + return std::forward(key); + } +#endif + /// @brief erase an element from the object and return the following one /// Not every map returns an iterator from erase(iterator): some containers /// (e.g., Abseil's hash maps) return void to avoid computing a successor @@ -2766,7 +2784,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec JSON_THROW(type_error::create(304, detail::concat("cannot use at() with ", type_name()), this)); } - auto it = m_data.m_value.object->find(std::forward(key)); + auto it = m_data.m_value.object->find(lookup_key(std::forward(key))); if (it == m_data.m_value.object->end()) { JSON_THROW(out_of_range::create(403, detail::concat("key '", string_t(std::forward(key)), "' not found"), this)); @@ -2804,7 +2822,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec JSON_THROW(type_error::create(304, detail::concat("cannot use at() with ", type_name()), this)); } - auto it = m_data.m_value.object->find(std::forward(key)); + auto it = m_data.m_value.object->find(lookup_key(std::forward(key))); if (it == m_data.m_value.object->end()) { JSON_THROW(out_of_range::create(403, detail::concat("key '", string_t(std::forward(key)), "' not found"), this)); @@ -2940,7 +2958,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // operator[] only works for objects if (JSON_HEDLEY_LIKELY(is_object())) { - auto result = m_data.m_value.object->emplace(std::forward(key), nullptr); + auto result = m_data.m_value.object->emplace(lookup_key(std::forward(key)), nullptr); return set_parent(result.first->second); } @@ -2956,7 +2974,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // const operator[] only works for objects if (JSON_HEDLEY_LIKELY(is_object())) { - auto it = m_data.m_value.object->find(std::forward(key)); + auto it = m_data.m_value.object->find(lookup_key(std::forward(key))); JSON_ASSERT(it != m_data.m_value.object->end()); return it->second; } @@ -2966,8 +2984,10 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec private: template - using is_comparable_with_object_key = detail::is_comparable < - object_comparator_t, const typename object_t::key_type&, KeyType >; + using is_comparable_with_object_key = std::integral_constant < bool, + detail::is_comparable < + object_comparator_t, const typename object_t::key_type&, KeyType >::value + || detail::is_string_view_convertible_key_type::value >; template using value_return_type = std::conditional < @@ -3350,7 +3370,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec JSON_THROW(type_error::create(307, detail::concat("cannot use erase() with ", type_name()), this)); } - const auto it = m_data.m_value.object->find(std::forward(key)); + const auto it = m_data.m_value.object->find(lookup_key(std::forward(key))); if (it != m_data.m_value.object->end()) { m_data.m_value.object->erase(it); @@ -3377,7 +3397,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec detail::is_usable_as_basic_json_key_type::value, int> = 0> size_type erase(KeyType && key) { - return erase_internal(std::forward(key)); + return erase_internal(lookup_key(std::forward(key))); } /// @brief remove element from a JSON array given an index @@ -3447,7 +3467,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec if (is_object()) { - result.m_it.object_iterator = m_data.m_value.object->find(std::forward(key)); + result.m_it.object_iterator = m_data.m_value.object->find(lookup_key(std::forward(key))); } return result; @@ -3463,7 +3483,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec if (is_object()) { - result.m_it.object_iterator = m_data.m_value.object->find(std::forward(key)); + result.m_it.object_iterator = m_data.m_value.object->find(lookup_key(std::forward(key))); } return result; @@ -3486,7 +3506,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec size_type count(KeyType && key) const { // return 0 for all nonobject types - return is_object() ? m_data.m_value.object->count(std::forward(key)) : 0; + return is_object() ? m_data.m_value.object->count(lookup_key(std::forward(key))) : 0; } /// @brief check the existence of an element in a JSON object @@ -3504,7 +3524,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec JSON_HEDLEY_WARN_UNUSED_RESULT bool contains(KeyType && key) const { - return is_object() && m_data.m_value.object->find(std::forward(key)) != m_data.m_value.object->end(); + return is_object() && m_data.m_value.object->find(lookup_key(std::forward(key))) != m_data.m_value.object->end(); } /// @brief check the existence of an element in a JSON object given a JSON pointer diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 576498738..132e001f9 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -4758,6 +4758,30 @@ using is_usable_as_key_type = typename std::conditional < std::true_type, std::false_type >::type; +#ifdef JSON_HAS_CPP_17 +// type trait to check if KeyType can only be used as an object key after +// converting it to std::string_view: it is convertible to std::string_view, the +// object's comparator cannot compare it with object_t::key_type directly, but +// can compare a std::string_view. JSON pointers and JSON iterators are ruled out +// first, so that the conversion checks are never instantiated for them (a JSON +// pointer's deprecated conversion to string_t would be named otherwise). +template < typename BasicJsonType, typename KeyTypeCVRef, typename KeyType = uncvref_t, + bool = is_json_pointer::value || is_json_iterator_of::value > +struct is_string_view_convertible_key_type : std::false_type {}; + +template +struct is_string_view_convertible_key_type + : std::integral_constant < bool, + std::is_convertible::value + && !is_usable_as_key_type::value + && is_usable_as_key_type::value > {}; +#else +template +struct is_string_view_convertible_key_type : std::false_type {}; +#endif + // type trait to check if KeyType can be used as an object key // true if: // - KeyType is comparable with BasicJsonType::object_t::key_type @@ -4771,9 +4795,7 @@ using is_usable_as_basic_json_key_type = typename std::conditional < typename BasicJsonType::object_t::key_type, KeyTypeCVRef, RequireTransparentComparator, ExcludeObjectKeyType>::value && !is_json_iterator_of::value) -#ifdef JSON_HAS_CPP_17 - || std::is_convertible::value -#endif + || is_string_view_convertible_key_type::value , std::true_type, std::false_type >::type; @@ -26886,6 +26908,24 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec return it; } + /// @brief the key to look up an object member with: the key itself, or its + /// std::string_view if the object can only be searched with that + template < typename KeyType, detail::enable_if_t < + !detail::is_string_view_convertible_key_type::value, int > = 0 > + static KeyType && lookup_key(KeyType && key) noexcept + { + return std::forward(key); + } + +#ifdef JSON_HAS_CPP_17 + template < typename KeyType, detail::enable_if_t < + detail::is_string_view_convertible_key_type::value, int > = 0 > + static std::string_view lookup_key(KeyType && key) + { + return std::forward(key); + } +#endif + /// @brief erase an element from the object and return the following one /// Not every map returns an iterator from erase(iterator): some containers /// (e.g., Abseil's hash maps) return void to avoid computing a successor @@ -28847,7 +28887,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec JSON_THROW(type_error::create(304, detail::concat("cannot use at() with ", type_name()), this)); } - auto it = m_data.m_value.object->find(std::forward(key)); + auto it = m_data.m_value.object->find(lookup_key(std::forward(key))); if (it == m_data.m_value.object->end()) { JSON_THROW(out_of_range::create(403, detail::concat("key '", string_t(std::forward(key)), "' not found"), this)); @@ -28885,7 +28925,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec JSON_THROW(type_error::create(304, detail::concat("cannot use at() with ", type_name()), this)); } - auto it = m_data.m_value.object->find(std::forward(key)); + auto it = m_data.m_value.object->find(lookup_key(std::forward(key))); if (it == m_data.m_value.object->end()) { JSON_THROW(out_of_range::create(403, detail::concat("key '", string_t(std::forward(key)), "' not found"), this)); @@ -29021,7 +29061,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // operator[] only works for objects if (JSON_HEDLEY_LIKELY(is_object())) { - auto result = m_data.m_value.object->emplace(std::forward(key), nullptr); + auto result = m_data.m_value.object->emplace(lookup_key(std::forward(key)), nullptr); return set_parent(result.first->second); } @@ -29037,7 +29077,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // const operator[] only works for objects if (JSON_HEDLEY_LIKELY(is_object())) { - auto it = m_data.m_value.object->find(std::forward(key)); + auto it = m_data.m_value.object->find(lookup_key(std::forward(key))); JSON_ASSERT(it != m_data.m_value.object->end()); return it->second; } @@ -29047,8 +29087,10 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec private: template - using is_comparable_with_object_key = detail::is_comparable < - object_comparator_t, const typename object_t::key_type&, KeyType >; + using is_comparable_with_object_key = std::integral_constant < bool, + detail::is_comparable < + object_comparator_t, const typename object_t::key_type&, KeyType >::value + || detail::is_string_view_convertible_key_type::value >; template using value_return_type = std::conditional < @@ -29431,7 +29473,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec JSON_THROW(type_error::create(307, detail::concat("cannot use erase() with ", type_name()), this)); } - const auto it = m_data.m_value.object->find(std::forward(key)); + const auto it = m_data.m_value.object->find(lookup_key(std::forward(key))); if (it != m_data.m_value.object->end()) { m_data.m_value.object->erase(it); @@ -29458,7 +29500,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec detail::is_usable_as_basic_json_key_type::value, int> = 0> size_type erase(KeyType && key) { - return erase_internal(std::forward(key)); + return erase_internal(lookup_key(std::forward(key))); } /// @brief remove element from a JSON array given an index @@ -29528,7 +29570,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec if (is_object()) { - result.m_it.object_iterator = m_data.m_value.object->find(std::forward(key)); + result.m_it.object_iterator = m_data.m_value.object->find(lookup_key(std::forward(key))); } return result; @@ -29544,7 +29586,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec if (is_object()) { - result.m_it.object_iterator = m_data.m_value.object->find(std::forward(key)); + result.m_it.object_iterator = m_data.m_value.object->find(lookup_key(std::forward(key))); } return result; @@ -29567,7 +29609,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec size_type count(KeyType && key) const { // return 0 for all nonobject types - return is_object() ? m_data.m_value.object->count(std::forward(key)) : 0; + return is_object() ? m_data.m_value.object->count(lookup_key(std::forward(key))) : 0; } /// @brief check the existence of an element in a JSON object @@ -29585,7 +29627,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec JSON_HEDLEY_WARN_UNUSED_RESULT bool contains(KeyType && key) const { - return is_object() && m_data.m_value.object->find(std::forward(key)) != m_data.m_value.object->end(); + return is_object() && m_data.m_value.object->find(lookup_key(std::forward(key))) != m_data.m_value.object->end(); } /// @brief check the existence of an element in a JSON object given a JSON pointer diff --git a/tests/src/unit-element_access2.cpp b/tests/src/unit-element_access2.cpp index 40e8216d5..1647c422a 100644 --- a/tests/src/unit-element_access2.cpp +++ b/tests/src/unit-element_access2.cpp @@ -1892,4 +1892,115 @@ TEST_CASE("operator[] with user-defined std::string_view-convertible types") } } } + +TEST_CASE("keys convertible to std::string_view work with all lookup functions (regression test for #5663)") +{ + // a key type convertible only to std::string_view: the case #4958 added + // support for, but only the non-const operator[] compiled with it + struct ViewKey + { + operator std::string_view() const + { + return "a"; + } + }; + + // a key type convertible to both std::string and std::string_view: with + // 3.12.0, such a key worked with at, the const operator[], find, count and + // contains via the conversion to std::string; #4958 made the KeyType&& + // templates win overload resolution for it instead, and those then failed + struct DualKey + { + operator std::string() const + { + return "a"; + } + operator std::string_view() const + { + return "a"; + } + }; + + SECTION("nlohmann::json") + { + using json = nlohmann::json; + + SECTION("ViewKey") + { + json j = {{"a", 1}}; + const json& cj = j; + + CHECK(j[ViewKey{}] == 1); + CHECK(cj[ViewKey{}] == 1); + CHECK(j.at(ViewKey{}) == 1); + CHECK(cj.at(ViewKey{}) == 1); + CHECK(j.find(ViewKey{}) != j.end()); + CHECK(cj.find(ViewKey{}) != cj.end()); + CHECK(j.count(ViewKey{}) == 1); + CHECK(j.contains(ViewKey{})); + CHECK(j.value(ViewKey{}, 0) == 1); + CHECK(j.erase(ViewKey{}) == 1); + CHECK(!j.contains("a")); + } + + SECTION("DualKey") + { + json j = {{"a", 1}}; + const json& cj = j; + + CHECK(j[DualKey{}] == 1); + CHECK(cj[DualKey{}] == 1); + CHECK(j.at(DualKey{}) == 1); + CHECK(cj.at(DualKey{}) == 1); + CHECK(j.find(DualKey{}) != j.end()); + CHECK(cj.find(DualKey{}) != cj.end()); + CHECK(j.count(DualKey{}) == 1); + CHECK(j.contains(DualKey{})); + CHECK(j.value(DualKey{}, 0) == 1); + CHECK(j.erase(DualKey{}) == 1); + CHECK(!j.contains("a")); + } + } + + SECTION("nlohmann::ordered_json") + { + using ordered_json = nlohmann::ordered_json; + + SECTION("ViewKey") + { + ordered_json j = {{"a", 1}}; + const ordered_json& cj = j; + + CHECK(j[ViewKey{}] == 1); + CHECK(cj[ViewKey{}] == 1); + CHECK(j.at(ViewKey{}) == 1); + CHECK(cj.at(ViewKey{}) == 1); + CHECK(j.find(ViewKey{}) != j.end()); + CHECK(cj.find(ViewKey{}) != cj.end()); + CHECK(j.count(ViewKey{}) == 1); + CHECK(j.contains(ViewKey{})); + CHECK(j.value(ViewKey{}, 0) == 1); + CHECK(j.erase(ViewKey{}) == 1); + CHECK(!j.contains("a")); + } + + SECTION("DualKey") + { + ordered_json j = {{"a", 1}}; + const ordered_json& cj = j; + + CHECK(j[DualKey{}] == 1); + CHECK(cj[DualKey{}] == 1); + CHECK(j.at(DualKey{}) == 1); + CHECK(cj.at(DualKey{}) == 1); + CHECK(j.find(DualKey{}) != j.end()); + CHECK(cj.find(DualKey{}) != cj.end()); + CHECK(j.count(DualKey{}) == 1); + CHECK(j.contains(DualKey{})); + CHECK(j.value(DualKey{}, 0) == 1); + CHECK(j.erase(DualKey{}) == 1); + CHECK(!j.contains("a")); + } + } +} #endif