diff --git a/include/nlohmann/detail/macro_scope.hpp b/include/nlohmann/detail/macro_scope.hpp index afb400c90b..61f976bce4 100644 --- a/include/nlohmann/detail/macro_scope.hpp +++ b/include/nlohmann/detail/macro_scope.hpp @@ -58,6 +58,12 @@ #define JSON_HAS_CPP_11 #endif +// Since C++20 the preprocessor supports __VA_OPT__(x) which expands to x only +// when __VA_ARGS__ is non-empty. We use this in NLOHMANN_DEFINE_TYPE_* macros +// to avoid generating invalid code when no member arguments are passed. +// On pre-C++20 compilers __VA_OPT__ is unavailable; zero-member invocations +// were already ill-formed on those compilers, so there is no regression. + #ifdef __has_include #if __has_include() #include @@ -416,9 +422,9 @@ */ #define NLOHMANN_DEFINE_TYPE_INTRUSIVE(Type, ...) \ template::value, int> = 0> \ - friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { __VA_OPT__(NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__))) } \ template::value, int> = 0> \ - friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) } + friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { __VA_OPT__(NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__))) } /*! @brief macro @@ -428,9 +434,9 @@ */ #define NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Type, ...) \ template::value, int> = 0> \ - friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { __VA_OPT__(NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__))) } \ template::value, int> = 0> \ - friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) } + friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { __VA_OPT__(const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__))) } /*! @brief macro @@ -440,7 +446,7 @@ */ #define NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(Type, ...) \ template::value, int> = 0> \ - friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } + friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { __VA_OPT__(NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__))) } /*! @brief macro @@ -450,9 +456,9 @@ */ #define NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Type, ...) \ template::value, int> = 0> \ - void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { __VA_OPT__(NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__))) } \ template::value, int> = 0> \ - void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) } + void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { __VA_OPT__(NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__))) } /*! @brief macro @@ -462,9 +468,9 @@ */ #define NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(Type, ...) \ template::value, int> = 0> \ - void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { __VA_OPT__(NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__))) } \ template::value, int> = 0> \ - void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) } + void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { __VA_OPT__(const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__))) } /*! @brief macro @@ -474,7 +480,7 @@ */ #define NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_ONLY_SERIALIZE(Type, ...) \ template::value, int> = 0> \ - void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } + void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { __VA_OPT__(NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__))) } /*! @brief macro @@ -484,9 +490,9 @@ */ #define NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE(Type, BaseType, ...) \ template::value, int> = 0> \ - friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); __VA_OPT__(NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__))) } \ template::value, int> = 0> \ - friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) } + friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); __VA_OPT__(NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__))) } /*! @brief macro @@ -496,9 +502,9 @@ */ #define NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE_WITH_DEFAULT(Type, BaseType, ...) \ template::value, int> = 0> \ - friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); __VA_OPT__(NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__))) } \ template::value, int> = 0> \ - friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) } + friend void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); __VA_OPT__(const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__))) } /*! @brief macro @@ -508,7 +514,7 @@ */ #define NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE_ONLY_SERIALIZE(Type, BaseType, ...) \ template::value, int> = 0> \ - friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } + friend void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); __VA_OPT__(NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__))) } /*! @brief macro @@ -518,9 +524,9 @@ */ #define NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE(Type, BaseType, ...) \ template::value, int> = 0> \ - void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); __VA_OPT__(NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__))) } \ template::value, int> = 0> \ - void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) } + void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); __VA_OPT__(NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__))) } /*! @brief macro @@ -530,9 +536,9 @@ */ #define NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_WITH_DEFAULT(Type, BaseType, ...) \ template::value, int> = 0> \ - void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); __VA_OPT__(NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__))) } \ template::value, int> = 0> \ - void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) } + void from_json(const BasicJsonType& nlohmann_json_j, Type& nlohmann_json_t) { nlohmann::from_json(nlohmann_json_j, static_cast(nlohmann_json_t)); __VA_OPT__(const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__))) } /*! @brief macro @@ -542,7 +548,7 @@ */ #define NLOHMANN_DEFINE_DERIVED_TYPE_NON_INTRUSIVE_ONLY_SERIALIZE(Type, BaseType, ...) \ template::value, int> = 0> \ - void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } + void to_json(BasicJsonType& nlohmann_json_j, const Type& nlohmann_json_t) { nlohmann::to_json(nlohmann_json_j, static_cast(nlohmann_json_t)); __VA_OPT__(NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__))) } // inspired from https://stackoverflow.com/a/26745591 // allows calling any std function as if (e.g., with begin): diff --git a/test_empty_macro.cpp b/test_empty_macro.cpp new file mode 100644 index 0000000000..539a18f3e1 --- /dev/null +++ b/test_empty_macro.cpp @@ -0,0 +1,59 @@ +// Standalone compile test for issue #4041 +// Tests that zero-member NLOHMANN_DEFINE_TYPE_* macros compile and work correctly. +#include +#include +#include + +// --- Intrusive (friend functions inside the class) --- +struct EmptyIntrusive +{ + bool operator==(const EmptyIntrusive&) const { return true; } + NLOHMANN_DEFINE_TYPE_INTRUSIVE(EmptyIntrusive) +}; + +struct EmptyIntrusiveWithDefault +{ + bool operator==(const EmptyIntrusiveWithDefault&) const { return true; } + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(EmptyIntrusiveWithDefault) +}; + +struct EmptyIntrusiveOnlySerialize +{ + NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(EmptyIntrusiveOnlySerialize) +}; + +// --- Non-intrusive (free functions in the same namespace) --- +struct EmptyNonIntrusive +{ + bool operator==(const EmptyNonIntrusive&) const { return true; } +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(EmptyNonIntrusive) + +struct EmptyNonIntrusiveWithDefault +{ + bool operator==(const EmptyNonIntrusiveWithDefault&) const { return true; } +}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(EmptyNonIntrusiveWithDefault) + +struct EmptyNonIntrusiveOnlySerialize {}; +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_ONLY_SERIALIZE(EmptyNonIntrusiveOnlySerialize) + +int main() +{ + // to_json produces {} + assert(nlohmann::json(EmptyIntrusive{}).dump() == "{}"); + assert(nlohmann::json(EmptyIntrusiveWithDefault{}).dump() == "{}"); + assert(nlohmann::json(EmptyIntrusiveOnlySerialize{}).dump() == "{}"); + assert(nlohmann::json(EmptyNonIntrusive{}).dump() == "{}"); + assert(nlohmann::json(EmptyNonIntrusiveWithDefault{}).dump() == "{}"); + assert(nlohmann::json(EmptyNonIntrusiveOnlySerialize{}).dump() == "{}"); + + // from_json round-trips successfully + nlohmann::json j = nlohmann::json::object(); + auto e1 = j.get(); (void)e1; + auto e2 = j.get(); (void)e2; + auto e3 = j.get(); (void)e3; + auto e4 = j.get(); (void)e4; + + return 0; +} diff --git a/tests/src/unit-udt_macro.cpp b/tests/src/unit-udt_macro.cpp index cd65134297..c5452bf975 100644 --- a/tests/src/unit-udt_macro.cpp +++ b/tests/src/unit-udt_macro.cpp @@ -460,6 +460,43 @@ class derived_person_only_serialize_private : person_without_default_constructor NLOHMANN_DEFINE_DERIVED_TYPE_INTRUSIVE_ONLY_SERIALIZE(derived_person_only_serialize_private, person_without_default_constructor_1, hair_color) }; +// Zero-member types for issue #4041 regression: macros must compile and +// produce valid (empty) JSON objects when no member arguments are given. +struct empty_type_intrusive +{ + bool operator==(const empty_type_intrusive& /*rhs*/) const { return true; } + NLOHMANN_DEFINE_TYPE_INTRUSIVE(empty_type_intrusive) +}; + +struct empty_type_intrusive_with_default +{ + bool operator==(const empty_type_intrusive_with_default& /*rhs*/) const { return true; } + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(empty_type_intrusive_with_default) +}; + +struct empty_type_intrusive_only_serialize +{ + NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(empty_type_intrusive_only_serialize) +}; + +struct empty_type_non_intrusive +{ + bool operator==(const empty_type_non_intrusive& /*rhs*/) const { return true; } +}; +// NOLINTNEXTLINE(misc-use-internal-linkage) +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(empty_type_non_intrusive) + +struct empty_type_non_intrusive_with_default +{ + bool operator==(const empty_type_non_intrusive_with_default& /*rhs*/) const { return true; } +}; +// NOLINTNEXTLINE(misc-use-internal-linkage) +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(empty_type_non_intrusive_with_default) + +struct empty_type_non_intrusive_only_serialize {}; +// NOLINTNEXTLINE(misc-use-internal-linkage) +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_ONLY_SERIALIZE(empty_type_non_intrusive_only_serialize) + } // namespace persons TEST_CASE_TEMPLATE("Serialization/deserialization via NLOHMANN_DEFINE_TYPE_INTRUSIVE and NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE", Pair, // NOLINT(readability-math-missing-parentheses, bugprone-throwing-static-initialization) @@ -707,3 +744,59 @@ TEST_CASE_TEMPLATE("Serialization of non-default-constructible classes via NLOHM R"([{"age":1,"hair_color":"brown","name":"Erik"},{"age":2,"hair_color":"black","name":"Kyle"}])")); } } + +// Regression test for issue #4041: zero-member NLOHMANN_DEFINE_TYPE_* macros +// must compile and produce valid (empty) JSON objects. +TEST_CASE_TEMPLATE("Zero-member NLOHMANN_DEFINE_TYPE_* macros produce empty JSON objects (issue #4041)", Json, // NOLINT + nlohmann::json, nlohmann::ordered_json) +{ + SECTION("NLOHMANN_DEFINE_TYPE_INTRUSIVE with zero members") + { + persons::empty_type_intrusive obj{}; + Json j = obj; + CHECK(j.dump() == "{}"); + persons::empty_type_intrusive obj2 = j.template get(); + CHECK(obj2 == obj); + } + + SECTION("NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT with zero members") + { + persons::empty_type_intrusive_with_default obj{}; + Json j = obj; + CHECK(j.dump() == "{}"); + persons::empty_type_intrusive_with_default obj2 = j.template get(); + CHECK(obj2 == obj); + } + + SECTION("NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE with zero members") + { + persons::empty_type_intrusive_only_serialize obj{}; + Json j = obj; + CHECK(j.dump() == "{}"); + } + + SECTION("NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE with zero members") + { + persons::empty_type_non_intrusive obj{}; + Json j = obj; + CHECK(j.dump() == "{}"); + persons::empty_type_non_intrusive obj2 = j.template get(); + CHECK(obj2 == obj); + } + + SECTION("NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT with zero members") + { + persons::empty_type_non_intrusive_with_default obj{}; + Json j = obj; + CHECK(j.dump() == "{}"); + persons::empty_type_non_intrusive_with_default obj2 = j.template get(); + CHECK(obj2 == obj); + } + + SECTION("NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_ONLY_SERIALIZE with zero members") + { + persons::empty_type_non_intrusive_only_serialize obj{}; + Json j = obj; + CHECK(j.dump() == "{}"); + } +}