![]() |
Home | Libraries | People | FAQ | More |
Boost.JSON uses two mechanisms to customize conversion between value
and user types. One mechanism
involves specializing type traits. The other one is more powerful and requires
defining overloads of tag_invoke
.
Both mechanisms will be further explained in this section.
Previously a number of conversion type traits, like is_tuple_like
or is_sequence_like
, were introduced.
The library tries the traits one after another and uses the implementation
that corresponds to the first matching trait. In some cases, though, a type
would match a trait with a higher priority, but the user intends for it to
belong to a lower priority category. If this happens the user can specialize
the trait that's not supposed to match for that type to be an equivalent
of std::false_type
.
Consider this type:
namespace user_ns { class ip_address { public: ip_address( unsigned char oct1, unsigned char oct2, unsigned char oct3, unsigned char oct4 ); const unsigned char* begin() const; const unsigned char* end() const; private: std::array<unsigned char, 4> octets_; }; template< std::size_t N > unsigned char get(const ip_address& addr); } // namespace user_ns namespace std { template<> struct tuple_size< user_ns::ip_address > : std::integral_constant<std::size_t, 4> { }; template< std::size_t N > struct tuple_element< N, user_ns::ip_address > { using type = unsigned char; }; } // namespace std
It exposes both a sequence API and a tuple API. But converting from value
to user_ns::ip_address
would not be able to use implementation
for sequences, since those are constructed empty and then populated one element
at a time, while ip_address
has a fixed size of 4. The tuple conversion would fit, though. The only problem
is that is_tuple_like
has a lower priority
than is_sequence_like
. In order to circumvent
this, the user only needs to specialize is_sequence_like
to not match ip_address
.
namespace boost { namespace json { template<> struct is_sequence_like< user_ns::ip_address > : std::false_type { }; } // namespace json } // namespace boost
tag_invoke
overloads
The second, more powerful approach, is to provide the conversion implementation
yourself. With Boost.JSON this is done by defining an overload of tag_invoke
function (the benefits of this
mechanism are outlined in C++
proposal P1895). In essence, tag_invoke
provides a uniform interface for defining customization points by using argument-dependent
lookup to find a viable overload from the point at which it is called. As
the name suggests, a tag type is passed as an argument in order to:
value_to_tag<T>
) so that its associated
namespaces and entities are examined when name lookup is performed.
This has the effect of finding user-provided tag_invoke
overloads, even if they are declared (lexically) after the definition of
the calling function.
Overloads of tag_invoke
called
by value_from
take the form:
void tag_invoke( const value_from_tag&, value&, T );
While overloads of tag_invoke
called by value_to
take the form:
T tag_invoke( const value_to_tag< T >&, const value& );
If we implemented conversion for user_ns::ip_address
manually with this approach, it would look like this:
void tag_invoke( const value_from_tag&, value& jv, ip_address const& addr ) { // Store the IP address as a 4-element array of octets const unsigned char* b = addr.begin(); jv = { b[0], b[1], b[2], b[3] }; } ip_address tag_invoke( const value_to_tag< ip_address >&, value const& jv ) { array const& arr = jv.as_array(); return ip_address( arr.at(0).to_number< unsigned char >(), arr.at(1).to_number< unsigned char >(), arr.at(2).to_number< unsigned char >(), arr.at(3).to_number< unsigned char >() ); }
Since the type being converted is embedded into the function's signature, user-provided overloads are visible to argument-dependent lookup and will be candidates when a conversion is performed:
ip_address addr = { 127, 0, 0, 12 }; value jv = value_from( addr ); assert( serialize( jv ) == R"([127,0,0,12])" ); // Convert back to IP address ip_address addr2 = value_to< ip_address >( jv ); assert(std::equal( addr.begin(), addr.end(), addr2.begin() ));
Users can freely combine types with custom conversions with types with library-provided conversions. The library handles them correctly:
std::map< std::string, ip_address > computers = { { "Alex", { 192, 168, 1, 1 } }, { "Blake", { 192, 168, 1, 2 } }, { "Carol", { 192, 168, 1, 3 } }, }; // conversions are applied recursively; // the key type and value type will be converted // using value_from as well value jv = value_from( computers ); assert( jv.is_object() ); value serialized = parse(R"( { "Alex": [ 192, 168, 1, 1 ], "Blake": [ 192, 168, 1, 2 ], "Carol": [ 192, 168, 1, 3 ] } )"); assert( jv == serialized );