Custom Conversions
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.
Conversion Traits
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:
-
discard candidates that are unrelated to that particular customization point, and
-
embed the user-defined type into the arguments list (e.g. by using a tag type template such as
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 );