nlohmann / json

JSON for Modern C++
https://json.nlohmann.me
MIT License
42.4k stars 6.67k forks source link

NLOHMANN_DEFINE_TYPE_* fails with zero members #4041

Open dawinaj opened 1 year ago

dawinaj commented 1 year ago

Description

I found it annoying when working with multiple tiny polymorphic classes, trying to provide identical interface for each of them, that I can't use the macro for a class that has no members to (de)serialize.

Reproduction steps

Use (probably) any of the NLOHMANN_DEFINE_TYPE_* macros with only first argument and no members. F.e. NLOHMANN_DEFINE_TYPE_INTRUSIVE(MyClass)

Expected vs. actual results

Expected:

friend void to_json(nlohmann::json&, const MyClass&) {}
friend void from_json(const nlohmann::json&, MyClass&) {}

Actual:

friend void to_json(nlohmann::json& nlohmann_json_j, const MyClass& nlohmann_json_t) {
    nlohmann_json_j[] = nlohmann_json_t.;
}
friend void from_json(const nlohmann::json& nlohmann_json_j, MyClass& nlohmann_json_t) {
    nlohmann_json_j.at().get_to(nlohmann_json_t.);
}

Minimal code example

class MyClass
{
    NLOHMANN_DEFINE_TYPE_INTRUSIVE(MyClass);
};

Error messages

Error   C2661   'nlohmann::json_abi_v3_11_2::basic_json<std::map,std::vector,std::string,bool,int64_t,uint64_t,double,std::allocator,nlohmann::json_abi_v3_11_2::adl_serializer,std::vector<uint8_t,std::allocator<uint8_t>>>::at': no overloaded function takes 0 arguments
Error   C2661   'nlohmann::json_abi_v3_11_2::basic_json<std::map,std::vector,std::string,bool,int64_t,uint64_t,double,std::allocator,nlohmann::json_abi_v3_11_2::adl_serializer,std::vector<uint8_t,std::allocator<uint8_t>>>::at': no overloaded function takes 0 arguments
Error   C2661   'nlohmann::json_abi_v3_11_2::basic_json<std::map,std::vector,std::string,bool,int64_t,uint64_t,double,std::allocator,nlohmann::json_abi_v3_11_2::adl_serializer,std::vector<uint8_t,std::allocator<uint8_t>>>::at': no overloaded function takes 0 arguments
Error   C2661   'nlohmann::json_abi_v3_11_2::basic_json<std::map,std::vector,std::string,bool,int64_t,uint64_t,double,std::allocator,nlohmann::json_abi_v3_11_2::adl_serializer,std::vector<uint8_t,std::allocator<uint8_t>>>::at': no overloaded function takes 0 arguments
Error   C2059   syntax error: ')'
Error   C2059   syntax error: ')'
Error   C2059   syntax error: ')'
Error   C2059   syntax error: ')'
Error   C2059   syntax error: ']'
Error   C2059   syntax error: ']'
Error   C2059   syntax error: ']'
Error   C2059   syntax error: ']'

Compiler and operating system

Microsoft Visual C++ 2022

Library version

3.11.2

Validation

Romop5 commented 1 year ago

In case someone was interested in how to proceed with fixing this issue:

This bug is caused by the way the macro functional meta programming is implemented for this project and by a standard behaviour of C preprocessor's __VA_ARGS__ that expands as an empty token when no arguments are passed to a variadic macro.

Due to this property of __VA_ARGS__, for example, #define M(...) __VA_ARGS__ would be expanded to an empty token ` forM()`).

So, for instance, NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(MyStruct) leads to an expansion, that contains NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, ), thus expanding NLOHMANN_JSON_TO().

See https://github.com/nlohmann/json/blob/a0c1318830519eac027a31edec1a99ce1ae5670e/include/nlohmann/detail/macro_scope.hpp#L406 for more details.

There are several ways how to tackle with this problem:

  1. Using non-standard comma swallow extension (GNU) By replacing , __VA_ARGS__ with , ## __VA_ARGS__, this effectively discards , in case of empty arguments. Unfortunately, only supported by gcc/clang/msvc and maybe other compilers . See the last paragraph of https://gcc.gnu.org/onlinedocs/gcc/Variadic-Macros.html
  2. Using some standard, yet complicated macro property to avoid use of the extension mentioned above See https://stackoverflow.com/a/8445641
dawinaj commented 1 year ago

Alternatively, just add another macro, eg. _EMPTY which only takes the type argument. But I guess it's not ideal. In the beginning I thought "Just add an overload with 1 arg". And then I understood, yeah, macros are not functions 😩

radistmorse commented 1 year ago

It can be easily fixed with __VA_OPT__

#define NLOHMANN_DEFINE_TYPE_INTRUSIVE(Type, ...)  \
    friend void to_json(nlohmann::json& __VA_OPT__(nlohmann_json_j), const Type& __VA_OPT__(nlohmann_json_t)) { __VA_OPT__(NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__))) } \
    friend void from_json(const nlohmann::json& __VA_OPT__(nlohmann_json_j), Type& __VA_OPT__(nlohmann_json_t)) { __VA_OPT__(NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__))) }

But __VA_OPT__ requires c++20. I guess that also can be circumvented with

#ifndef JSON_HAS_CPP_20
#define __VA_OPT__(x) x
#endif

but that still won't fix the bug for the earlier versions.