Boost C++ Libraries

...one of the most highly regarded and expertly designed C++ library projects in the world. Herb Sutter and Andrei Alexandrescu, C++ Coding Standards

This is the documentation for a snapshot of the develop branch, built from commit afebb7b30f.
PrevUpHomeNext

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:

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 );

PrevUpHomeNext