Custom Conversions
Boost.JSON uses two mechanisms to customize conversion between value
and user types. One mechanism involves specializing use_category. The
other one is more powerful and requires defining overloads of tag_invoke.
Both mechanisms will be further explained in this section.
use_category
The library generally deduces the appropriate implementation of conversion for a given type using internal type traits. These type traits attempt to deduce the correct conversion category by checking if the type satisfies the requirements associated with that category. The categories are checked one by one, in the order of their appearance in the table in the previous subsection, and the first one whose requirements are satisfied is selected.
In some cases, though, a type would match a category 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 template use_category for that type to be
equivalent to the intended category.
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 tuple_category has a lower priority
than sequence_category. In order to circumvent this, the user only needs to
specialize use_category for ip_address.
namespace boost {
namespace json {
template<>
struct use_category<user_ns::ip_address> : tuple_category
{ };
} // 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 become 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 );