Usage of NLOHMANN_JSON_SERIALIZE_ENUM with nlohmann::ordered_json results in a compilation error for libc++ with -std=c++17 and greater.
The NLOHMANN_JSON_SERIALIZE_ENUM expands to the custom to_json and from_json functions. Both of these functions have the following code:
static const std::pair<Enum, BasicJsonType> m[] = /* macro argument */;
And they work fine with using GCC or using (Clang + libstdc++) or using (Clang + libc++) but trying to convert enum value to nlohmann::json, but when (Clang + libc++) is used and enum value is converting to nlohmann::ordered_json then there is a compilation error.
Entire compiler output with error
In file included from <source>:1:
In file included from /opt/compiler-explorer/libs/nlohmann_json/trunk/single_include/nlohmann/json.hpp:37:
In file included from /opt/compiler-explorer/clang-11.0.0/bin/../include/c++/v1/algorithm:642:
/opt/compiler-explorer/clang-11.0.0/bin/../include/c++/v1/utility:514:58: error: no member named 'value' in 'std::__1::is_copy_assignable<nlohmann::basic_json<nlohmann::ordered_map, std::vector, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, bool, long, unsigned long, double, std::allocator, adl_serializer, std::__1::vector<unsigned char, std::__1::allocator<unsigned char>>>>'
is_copy_assignable<second_type>::value,
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
/opt/compiler-explorer/libs/nlohmann_json/trunk/single_include/nlohmann/json.hpp:3856:17: note: in instantiation of template class 'std::__1::pair<const std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, nlohmann::basic_json<nlohmann::ordered_map, std::vector, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, bool, long, unsigned long, double, std::allocator, adl_serializer, std::__1::vector<unsigned char, std::__1::allocator<unsigned char>>>>' requested here
-> decltype(from_json(j, val), void())
^
/opt/compiler-explorer/libs/nlohmann_json/trunk/single_include/nlohmann/json.hpp:4451:17: note: while substituting deduced template arguments into function template 'operator()' [with BasicJsonType = nlohmann::basic_json<nlohmann::ordered_map, std::vector, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, bool, long, unsigned long, double, std::allocator, adl_serializer, std::__1::vector<unsigned char, std::__1::allocator<unsigned char>>>, T = nlohmann::ordered_map<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, nlohmann::basic_json<nlohmann::ordered_map, std::vector, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, bool, long, unsigned long, double, std::allocator, adl_serializer, std::__1::vector<unsigned char, std::__1::allocator<unsigned char>>>, std::__1::less<void>, std::__1::allocator<std::__1::pair<const std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, nlohmann::basic_json<nlohmann::ordered_map, std::vector, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, bool, long, unsigned long, double, std::allocator, adl_serializer, std::__1::vector<unsigned char, std::__1::allocator<unsigned char>>>>>> *]
-> decltype(::nlohmann::from_json(std::forward<BasicJsonType>(j), val), void())
^
/opt/compiler-explorer/libs/nlohmann_json/trunk/single_include/nlohmann/json.hpp:3094:37: note: while substituting deduced template arguments into function template 'from_json' [with BasicJsonType = const nlohmann::basic_json<nlohmann::ordered_map, std::vector, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, bool, long, unsigned long, double, std::allocator, adl_serializer, std::__1::vector<unsigned char, std::__1::allocator<unsigned char>>> &, ValueType = nlohmann::ordered_map<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, nlohmann::basic_json<nlohmann::ordered_map, std::vector, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, bool, long, unsigned long, double, std::allocator, adl_serializer, std::__1::vector<unsigned char, std::__1::allocator<unsigned char>>>, std::__1::less<void>, std::__1::allocator<std::__1::pair<const std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, nlohmann::basic_json<nlohmann::ordered_map, std::vector, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, bool, long, unsigned long, double, std::allocator, adl_serializer, std::__1::vector<unsigned char, std::__1::allocator<unsigned char>>>>>> *]
using from_json_function = decltype(T::from_json(std::declval<Args>()...));
^
/opt/compiler-explorer/libs/nlohmann_json/trunk/single_include/nlohmann/json.hpp:2911:33: note: in instantiation of template type alias 'from_json_function' requested here
struct detector<Default, void_t<Op<Args...>>, Op, Args...>
^
/opt/compiler-explorer/libs/nlohmann_json/trunk/single_include/nlohmann/json.hpp:2921:1: note: during template argument deduction for class template partial specialization 'detector<Default, void_t<Op<Args...>>, Op, Args...>' [with Default = nlohmann::detail::nonesuch, Op = from_json_function, Args = <nlohmann::adl_serializer<nlohmann::ordered_map<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, nlohmann::basic_json<nlohmann::ordered_map, std::vector, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, bool, long, unsigned long, double, std::allocator, adl_serializer, std::__1::vector<unsigned char, std::__1::allocator<unsigned char>>>, std::__1::less<void>, std::__1::allocator<std::__1::pair<const std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, nlohmann::basic_json<nlohmann::ordered_map, std::vector, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, bool, long, unsigned long, double, std::allocator, adl_serializer, std::__1::vector<unsigned char, std::__1::allocator<unsigned char>>>>>> *, void>, const nlohmann::basic_json<nlohmann::ordered_map, std::vector, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, bool, long, unsigned long, double, std::allocator, adl_serializer, std::__1::vector<unsigned char, std::__1::allocator<unsigned char>>> &, nlohmann::ordered_map<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, nlohmann::basic_json<nlohmann::ordered_map, std::vector, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, bool, long, unsigned long, double, std::allocator, adl_serializer, std::__1::vector<unsigned char, std::__1::allocator<unsigned char>>>, std::__1::less<void>, std::__1::allocator<std::__1::pair<const std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, nlohmann::basic_json<nlohmann::ordered_map, std::vector, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, bool, long, unsigned long, double, std::allocator, adl_serializer, std::__1::vector<unsigned char, std::__1::allocator<unsigned char>>>>>> *&>]
using detected_t = typename detector<nonesuch, void, Op, Args...>::type;
^
/opt/compiler-explorer/libs/nlohmann_json/trunk/single_include/nlohmann/json.hpp:2921:1: note: (skipping 22 contexts in backtrace; use -ftemplate-backtrace-limit=0 to see all)
<source>:12:50: note: in instantiation of template class 'std::__1::pair<Enum1, nlohmann::basic_json<nlohmann::ordered_map, std::vector, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, bool, long, unsigned long, double, std::allocator, adl_serializer, std::__1::vector<unsigned char, std::__1::allocator<unsigned char>>>>' requested here
static const std::pair<Enum1, BasicJsonType> m[] = {
^
/opt/compiler-explorer/libs/nlohmann_json/trunk/single_include/nlohmann/json.hpp:4420:16: note: in instantiation of function template specialization 'to_json<nlohmann::basic_json<nlohmann::ordered_map, std::vector, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, bool, long, unsigned long, double, std::allocator, adl_serializer, std::__1::vector<unsigned char, std::__1::allocator<unsigned char>>>>' requested here
return to_json(j, std::forward<T>(val));
^
/opt/compiler-explorer/libs/nlohmann_json/trunk/single_include/nlohmann/json.hpp:4470:9: note: in instantiation of function template specialization 'nlohmann::detail::to_json_fn::operator()<nlohmann::basic_json<nlohmann::ordered_map, std::vector, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, bool, long, unsigned long, double, std::allocator, adl_serializer, std::__1::vector<unsigned char, std::__1::allocator<unsigned char>>>, Enum1>' requested here
::nlohmann::to_json(j, std::forward<ValueType>(val));
^
/opt/compiler-explorer/libs/nlohmann_json/trunk/single_include/nlohmann/json.hpp:18014:28: note: in instantiation of function template specialization 'nlohmann::adl_serializer<Enum1, void>::to_json<nlohmann::basic_json<nlohmann::ordered_map, std::vector, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, bool, long, unsigned long, double, std::allocator, adl_serializer, std::__1::vector<unsigned char, std::__1::allocator<unsigned char>>>, Enum1>' requested here
JSONSerializer<U>::to_json(*this, std::forward<CompatibleType>(val));
^
<source>:19:32: note: in instantiation of function template specialization 'nlohmann::basic_json<nlohmann::ordered_map, std::vector, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>, bool, long, unsigned long, double, std::allocator, adl_serializer, std::__1::vector<unsigned char, std::__1::allocator<unsigned char>>>::basic_json<Enum1, Enum1, 0>' requested here
nlohmann::ordered_json j = Enum1::Value1;
^
This probably depends on C++ standard version in libc++, for example with -std=c++11 and -std=c++14 it works fine.
You can reproduce the issue by looking at this useful example on Compiler Explorer with Clang 11.0.0 and -std=c++17.
Note that NLOHMANN_JSON_SERIALIZE_ENUM is expanded already there and a bit simplified, if this is not necessary then here is also an example without NLOHMANN_JSON_SERIALIZE_ENUM macro expansion.
Example with only std::is_copy_assignable<nlohmann::ordered_json>::value usage that also fails.
Clang 11.0.0 with -std=c++14 compiles this code on Compiler Explorer.
#include "nlohmann/json.hpp"
enum class Enum1
{
Value1,
Value2
};
NLOHMANN_JSON_SERIALIZE_ENUM( Enum1,
{
{Enum1::Value1, "Enum1::Value1"},
{Enum1::Value2, "Enum1::Value2"},
})
int main()
{
nlohmann::ordered_json j = Enum1::Value1;
}
It should compile
Compilation error
Additional info. I refuse to comprehend it, but forced instantination of std::is_copy_constructible<nlohmann::ordered_json> 'fixes' this problem. Looks like a bug in libc++.
https://godbolt.org/z/bsdT1z
B̶t̶w̶ ̶i̶t̶ ̶s̶e̶e̶m̶s̶ ̶t̶h̶a̶t̶ ̶#̶2̶5̶1̶9̶ ̶h̶a̶s̶ ̶a̶l̶m̶o̶s̶t̶ ̶t̶h̶e̶ ̶s̶a̶m̶e̶ ̶i̶s̶s̶u̶e̶,̶ ̶b̶u̶t̶ ̶w̶i̶t̶h̶ ̶Q̶T̶ ̶i̶n̶s̶t̶e̶a̶d̶ ̶o̶f̶ ̶l̶i̶b̶c̶+̶+̶.̶ ̶M̶a̶y̶b̶e̶ ̶w̶o̶r̶k̶a̶r̶o̶u̶n̶d̶ ̶w̶i̶l̶l̶ ̶b̶e̶ ̶t̶h̶e̶ ̶s̶a̶m̶e̶?̶
Good catch, but mega confusing...
Posted bug in llvm bugtracker. Maybe someone who understands type-traits implementation will answer.
https://bugs.llvm.org/show_bug.cgi?id=48507
This is definitively a Clang compiler bug (not to be mixed with Qt bug from #2519 :). As @YarikTH pointed out, an unrelated declaration can change the compiler behavior. Consider also the following:
using J = nlohmann::ordered_json;
using magic = decltype(std::declval<J&>() = std::declval<J&>());
bool check = std::is_copy_assignable<J>::value;
Assuming clang -std=c++17 -stdlib=libc++, the above code will not be compiled if the "magic" is removed.
(see https://godbolt.org/z/qozhj7)
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 further. It's too much to paste here, so here are the links:
Godbolt: https://godbolt.org/z/zf17vT
GitHub: https://github.com/YarikTH/json/blob/feature/libcxx_debug/single_include/nlohmann/json.hpp
Is there anything we can do for this library at the moment?
I'm stuck with 400 lines of templated spaghetti code and because I neither nlohmann::json maintainer, nor llvm specialist I have no idea how to dig it further. I see no meaningful workarounds of the problem on the nlohmann::json side. So maybe it's time to say that it's obviously llvm bug and call it a day.
Maybe some kind of "magic" in nlohmann::detail will be enough for now?
I think that some workaround can be added unless it is something forbidden for the library. I mean, I found only one workaround (for MSVC) in code with a proper comment...
IMHO, even though it's an LLVM bug, it doesn't mean that the library should require that kind of workarounds on the client-side
Ok. If the common decision is to add a workaround, then I have it for you:
Code: https://godbolt.org/z/WdWeTY
#include "nlohmann/json.hpp"
enum class Enum1
{
Value1,
Value2
};
// Work around llvm bug described in #2491
#if JSON_USING_WORKAROUND //JSON_HAS_CPP_17
# define JSON_LIBCXX_STD17_WORKAROUND( BasicJsonType ) \
static_assert( std::is_copy_constructible<BasicJsonType>::value )
#else
# define JSON_LIBCXX_STD17_WORKAROUND( BasicJsonType ) static_assert(true, "")
#endif
/*!
@brief macro to briefly define a mapping between an enum and JSON
@def NLOHMANN_JSON_SERIALIZE_ENUM
@since version 3.4.0
*/
#define NLOHMANN_JSON_SERIALIZE_ENUM_2(ENUM_TYPE, ...) \
template<typename BasicJsonType> \
inline void to_json(BasicJsonType& j, const ENUM_TYPE& e) \
{ \
static_assert(std::is_enum<ENUM_TYPE>::value, #ENUM_TYPE " must be an enum!"); \
JSON_LIBCXX_STD17_WORKAROUND(BasicJsonType); \
static const std::pair<ENUM_TYPE, BasicJsonType> m[] = __VA_ARGS__; \
auto it = std::find_if(std::begin(m), std::end(m), \
[e](const std::pair<ENUM_TYPE, BasicJsonType>& ej_pair) -> bool \
{ \
return ej_pair.first == e; \
}); \
j = ((it != std::end(m)) ? it : std::begin(m))->second; \
} \
template<typename BasicJsonType> \
inline void from_json(const BasicJsonType& j, ENUM_TYPE& e) \
{ \
static_assert(std::is_enum<ENUM_TYPE>::value, #ENUM_TYPE " must be an enum!"); \
JSON_LIBCXX_STD17_WORKAROUND(BasicJsonType); \
static const std::pair<ENUM_TYPE, BasicJsonType> m[] = __VA_ARGS__; \
auto it = std::find_if(std::begin(m), std::end(m), \
[&j](const std::pair<ENUM_TYPE, BasicJsonType>& ej_pair) -> bool \
{ \
return ej_pair.second == j; \
}); \
e = ((it != std::end(m)) ? it : std::begin(m))->first; \
}
NLOHMANN_JSON_SERIALIZE_ENUM_2( Enum1, {
{Enum1::Value1, "Value1"},
{Enum1::Value2, "Value2"}} )
int main()
{
nlohmann::ordered_json j = Enum1::Value1;
}
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 further. It's too much to paste here, so here are the links:
Godbolt: https://godbolt.org/z/zf17vT
GitHub: https://github.com/YarikTH/json/blob/feature/libcxx_debug/single_include/nlohmann/json.hpp
Thanks to your work here, @YarikTH, I was able to make a deeper investigation and found a bit smaller code example (69 lines) 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 std::pair, the first workaround just replaces the usage of std::pair with std::tuple in the code, while the second one explicitly instantiates std::pair with used template parameters.
I also posted this on LLVM bug tracker.
I have another idea. If replacing of std::pair with std::tuple is a viable strategy, then we can change ordered_map to work with std::vectorordered_map like this just as a workaround for libc++ c++17 bug in one specific use-case.
Dug in a little. Altering of internal type for ordered_map is impossible becase of
using object_t = ObjectType<StringType,
basic_json,
object_comparator_t,
AllocatorType<std::pair<const StringType,
basic_json>>>;
Allocator type is hardcoded into basic_json type. So my genius idea is not viable in the current codebase.
Most helpful comment
This is definitively a Clang compiler bug (not to be mixed with Qt bug from #2519 :). As @YarikTH pointed out, an unrelated declaration can change the compiler behavior. Consider also the following:
Assuming
clang -std=c++17 -stdlib=libc++, the above code will not be compiled if the "magic" is removed.(see https://godbolt.org/z/qozhj7)