...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
While the value
container makes it easy to create ad-hoc structures, often it is necessary
to convert between JSON and specific user-defined types. Converting from
a type T
to value
is done by value_from
. The conversion in the
opposite direction is done with value_to
.
std::vector< int > v1{ 1, 2, 3, 4 }; // Convert the vector to a JSON array value jv = value_from( v1 ); assert( jv.is_array() ); array& ja = jv.as_array(); assert( ja.size() == 4 ); for ( std::size_t i = 0; i < v1.size(); ++i ) assert( v1[i] == ja[i].as_int64() ); // Convert back to vector< int > std::vector< int > v2 = value_to< std::vector< int > >( jv ); assert( v1 == v2 );
A customization point is a mechanism where a library
delegates behavior of some operation to the user, or gives the use the option
of controlling the behavior of some operation for a specific type. Within
the standard library, the swap
function is a customization point that uses argument-dependent
lookup to find user-provided overloads within the namespace of the
arguments:
template< class T > void identity_swap( T& a, T& b ) { // introduces the declaration of // std::swap into this scope using std::swap; if( &a == &b ) return; // the overload set will contain std::swap, // any declarations of swap within the enclosing // namespace, and any declarations of swap within // the namespaces associated with T swap( a, b ); }
Another example would be the class template std::hash
,
which can be specialized for some type T
to implement custom behavior:
template< std::size_t N > struct static_string { }; namespace std { template< std::size_t N > class hash< static_string< N > > { public: std::size_t operator()(const static_string< N >& str ) const noexcept { return std::hash< std::string >()( str ); } }; } // std
While these mechanisms work, they are not without problems. Boost.JSON implements
value conversion customization points using the tag_invoke
mechanism outlined in P1895,
allowing users to define conversions to and from their own types. In essence,
tag_invoke
provides a uniform
interface for defining customization points by using argument-dependent lookup
to find a viable function from the point at which it's 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.
tag_invoke
overloads
In all cases, conversions are done by calling an appropriate overload of
tag_invoke
. For value_from
, these have the form:
void tag_invoke( const value_from_tag&, value&, T );
Likewise, the overloads of tag_invoke
called by value_to
take the form:
T tag_invoke( const value_to_tag< T >&, const value& );
In both cases, overloads for user-provided types can be implemented:
template< class T > void tag_invoke( const value_from_tag&, value& jv, std::complex< T > const& t) { // Store a complex number as a 2-element array // with the real part followed by the imaginary part jv = { t.real(), t.imag() }; } template< class T > std::complex< T > tag_invoke( const value_to_tag< std::complex< T > >&, value const& jv ) { return std::complex< T >( jv.as_array().at(0).to_number< T >(), jv.as_array().at(1).to_number< T >()); }
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:
template< class T > struct vec3 { T x, y, z; }; template< class T > void tag_invoke( const value_from_tag&, value& jv, const vec3<T>& vec ) { jv = { { "x", vec.x }, { "y", vec.y }, { "z", vec.z } }; }
When value_from
is called, the tag_invoke
function template will be found by argument-dependent lookup and used to
perform the conversion:
vec3< int > pos = { 4, 1, 4 }; value jv = value_from( pos ); assert( serialize( jv ) == "{\"x\":4,\"y\":1,\"z\":4}" );
In addition to user-provided overloads of tag_invoke
,
the library will add its own function to the overload set when certain constraints
are satisfied. The library provided overloads have no special prioritization
over those provided by the user, so care should be taken to avoid writing
ambiguous declarations:
template< class T, typename std::enable_if< std::is_floating_point< T >::value>::type* = nullptr > void tag_invoke( const value_from_tag&, value& jv, T t ) { jv = std::llround( t ); }
Upon calling this function, overload resolution will fail because the library already provides an overload for floating-point types:
value jv = value_from( 1.5 ); // error
Library-provided overloads of tag_invoke
come in two variants: those that convert between JSON types (known as built-in
conversions), and those that convert to/from container and string
types (known as generic conversions). Generic conversions
offer convenience by eliminating the need to write repetitive overloads for
types that model common C++ concepts (e.g. sequence containers, associative
containers, tuples, and strings).
std::map< std::string, vec3< int > > positions = { { "Alex", { 42, -60, 18 } }, { "Blake", { 300, -60, -240} }, { "Carol", { -60, 30, 30 } } }; // conversions are applied recursively; // the key type and value type will be converted // using value_from as well value jv = value_from( positions ); assert( jv.is_object() ); object& jo = jv.as_object(); assert( jo.size() == 3 ); // The sum of the coordinates is 0 assert( std::accumulate( jo.begin(), jo.end(), std::int64_t(0), []( std::int64_t total, const key_value_pair& jp ) { assert ( jp.value().is_object() ); const object& pos = jp.value().as_object(); return total + pos.at( "x" ).as_int64() + pos.at( "y" ).as_int64() + pos.at( "z" ).as_int64(); } ) == 0 );
The function template value_from
provides an interface
to construct a value
from a user- or library-provided
type T
. The optionally supplied
storage_ptr
argument is used as the memory_resource
for the resulting
value
object. The parameter of type value&
is the result of the conversion; this
ensures that the storage_ptr
is correctly propagated
to the result. For example, consider the following struct:
struct customer { std::uint64_t id; std::string name; bool late; customer() = default; customer( std::uint64_t i, const std::string& n, bool l ) : id( i ), name( n ), late( l ) { } explicit customer( value const& ); }; void tag_invoke( const value_from_tag&, value& jv, customer const& c ) { // Assign a JSON value jv = { { "id", c.id }, { "name", c.name }, { "late", c.late } }; }
If our store has a lot of customers, it may be desirable to use a monotonic_resource
when serializing
customer
objects to JSON.
value_from
ensures that the correct memory_resource
is used:
std::vector< customer > customers = { customer( 0, "Alison", false ), customer( 1, "Bill", false ), customer( 3, "Catherine", true ), customer( 4, "Doug", false ) }; storage_ptr sp = make_shared_resource< monotonic_resource >(); value jv = value_from( customers, sp ); assert( jv.storage() == sp ); assert( jv.is_array() );
In addition to the user-provided overloads found by argument-dependent lookup,
the library provides its own overload of tag_invoke
when certain conditions are met.
If, for the type T
being
converted
std::is_assignable<value&,
T&&>::value
is true
,
or
T
satisfies StringLike,
or
T
satisfies TupleLike,
or
T
satisfies FromMapLike,
or
T
satisfies FromContainerLike.
Then a function template of the form
template< class T > void tag_invoke( value_from_tag, value& jv, T&& t );
is added to the set of user-provided overloads found by argument-dependent
lookup; it performs the conversion corresponding to first condition met by
T
in the above list. For
example, if T
satisfies both
FromMapLike and FromContainerLike,
the conversion will be performed the one corresponding to FromMapLike;
it will not be ambiguous.
// Satisfies both FromMapLike and FromContainerLike std::unordered_map< std::string, bool > available_tools = { { "Crowbar", true }, { "Hammer", true }, { "Drill", true }, { "Saw", false }, }; value jv = value_from( available_tools ); assert( jv.is_object() );
The conversion performed when the first condition is met (the library-provided
built-in conversion) is assignment to the value
parameter. For the generic
conversions, types that satisfy TupleLike or FromContainerLike
are converted to array
, those that satisfy FromMapLike
are converted to object
, and types that satisfy
StringLike are converted to string
.
The function template value_to
provides an interface to
construct a type T
from a
value
.
In contrast to value_from
, no output parameter
is used as there is no storage_ptr
to propagate.
std::complex< double > c1 = { 3.14159, 2.71828 }; // Convert a complex number to JSON value jv = value_from( c1 ); assert ( jv.is_array() ); // Convert back to a complex number std::complex< double > c2 = value_to< std::complex< double > >( jv );
As with value_from
,
the library provides its own overload of tag_invoke
when certain conditions are met.
If, for the type T
T
is value
, object
, array
, string
, string_view
, __value_ref__,
std::initializer_list<value_ref>
or bool
, or if std::is_arithmetic<T>::value
is true
,
or
T
satisfies StringLike,
or
T
satisfies ToMapLike,
or
T
satisfies ToContainerLike.
Then a function template of the form
template< class T > T tag_invoke( value_to_tag< T >, const value& jv );
is added to the set of user-provided overloads found by argument-dependent
lookup. As with value_from
, it performs the conversion
corresponding to first condition met by T
in the above list. Given the following definition of customer::customer( const value& )
:
customer tag_invoke( const value_to_tag<customer>&, const value& jv ) { // at() throws if `jv` is not an object, or if the key is not found. // as_uint64() will throw if the value is not an unsigned 64-bit integer. std::uint64_t id = jv.at( "id" ).as_uint64(); // We already know that jv is an object from // the previous call to jv.as_object() succeeding, // now we use jv.get_object() which skips the // check. value_to will throw if jv.kind() != kind::string std::string name = value_to< std::string >( jv.get_object().at( "name" ) ); // id and name are constructed from JSON in the member // initializer list above, but we can also use regular // assignments in the body of the function as shown below. // as_bool() will throw if kv.kind() != kind::bool bool late = jv.get_object().at( "late" ).as_bool(); return customer(id, name, late); }
Objects of type customer
can be converted to and from value
:
customer c1( 5, "Ed", false ); // Convert customer to value value jv = value_from( c1 ); // Convert the result back to customer customer c2 = value_to< customer >( jv ); // The resulting customer is unchanged assert( c1.name == c2.name );
When the first condition is met, the conversion will simply return the object
of type T
stored within the
value
(e.g. using jv.as_object()
,
jv.as_array()
,
etc). When the second condition is met, the result of the conversion will
be T(jv)
. As
with value_from
,
when generic conversions are selected, an attempt will be made to convert
the value
to T
.
value available_tools = { { "Crowbar", true }, { "Hammer", true }, { "Drill", true }, { "Saw", false } }; assert( available_tools.is_object() ); auto as_map = value_to< std::map< std::string, bool > >( available_tools ); assert( available_tools.as_object().size() == as_map.size() );
Each of the following tables specify valid operations on a type or expression
thereof meeting the requirement R. A requirement Req
prefixed with From/To does not define a single requirement;
it defines the two requirements FromReq and ToReq
which correspond to value_to
and value_from
, respectively.
In each of the following:
T
is a type that satisfies
R,
e
is an lvalue of type
T
,
has_value_trait
names
the template has_value_to
(when R
is prefixed with To) or has_value_from
(when R
is prefixed with From).
using
std::begin;
and using std::end;
precede the point at which the validity
and semantics of an expression is determined.
Table 1.3. Valid expressions
Expression |
Type |
Semantics and Constraints |
---|---|---|
|
|
Constraints: |
Table 1.4. Valid expressions
Expression |
Type |
Semantics and Constraints |
---|---|---|
|
|
Constraints: |
|
|
Constraints: |
|
|
Constraints: |
Table 1.5. Valid expressions
Expression |
Type |
Semantics and Constraints |
---|---|---|
|
|
Constraints: |
|
|
|
|
|
In the following table vt
is a prvalue of type T::value_type
.
Table 1.6. Valid expressions
Expression |
Type |
Semantics and Constraints |
---|---|---|
|
|
|
|
|
|
|
|
Constraints: |
|
|
Constraints for FromMapLike:
Constraints for ToMapLike: |
|
|
Constraints: |
|
|
Constraints: |