With boost::json, Is it possible to declare two separate value_from conversions in different namespaces?

Viewed 117

Lets say I have a class Customer in namespace Data. Using boost, I know I can convert this into valid JSON by declaring a function with the signature: void tag_invoke( const value_from_tag&, value& jv, Customer const& c ).

However, in all of the examples in the boost documentation, it appears that this tag_invoke function must reside in the Data namespace, i.e. the same namespace as Customer.

I would like to know if it is possible to have something like the following:

namespace FileIO
{
void tag_invoke( const value_from_tag&, value& jv, Data::Customer const& c );
{
    jv = {
        { "id", c.id },
        { "name", c.name },
        { "late", c.late }
    };
}
}

namespace Network
{
void tag_invoke( const value_from_tag&, value& jv, Data::Customer const& c )
{
    jv = {
        { "id", c.id },
        { "late", c.late }
    };
}
}

In other words, have two different implementations of the value conversion, where the current namespace would determine which one gets used. I've not had any success getting it working, being met with various "no matching call to value_from_impl" errors.

In case it helps, my CMake setup is such that Data, FileIO, and Network are all separate targets, where both FileIO and Network are linked to/depend Data, but not to each other, and Data depends on no other libraries.

Any advice would be appreciated

1 Answers

Tag invoke is a more useful implementation of the old-fashioned ADL customization points. But it still heavily uses ADL. That means that associated namespaces are considered for overload resolution.

See https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1895r0.pdf for rationale.

You can declare unrelated overloads in other namespaces but,

  • best case that namespace is not associated, and the overload is ignored
  • worst case, that namespace is also associated, and the call becomes ambiguous.

In case it helps, my CMake setup is such that Data, FileIO, and Network are all separate targets, where both FileIO and Network are linked to/depend Data, but not to each other, and Data depends on no other libraries.

That sounds like you have isolated source (translation units). You can define different customization points locally in those TUs. Make (doubly) sure they have internal linkage, so that you don't risk ODR violation. E.g.

namespace Data
{
     namespace { // anonymous namespace for file static (no external linkage)
        void tag_invoke( const value_from_tag&, value& jv, Customer const& c );
        {
          // ...
        };
     }
}

Alternative

Alternatively, you can use tagged wrappers, a bit like manipulators:

Live On Coliru

#include <boost/json.hpp>
#include <boost/json/src.hpp>
#include <iostream>
namespace json = boost::json;

template <typename T> struct UPPERCASE {
    T const& ref;
    UPPERCASE(T const& ref) : ref(ref) {}
};

namespace Data {
    struct Customer {
        int         id;
        std::string name;
        bool        late;

        friend void tag_invoke(json::value_from_tag,json::value&jv, Customer const& c) {
            jv = {{"id", c.id}, {"name", c.name}, {"late", c.late}};
        }
        friend void tag_invoke(json::value_from_tag, json::value& jv, UPPERCASE<Customer> const& c) {
            jv = {{"ID", c.ref.id}, {"NAME", c.ref.name}, {"LATE", c.ref.late}};
        }
    };
} // namespace Data

int main() {
    Data::Customer const c{1, "One", true};

    std::cout << json::value_from(c) << "\n";
    std::cout << json::value_from(UPPERCASE(c)) << "\n";
}

Prints

{"id":1,"name":"One","late":true}
{"ID":1,"NAME":"One","LATE":true}
Related