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

Fix conversion from empty json to std::optional<> #2229

Draft
wants to merge 11 commits into
base: develop
Choose a base branch
from

Conversation

karzhenkov
Copy link
Contributor

@karzhenkov karzhenkov commented Jun 28, 2020

Here is possible solution (or workaround) for issues in PR #2117, #2213.
The problem is caused by "wrong" user defined conversion selected by overload resolution. Compiler selects a converting constructor for std::optional<std::string>, while template <...> json::operator ValueType is needed. This behavoir is correct because json can be implicitly converted to std::string. The constructor is unaware about from_json function.
"Non-template" version of conversion operator presented here "adjusts" the resolution process. However, the desired result is achieved for copy initailization only. Direct initialization still selects the converting constructor (this probably cannot be changed).

@karzhenkov
Copy link
Contributor Author

Is the following really needed?

...\test\src\unit-concepts.cpp(124): ERROR: CHECK( std::is_standard_layout<json>::value ) is NOT correct!

@nlohmann
Copy link
Owner

That assertion held true so far. If that changes now, this may indicate a breaking change.

@karzhenkov
Copy link
Contributor Author

karzhenkov commented Jun 28, 2020

Modified json may have two (or more) base classes of the same type, and thus not to be StandardLayout. This may happen if there are identical types in argument pack for optional_converter_helper. Possible effects need to be explored.

@coveralls
Copy link

coveralls commented Jun 28, 2020

Coverage Status

Coverage: 100.0%. Remained the same when pulling 14e3318 on karzhenkov:fix-conv-to-optional into 4c6cde7 on nlohmann:develop.

@karzhenkov
Copy link
Contributor Author

There are no identical bases. Moreover, direct identical bases are not allowed.

@dota17
Copy link
Contributor

dota17 commented Jun 29, 2020

Direct initialization still selects the converting constructor (this probably cannot be changed).

As i said in #2213, this is why an exception is raised when calling std::optional<std::string>(j_null). It is a problem of std::optional not this json library, i think.
IMO, we can add notes in documents to tell people that they should not use std::optional<std::string>(j_null) directly if it still raise an exception.

std::optional<std::string> opt_null;

CHECK(json(opt_null) == j_null);
CHECK_THROWS_WITH(std::optional<std::string>(j_null),
Copy link
Contributor

@dota17 dota17 Jun 29, 2020

Choose a reason for hiding this comment

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

Weird. In my local env, the exception would not raise. By from CI online logs , it will get the exception.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What is special in your local environment?

Copy link
Contributor

Choose a reason for hiding this comment

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

ubuntu 18.04
gcc 7.4
cmake 3.10.2

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Somewhat has changed since GCC 7.4
https://godbolt.org/z/4MfL8i

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is probably a bug in GCC 7. When used with "-std=c++17" it may perform wrong overload resolution for user-defined conversion. In previous example and also here (with no std::optional involved) it behaves as if the implicit copy constructor is selected, whereas there is a more suitable template constructor. Other major releases of GCC (including older versions) prefer template constructor.

@nlohmann
Copy link
Owner

IMO, we can add notes in documents to tell people that they should not use std::optional<std::string>(j_null) directly if it still raise an exception.

I do not like this approach. I cannot believe this is an issue in std::optional. It feels like a bug in the library. If we cannot properly support a quite frequent type like std::optional<std::string>, then maybe we should not add support for std::optional to the library in the first place as this would be highly confusing.

@bhardwajs
Copy link

IMO, we can add notes in documents to tell people that they should not use std::optional<std::string>(j_null) directly if it still raise an exception.

I do not like this approach. I cannot believe this is an issue in std::optional. It feels like a bug in the library. If we cannot properly support a quite frequent type like std::optional<std::string>, then maybe we should not add support for std::optional to the library in the first place as this would be highly confusing.

Are there specific STL implementations where the issue with std::optional is surfacing? gcc, clang, and MSVC don't always have the same implementations.

@karzhenkov
Copy link
Contributor Author

karzhenkov commented Jun 29, 2020

It seems the issue is caused by std::optional intented desing and language standard, not by implementation features.
Perhaps it would be better to disable implicit conversions from json to std::string, bool, etc. When doing so, the overload resolution will prefer conversion operator defined in json over converting constructor of std::optional. The conversion operator will call the proper from_json function. But, оf course, this is a breaking change.
On the other hand, it may be sufficient to 'disable' direct initialization by throwing an exception. A bit ugly, but maybe not so bad.

@karzhenkov
Copy link
Contributor Author

MSVC has bad std::is_standard_layout. Here is a sample.
By the way, I see no reason why StandardLayoutType concept may be useful when using json.

@dota17
Copy link
Contributor

dota17 commented Jul 1, 2020

What is more, std::optional<std::string>(j_null) still raises an exception.
I added logs to check SECTION("string") and bool, number. Unfortunally, I found that they still didn't call the expected method - from_json(basic_json, std::optional).

Comment on lines 139 to 140
// should return true
return std::is_standard_layout<D>::value;
Copy link
Contributor Author

@karzhenkov karzhenkov Jul 5, 2020

Choose a reason for hiding this comment

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

Microsoft doesn't consider their implemenation wrong. Their story is about empty base optimization, backward binary compatibility and non-standard extension __declspec(empty_bases) which can be used to achieve standard behavior.

@stale
Copy link

stale bot commented Aug 8, 2020

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the state: stale the issue has not been updated in a while and will be closed automatically soon unless it is updated label Aug 8, 2020
@nlohmann nlohmann removed the state: stale the issue has not been updated in a while and will be closed automatically soon unless it is updated label Aug 9, 2020
@stale
Copy link

stale bot commented Oct 4, 2020

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the state: stale the issue has not been updated in a while and will be closed automatically soon unless it is updated label Oct 4, 2020
@nlohmann nlohmann removed the state: stale the issue has not been updated in a while and will be closed automatically soon unless it is updated label Oct 5, 2020
@stale
Copy link

stale bot commented Dec 25, 2020

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the state: stale the issue has not been updated in a while and will be closed automatically soon unless it is updated label Dec 25, 2020
@karzhenkov karzhenkov marked this pull request as draft December 31, 2020 05:59
@stale stale bot removed the state: stale the issue has not been updated in a while and will be closed automatically soon unless it is updated label Dec 31, 2020
@nlohmann
Copy link
Owner

Same question as with #2117 - how to proceed here?

@karzhenkov
Copy link
Contributor Author

Here is a "better" optional that probably allows to achieve the desired behavior. The nlohmann::optional class is derived from its standard counterpart and defines some constructors. The goal of these definitions is to change the preferred conversion performed by converting constructor. If the object being converted has operator optional<T>, this operator will be called by corresponding constructor. Other constructors are defined to resemble std::optional in any other initialization scenario.

@gregmarr
Copy link
Contributor

@karzhenkov This PR is conflicting again. Can you update?
@nlohmann Anything besides the merge conflict still outstanding here?

@nlohmann nlohmann added the please rebase Please rebase your branch to origin/develop label Jun 18, 2022
@karzhenkov karzhenkov force-pushed the fix-conv-to-optional branch 3 times, most recently from e0ff46c to b7a5d99 Compare June 21, 2022 17:40
Copy link
Contributor

@falbrechtskirchinger falbrechtskirchinger left a comment

Choose a reason for hiding this comment

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

I'd like to evaluate this branch in one of my projects to make sure there aren't any ugly side effects. Afterward, I'll come back for a more in-depth review.

tests/src/unit-optional.cpp Outdated Show resolved Hide resolved
tests/src/unit-optional.cpp Outdated Show resolved Hide resolved
@@ -51,15 +51,15 @@ using nlohmann::json;
// NLOHMANN_JSON_SERIALIZE_ENUM uses a static std::pair
DOCTEST_CLANG_SUPPRESS_WARNING_PUSH
DOCTEST_CLANG_SUPPRESS_WARNING("-Wexit-time-destructors")
DOCTEST_CLANG_SUPPRESS_WARNING("-Wunused-macros")
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it be better to just move the *_CONVERSION_WARNING macros into the #if JSON_HAS_CPP_17 block?
You're getting this warning because, in the C++11 build, the macros aren't used.

Copy link
Contributor Author

@karzhenkov karzhenkov Jun 22, 2022

Choose a reason for hiding this comment

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

I considered it, but I think it's hardly much better. I preferred a slightly more concise variant.

@karzhenkov
Copy link
Contributor Author

It's not clear if std::optional has noexcept default constructor in GCC 8.1. Actually, it isn't declared noexcept there. This can be checked in the example at godbolt. However, after the local variable definition in the example is uncommented, std::optional default constructor looks noexcept. I guess, there are both library and compiler bugs.

@nlohmann
Copy link
Owner

FYI: I suppressed the warnings for non-explicit constructors in Codacy.

@karzhenkov
Copy link
Contributor Author

The remaining problems are related to three-way comparisons in C++20. I hope to fix them when I have time.

@karzhenkov
Copy link
Contributor Author

The problem boils down to the following example, which complies in C++17 but not in C++20:

#include <optional>

struct opt : std::optional<int> {};

bool operator == (const opt&, const opt&);
bool operator >= (const opt&, const opt&);

int main()
{
    sizeof(opt() == opt());
    sizeof(opt() >= opt());
}

If any of the operator declarations is removed along with the corresponding check, the code compiles regardless of the language standard.

@falbrechtskirchinger
Copy link
Contributor

Digging a little bit deeper, the code triggers a recursion in __partially_ordered_with via std::operator <=>.
For std::optional, that recursion is broken via the constraint __is_optional_v.

Adding a specilization to std (for testing purposes only, of course!) makes the code compile without issue.

namespace std {
template<>
inline constexpr bool __is_optional_v<opt> = true;
}

How do we work around this?

@karzhenkov
Copy link
Contributor Author

How do we work around this?

It looks like a defect in the C++20 standard (or maybe in the standard library implementation). I think it is impossible to correctly define comparisons for a class derived from std::optional in C++20. My initial approach was to adjust the class constructors, but that doesn't seem viable now. Another option is to completely redefine the class, which also has disadvantages. All functionality needs to be duplicated and maintained; the issues of the interchangeability needs to be solved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
L please rebase Please rebase your branch to origin/develop tests
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants