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

Default initialized iterators are not comparable #4493

Open
2 tasks done
piyooshm opened this issue Nov 14, 2024 · 5 comments
Open
2 tasks done

Default initialized iterators are not comparable #4493

piyooshm opened this issue Nov 14, 2024 · 5 comments
Assignees
Labels
kind: bug state: please discuss please discuss the issue or vote for your favorite option

Comments

@piyooshm
Copy link

piyooshm commented Nov 14, 2024

Description

I have a use-case where I iterate over collections in a generic manner (using templates). However, iteration over basic_json fails due to the statement:

        JSON_ASSERT(m_object != nullptr);

Before asserting for non-null object, shouldn't we first check of other.m_object is null too? If both are null, the operator should return true before the assertion.

Reproduction steps

nlohmann::json::iterator{} == nlohmann::json::iterator{}

Expected vs. actual results

Expected: Should return true of m_object is nullptr for both objects

Actual: Assertion fails

Minimal code example

No response

Error messages

No response

Compiler and operating system

gcc 9.3

Library version

3.11.3

Validation

@nlohmann
Copy link
Owner

I understand the issue, and this seems to be a bigger change:

  1. Right now, we demand iterators to be initialized before comparing. This is the reason for the assertion you reported.
  2. We also require the iterators to compare to be long to the same container. If this is not the case, we throw an invalid_iterator exception.

I am not sure why we introduced these requirements in the first place, but I fear that changing them now would be a breaking change which cannot be introduced in the 3.x release scheme. Any ideas on this?

@nlohmann nlohmann added the state: please discuss please discuss the issue or vote for your favorite option label Nov 16, 2024
@piyooshm
Copy link
Author

piyooshm commented Nov 19, 2024

Currently, this prevents use of JSON iterator from being used like other STL iterators in certain use-cases since the JSON iterator violates the LegacyForwardIterator equality domain as described in https://en.cppreference.com/w/cpp/named_req/ForwardIterator#:~:text=However%2C%20value,same%20empty%20sequence.

Looking at the code, it seems like the requirement may have been introduced mainly to guard the m_object-dependent implementation code following right after the asserts. So, all I'm suggesting is to return true in the equality comparison operator, in one special case, when both iterators are value-initialized (i.e. initialized using default constructor, causing m_object to be null). That case can also be seen as if both have "equal" null containers that are same. The check would continue to throw as earlier if only one of the two iterators has a null m_object. Furthermore, all other operations on the iter_impl will retain their existing behavior (and assertions) that may require them to have non-null m_object. The suggested change is specifically in the equality comparison operator to have a conforming equality domain and would be:

    template < typename IterImpl, detail::enable_if_t < (std::is_same<IterImpl, iter_impl>::value || std::is_same<IterImpl, other_iter_impl>::value), std::nullptr_t > = nullptr >
    bool operator==(const IterImpl& other) const
    {
        // if objects are not the same, the comparison is undefined
        if (JSON_HEDLEY_UNLIKELY(m_object != other.m_object))
        {
            JSON_THROW(invalid_iterator::create(212, "cannot compare iterators of different containers", m_object));
        }

+        if (m_object == nullptr)
+        {
+            return true;
+        }
-
-        JSON_ASSERT(m_object != nullptr);

        switch (m_object->m_data.m_type)
        {

It may be worth trying the change and seeing which unit tests break. I wouldn't expect any unit tests to break except for a test that explicitly tries to compare two value-initialized iterators. Do you see other possible scenarios where comparison of two value-initialized iterators may break the existing code? Can you provide any such examples to help me understand the concern/severity a little better?

@nlohmann
Copy link
Owner

Thanks for the input. I will have a look.

@nlohmann nlohmann self-assigned this Nov 21, 2024
@gregmarr
Copy link
Contributor

Should also do the same for operator<, except return true, as if they are equal, the first is not less than the second.

bool operator<(const iter_impl& other) const
{
// if objects are not the same, the comparison is undefined
if (JSON_HEDLEY_UNLIKELY(m_object != other.m_object))
{
JSON_THROW(invalid_iterator::create(212, "cannot compare iterators of different containers", m_object));
}
JSON_ASSERT(m_object != nullptr);
switch (m_object->m_data.m_type)

@gregmarr
Copy link
Contributor

I am not sure why we introduced these requirements in the first place, but I fear that changing them now would be a breaking change which cannot be introduced in the 3.x release scheme. Any ideas on this?

This will change an assert in debug and undefined behavior in release (dereferencing a nullptr) into well-defined behavior. I would say that this is not a breaking change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
kind: bug state: please discuss please discuss the issue or vote for your favorite option
Projects
None yet
Development

No branches or pull requests

3 participants