-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Type traits error in c++17 version #47851
Comments
assigned to @ldionne |
There were definitely some "tightening up" of the requirements on the type traits after C++14. One of the goals was to avoid having the same type trait give different answers at different points in the compilation. |
The "tightened up" requirements probably don't matter in this case. There is also some "magic" declaration than changes the compiler behavior: using J = nlohmann::ordered_json; This code compiles successfully, even with "-std=c++17 -stdlib=libc++". See a working example here: https://godbolt.org/z/87zGf3 |
Speaking about magic.
Declaration of See a working example here: https://godbolt.org/z/vooaWe |
To emphasize that this problem not in the static_assert, here is the code without it:
See a working example here: https://godbolt.org/z/h7cYa1 |
I minimized the library code as much as I can. Still 400 lines of complicated template code, but at least it's a good starting point to investigate futher. It's too much to paste here, so here are the links: Godbolt: https://godbolt.org/z/zf17vT |
Thanks to 400+ lines in the previous comment by Krylov Yaroslav, I was able to make a deeper investigation and found a bit smaller code example that also fails. But the example that I'm about to present also fails on libstdc++ too, GCC standard library implementation. While the original 400+ lines version fails only with libc++. Probably it's due to a reduced number of included files because I was only removing code, but not adding. Here are two demonstrations on Compiler Explorer with the same code: The first one just shows that Clang now also fails with libstdc++, while GCC does not fail, as well as MSVC with its own STD library. The second demonstrates that by using some workarounds a compilation error can be eliminated on Clang. There are 2 workarounds, but they have one thing in common - they manipulate somehow with |
Actually, this looks like a close but not exactly the same bug as the initial one. |
I also discovered that Clang versions < 6.0 do not fail with both code examples - the initial one by Krylov Yaroslav and the modified one by me. Looks like there was some breaking change introduced in Clang 6.0. |
I ran git bisect for commits from 287f684 (root of 5.0 release) to d359f20 (latest commit in 6.0 release). Here are links for these commits on GitHub: 287f684 and d359f20. Bisection procedure pointed out the commit that fails a C++17 build with libc++ for the code example provided in comment #5 - it's 67ef14f: Author: Richard Smith [email protected]
Complete diff of this commit can be found on GitHub too - 67ef14f. It looks like the addition of a new feature resulted in invoking some underlying Clang bug. But since I know more or less nothing about what's going on there, I cannot determine what's exactly causing the underlying bug. |
Bisect run for the simplified code example (from comment #6) points to the same 67ef14f commit. So both of these code examples share the same exposition of that underlying Clang bug. The question here is why the first one does not fail with libstdc++? Explicit instantiations, different types, idk 🤷... Also, I think it's worth marking this issue as confirmed. |
This is probably a bug in the user code -- while attempting to form a copy assignment of nlohmann::ordered_json in order to determine whether the type is copy-assignable, the user code probably recursively queried is_copy_assignablenlohmann::ordered_json itself, causing a cyclic template instantiation that cached the "wrong" result in the template specialization. (That's usually the cause for problems that look like this.) |
First of all, thank you for bringing some clarity here. constexpr bool b = std::is_copy_assignable<some_type>::value; is not actually that fails but this line from std header does: _LIBCPP_INLINE_VISIBILITY
pair& operator=(typename conditional<
is_copy_assignable<first_type>::value &&
is_copy_assignable<second_type>::value, // << `some_type` here
pair, __nat>::type const& __p) There is a quite long way to instantiate
it's huge for one line, but the most important thing is that the conversion operator is instantiated for the [ValueType = const nlohmann::ordered_map<...> *] - a pointer type. For example, the error in the code from comment #6 can be resolved by making Still, I have unanswered questions about that problem. |
I'd imagine we have a dependency cycle: is_copy_assignablenlohmann::ordered_json depends on is_copy_constructiblenlohmann::ordered_json, which depends on is_copy_assignablenlohmann::ordered_json. Explicitly instantiating is_copy_constructible means we enter the cycle at a different point, which means we break the cycle at a different point, and the point at which we break the cycle will typically be the point at which we cache a meaningless value. Entering the trait dependency cycle at different points can result in different behavior. That's the same thing you see in the case in comment#2: there, we enter the cycle while actually performing the assignment, not while computing is_copy_assignable, so the cycle is broken in a different place, so we happen to not end up caching something bad in is_copy_assignable.
Different standard library implementations either will or won't check type traits as they go, and so may or may not cache bad values as a side-effect of any particular operation. In addition, GCC has an optimization where it doesn't consider template overload candidates if there's an exact match among non-template candidates, which can sometimes mean that GCC doesn't consider a SFINAE check that Clang does, so in the Clang compilation we end up performing more type trait computations, which can lead to us discovering more cycles. I think that's why GCC accepts https://godbolt.org/z/csWM3n. These kinds of problems usually arise when a function template (usually a constructor or assignment operator or a conversion function) is insufficiently constrained. Usually what's needed, when a constructor or assignment operator is templated to take a T (or const T& or similar) or a conversion function is templated to return a T (or const T& or similar) is a SFINAE check that T is not the same as the enclosing class type, before anything else is checked. For example, here's libc++'s std::optional converting constructor: https://github.com/llvm/llvm-project/blob/master/libcxx/include/optional#L715 Note that it checks _CheckOptionalArgsCtor<_Up>::template __enable_implicit<_Up>(), and the first things that _CheckOptionalArgsCtor checks are that _Up is not in_place_t nor optional:
Looking at the current implementation (https://github.com/YarikTH/json/blob/feature/libcxx_debug/single_include/nlohmann/json.hpp#L459) it looks like there is an attempt to apply this kind of protection to the conversion function, but it's not correct:
[...] The problem here is that substitution into the enable_if condition does not short-circuit when we hit the first ::value that evaluates to false. So even if is_basic_json::value evaluates to true, we still substitute into the is_detected<...> check, which is how we end up recursing back to checking the same thing we were already checking. The fix is to use std::conjunction instead of a sequence of &&s, so that evaluating the type traits short-circuits (see https://stackoverflow.com/a/55161485/1041090):
[...] |
And I should point out, critically, it does all these checks and short-circuits if they fail, before triggering instantiation of __enable_implicit, which is where the potentially recursive convertibility checks would happen. |
Thank you again for making the problem less suspicious, at least for me. But I want to clarify what is exactly strange in that code example where Clang is the only compiler that fails to compile (https://godbolt.org/z/csWM3n).
|
This commit probably introduces an issue that isn't directly related to the considered one. Since Clang 6 the result of overload resolution depends on the language standard. The following will produce different results when compiled as C++14 or C++17 (see https://godbolt.org/z/qqrod3): struct B struct A struct B int main() When Clang 6 (or a later version) is used, the conversion of B to A will be performed:
Clang 5 prefers the constructor in both cases. To see the list of candidate funcitons for overload resolution, we can declare the additional constructor "A(B)" that will cause the ambiguity (and compiler diagnostics). Besides this additional constructor there are the following candidates: A::A(const A&); // implicit The presence of the operator in the above list is probably an error. The mandatory copy elision in C++17 cannot be applied here. It is applicable only when initializer is an prvalue and has the same type as the object being initialized. In such a case no conversion is required (nor is the overload resolution). In the example we have an lvalue initializer of type B that is used to initialize an object of type A. GCC has a similar error since 8.4; see also: |
If the compiler had chosen the former option, it would have resulted in a copy-initialization of temporary A from lvalue B and the following direct-initialization of target object from const reference to the temporary. Here it's worth to note:
Here are variations of the example where all the compiler configurations considered behave as expected:
|
I missed the move constructor in the previous post, but it doesn't really matter. We can exclude it by adding "A(const A&) = default". Nothing will change. |
This bug was not resolved in time for the 12.0.0 release, so it will have to wait for 12.0.1. If you feel this is a high-priority bug that should be fixed for 12.0.0, please re-add release-12.0.0 to the Blocks field and add a comment explaining why this is high-priority. |
Thanks to everyone for their analysis, in particular Richard Smith for layout out the solution. I created a PR in nlohmann::json to fix this upstream: nlohmann/json#2825. I'm going to close this because it's not a libc++ or Clang bug as far as I can tell. |
Looking at this thread after some time, of course, the original problem in nlohmann/json is not libc++ or Clang bug, but there are some points from Alexander Karzhenkov that probably may be used to create a separate bug report. |
Please do file a bug report against Clang for those. I was just trying to clean up the list of blockers for the 12.x releases assigned to libc++. |
mentioned in issue #48661 |
Extended Description
Here is the two strings code using type traits:
https://godbolt.org/z/4eqjas
It works perfectly with libstdc++, and it works with libc++ using c++11 or c++14.
But libc++ with c++17 can't compile this code. I tried to dig into problem, to find the difference between c++17 and non-c++17 in libc++ headers, I tried to dig into nlohmann json to extract minimal example, but failed.
Root issue on Github: nlohmann/json#2491
The text was updated successfully, but these errors were encountered: