...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
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 );