0

I'm using nlohmann's single header json library to serialise a class I wrote. I want to use this class with various types (including but not limited to boost's multiprecision types). The problem is that some types including boost's cpp_bin_float_quad don't support to_json or from_json.

If my class's member variable doesn't have its own to/from_json then the class fails to compile. In this case, users should still be able to use this class's core functionality (member functions, etc) but not let them save/load the class's member variables.

Ideally, if the user needs to save/load the class with their custom types, then they can make the to/from_json functions for their custom types. Preferably the solution will work for C++11.

Here's a mwe to show the issue:

#include "json.hpp"
#include <boost/multiprecision/cpp_bin_float.hpp>
#include <iostream>

template <typename T>
class A {
protected:
  T m_x;
public:
  A() {m_x = static_cast<T>(1);}
  A(T x) : m_x(x) {}

  template <typename T1>
  friend void to_json(nlohmann::json& j, const A<T1>& a);
  template <typename T1>
  friend void from_json(const nlohmann::json& j, A<T1>& a);
};

/* I want useless versions of these two functions
 * if T doesn't have it's own to/from_json functions! */
template <typename T>
void to_json(nlohmann::json& j, const A<T>& a) {
  j = nlohmann::json{{"x", a.m_x}};
}

template <typename T>
void from_json(const nlohmann::json& j, A<T>& a) {
  j.at("x").get_to(a.m_x);
}

// no problems with this MY_TYPE
//#define MY_TYPE double

// fails because cpp_bin_float_quad doesn't have to/from_json
#define MY_TYPE boost::multiprecision::cpp_bin_float_quad

int main(){
  A<MY_TYPE> a(2);
  nlohmann::json js = a;
  std::cout << js << std::endl;
  auto s2 = js.get<A<MY_TYPE>>();
}

When I try compiling I see errors like these:

mwe.cpp:23:5: error: no matching function for call to ‘nlohmann::json_v3_11_0::basic_json<>::basic_json(<brace-enclosed initializer list>)’
   23 |   j = nlohmann::json{{"x", a.m_x}};

mwe.cpp:28:19: error: no matching function for call to ‘nlohmann::json_v3_11_0::basic_json<>::get_to(boost::multiprecision::number<boost::multiprecision::backends::cpp_bin_float<113, boost::multiprecision::backends::digit_base_2, void, short int, -16382, 16383>, boost::multiprecision::et_off>&) const’
   28 |   j.at("x").get_to(a.m_x);
  • 1
    and what do you expect the compiler to do in this case? – alfC Aug 16 '22 at 21:27
  • @alfC The compiler certainly behaves the way I'd expect it to for this code. Ideally, I want to tell it to use a version of to/from_json which does nothing instead. – Shawn McAdam Aug 16 '22 at 22:16

2 Answers2

3

It looks like you want these friend functions not to exist when they don’t make sense. Try changing the signature of your functions to something like this

template <typename T>
auto to_json(nlohmann::json& j, const A<T>& a) -> decltype(nlohmann::json{{"x", a.m_x}}, void()) {
  j = nlohmann::json{{"x", a.m_x}};
}
alfC
  • 14,261
  • 4
  • 67
  • 118
  • Why would using decltype like that would make the friend functions stop existing when they don't make sense? – Shawn McAdam Aug 17 '22 at 00:08
  • 1
    @ShawnMcAdam It's a form of [SFINAE](https://en.cppreference.com/w/cpp/language/sfinae) that `decltype`. `decltype` checks for the validity of the template-dependent expression (e.g. it uses the `template`'s `T` argument). If it can't deduce a type, then the template substitution fails -- which isn't an error, and thus just doesn't produce a valid overload. – Human-Compiler Aug 17 '22 at 00:23
  • I see, thank you for clarifying @Human-Compiler. Is there a way to specialise to_json when the decltype fails? – Shawn McAdam Aug 17 '22 at 01:05
  • 1
    @ShawnMcAdam Not with trailing decltype, that's when you will need traits similar to what you're doing with `to_json`/`from_json` (though ideally not from the `detail` namespace). That said, this `decltype` just detects that a `json` object is constructible from these arguments. Can you use [`std::is_constructible`](https://en.cppreference.com/w/cpp/types/is_constructible) to handle both cases without relying on the author's private `detail` namespace? – Human-Compiler Aug 17 '22 at 01:40
  • @Human-Compiler I think `std::is_constructible` is exactly the solution I was looking for. I have updated my solution to avoid using nlohmann's `detail` namespace (and the solution looks cleaner too). Thank you very much! – Shawn McAdam Aug 17 '22 at 06:43
2

Looks like we can use SFINAE with std::is_constructible to solve this problem (simply test if the json type can be constructed with). The specific solution I used for my mwe was this:

template <typename T>
class A {
protected:
  T m_x;
public:
  A() {m_x = static_cast<T>(1);}
  A(T x) : m_x(x) {}

  template <typename T1,
           typename std::enable_if<std::is_constructible<nlohmann::json,T1>::value, bool>::type>
  friend void to_json(nlohmann::json& j, const A<T1>& a);

  template <typename T1,
           typename std::enable_if<std::is_constructible<nlohmann::json,T1>::value, bool>::type>
  friend void from_json(const nlohmann::json& j, A<T1>& a);
};


template <typename T,
         typename std::enable_if<std::is_constructible<nlohmann::json,T>::value, bool>::type = true>
void to_json(nlohmann::json& j, const A<T>& a) {
  j = nlohmann::json{{"x", a.m_x}};
}

template <typename T,
         typename std::enable_if<!std::is_constructible<nlohmann::json,T>::value, bool>::type = true>
void to_json(nlohmann::json& j, const A<T>& a) {
  throw std::invalid_argument(std::string(typeid(T).name()) + " does not implement nlohmann's to_json");
}

template <typename T,
         typename std::enable_if<std::is_constructible<nlohmann::json,T>::value, bool>::type = true>
void from_json(const nlohmann::json& j, A<T>& a){
  j.at("x").get_to(a.m_x);
}

template <typename T,
         typename std::enable_if<!std::is_constructible<nlohmann::json,T>::value, bool>::type = true>
void from_json(const nlohmann::json& j, A<T>& a){
  throw std::invalid_argument(std::string(typeid(T).name()) + " does not implement nlohmann's from_json");
}

Edit: This solution used to depend on nlohmann::detail::has_from_json<nlohmann::json,T>::value. @Human-Compiler explains in the comments why this is a bad idea.

  • 2
    The `detail` namespace is a convention used by library authors for _internal-only_ types. In other words: **Here there be dragons**. Do not depend on another library's `detail` namespace types, since there is no guarantee that it will exist or behave the same between different versions. You can use other techniques like mentioned in `alfC`'s answers to SFINAE this away, or you can implement them yourself with the [detected idiom](https://en.cppreference.com/w/cpp/experimental/is_detected) or [`void_t`](https://en.cppreference.com/w/cpp/types/void_t) metaclass. – Human-Compiler Aug 17 '22 at 00:20
  • Ah, whoops! I did not know that. Is it teeeechnically okay to work with nlohmann's detail namespace? I can always package the appropriate version of its header with any code I write. – Shawn McAdam Aug 17 '22 at 01:13
  • 1
    That's more a question for the author, but in general: no. `detail` namespaces are generally used for header-only code as the private implementation. They can change drastically even in patch versions -- making it bad to depend on. _"I can always package the appropriate version of its header with any code I write"_ -- C++ is rarely this easy. If a consumer of your code also depends on a different version of `nlohmann::json`, then what? Worse yet, their header search path will just pick up whichever one it finds first. In general, I'd advise against it. – Human-Compiler Aug 17 '22 at 01:26
  • 1
    @alfC I don't see where is the problem? isn't it `enable_if::type=true`? – apple apple Aug 17 '22 at 07:42
  • 1
    If you have access to c++17 `if constexpr`, you might replace SFINAE&overload by `template void to_json([[maybe_unused]]nlohmann::json& j, [[maybe_unused]]const A& a) { if constexpr (std::is_constructible_v) { j = nlohmann::json{{"x", a.m_x}}; } else { throw std::invalid_argument(std::string(typeid(T).name()) + " does not implement nlohmann's to_json") } }` – Jarod42 Aug 17 '22 at 07:49