AnyValue

The AnyValue class represents runtime introspectable values that can be empty, scalar, structured or array values. Objects of this type respect value semantics and can easily be passed, assigned or returned by value.

Architecture

To support value semantics, the design is based on a single concrete AnyValue class that contains polymorphic implementations for the different values supported (scalar, array, etc.)

The following figure shows the relevant part of the class diagram.

_images/AnyValue_classes.png

Although largely an implementation detail, the underlying IValueData of an AnyValue object is never shared between different objects. This prevents nasty surprises when using these objects in a multithreaded environment.

Construction

Typically, AnyValue objects are created in one of two ways:

  • Statically: they are encoded directly by the programmer.

  • Dynamically: the values are runtime generated by parsing some other data structure.

To accomodate for these different use cases, the AnyValue class provides different options to create a specific value object.

For all AnyType objects, a corresponding AnyValue can be created with default values for its leaf values by using the following constructor:

explicit AnyValue::AnyValue(const AnyType &anytype)
Parameters:

anytype – Type of the constructed AnyValue.

Construct a value object with the given type.

There also exists a constructor that takes both an AnyType and an AnyValue parameter, that is provided mostly as a conversion constructor (see Conversion methods):

AnyValue::AnyValue(const AnyType &anytype, const AnyValue &anyvalue)
Parameters:
  • anytype – Type of the constructed AnyValue.

  • anyvalue – Value for the constructed AnyValue, with possible conversions.

Throws:

InvalidConversionException – When the value couldn’t be converted to the given type.

Construct a value object with the given type and value.

Empty value

The empty value is provided as a default null value, containing no data.

Empty values are created by using the default AnyValue constructor:

// Use default constructor to create an empty value:
AnyValue empty_value{};

Scalar values

Scalar values provide the elementary building blocks of all AnyValue objects. They contain a single arithmetic number (including booleans) or a string. The contained fundamental value type is an std::string or one of the following type aliases:

type boolean
type char8
type int8
type uint8
type int16
type uint16
type int32
type uint32
type int64
type uint64
type float32
type float64

The scalar AnyValue objects can be constructed from elementary integral, floating point or string types by using the converting constructors:

AnyValue::AnyValue(boolean val) const
AnyValue::AnyValue(char8 val) const
AnyValue::AnyValue(int8 val) const
AnyValue::AnyValue(uint8 val) const
AnyValue::AnyValue(int16 val) const
AnyValue::AnyValue(uint16 val) const
AnyValue::AnyValue(int32 val) const
AnyValue::AnyValue(uint32 val) const
AnyValue::AnyValue(uint64 val) const
AnyValue::AnyValue(float32 val) const
AnyValue::AnyValue(float64 val) const

Create an AnyValue object with the passed value as underlying fundamental value.

AnyValue::AnyValue(const std::string &val) const
AnyValue::AnyValue(const char *val) const

Create an AnyValue object of String type and initialize it with the given value.

Due to these non-explicit constructors, it is possible to construct scalar AnyValue objects of specific numeric types by numerical conversion from standard numeric literal values (see also Conversion methods for the supported conversions):

// Create a 64 bit unsigned integer value from a standard integer literal:
AnyValue my_uint64{UnsignedInteger64Type, 56};

Array values

Array values represent fixed size arrays of values of the same type. These are constructed using a dedicated constructor:

// Create array value containing 20 boolean values and provide a name:
AnyValue my_bool_array(20, BooleanType, "TwentyBooleans");

The last argument of this constructor is optional and if not provided, the typename will be an empty string.

To construct an array value with specific element values in a single statement, a convenience function can be used that accepts a list of AnyValue elements:

AnyValue my_int_array = ArrayValue(
   {10, 20, 30},
   "ThreeIntegers");

Structured values

Structured values correspond to the structured types (see Structured types). They are very similar to those type structures, but contain specific values in each of the leaf nodes, which are always scalar.

As for the structured types, structured values can be constructed by adding subvalues to an existing structured value using the AnyValue::AddMember() method.

The following example shows how this method can be used to populate a structured value:

// Create simple structured value containing:
// - an account name of StringType
// - an account number of UnsignedInteger64Type
// - an activated flag of BooleanType
auto account_val = EmptyStruct("AccountType");
account_val.AddMember("AccountName", {StringType, "John Vegas"});
account_val.AddMember("AccountNumber", {UnsignedInteger64Type, 44443789});
account_val.AddMember("Activated", true);

As can be seen in the previous example, the value argument for the AddMember method can omit the preferred type if the automatically deduced type is correct (Boolean in the last call).

Again, one can create structures of structures, structures of arrays, arrays of structures, etc.

To facilitate the static creation of structured values, a dedicated constructor is provided that accepts a braced-init-list of pairs of member names and values. This constructor also allows to provide a name for the type, which then needs to be passed as a final optional argument to the constructor:

// Create a customer value that contains:
// - a name (StringType)
// - an address structure, containing:
//   - a street field (StringType)
//   - a number field (UnsignedInteger16Type)
//   - a city field (StringType)
// And provide a type name: "CustomerType".
AnyValue customer_val({
  {"name", {StringType, "John Vegas"}},
  {"address", {
    {"street", {StringType, "Prosperity Road"}},
    {"number", {UnsignedInteger16Type, 1255}},
    {"city", "San Francisco"}
  }}
}, "CustomerType");

Copy and move

The AnyValue class provides copy and move constructors and assignment operators that are only slightly stricter than their AnyType counterparts. In general, all assignments are allowed, except when they would result in different elements of an array having different types. This means that assignment to array elements, or descendents thereof, will result in three alternatives:

  1. Source and destination value have the exact same type: the destination will become a copy of the source.

  2. The source value can be correctly converted to the destination type: the destination value will be a conversion of the source.

  3. Failure to assignment will throw an exception of type InvalidConversionException.

For the scalar types, conversion requires that the underlying value can be converted to the destination type and that it fits into that representation (e.g. a negative integer cannot be assigned to an unsigned value type).

For array values, conversion requires equal length arrays and compatibility for each of their elements.

Structured values can be converted to one another if they have the same member names (in the exact same order) and their member values are compatible. Note that for both array and structured value conversion, the type name is ignored.

Empty values can only be converted to and from other empty values.

The following example shows this behavior:

// Create a boolean value representing 'true':
AnyValue my_true{true};

// Assign this boolean value to an integer AnyValue:
AnyValue my_int{UnsignedInteger32Type};  // gets default value zero
my_int = my_true;  // my_int now contains the boolean value 'true' (type changed)

// Array element assignment:
AnyValue my_array = ArrayValue({0, 1, 2, 3});
my_array[1] = my_true;  // The second array element now contains the value '1', converted from
                        // boolean value 'true'.
AnyValue my_struct{{
   {"a": {UnsignedInteger8Type, 42}}
}};
my_array[0] = my_struct;  // ERROR! Throws InvalidConversionException.

Query methods

The AnyValue API contains a number of methods for querying specific information about the value. These are listed here.

TypeCode AnyValue::GetTypeCode() const
Returns:

TypeCode enumerator.

Retrieve the typecode enumerator for this object.

AnyType AnyValue::GetType() const
Returns:

AnyType of this object.

Retrieve an AnyType object, representing this object’s type.

std::string AnyValue::GetTypeName() const
Returns:

Type name.

Retrieve the type name.

std::vector<std::string> AnyValue::MemberNames() const
Returns:

List of member names.

Return an ordered list of all direct member names.

std::size_t AnyValue::NumberOfMembers() const
Returns:

Number of direct members for structured values and zero otherwise.

Retrieve the number of direct members. This is always zero for non-structured values.

std::size_t AnyValue::NumberOfElements() const
Returns:

Number of elements for an array value and zero otherwise.

Retrieve the number of elements in the array. Returns zero when the current value is not an array value.

bool AnyValue::HasField(const std::string &fieldname) const
Parameters:

fieldname – Name of the subvalue to search for.

Returns:

true when a subvalue with the given fieldname exists.

Check the presence of a subvalue with the given name. Composite fieldnames are supported.

Conversion methods

During assigment and construction, a number of implicit conversions can take place. These always concern scalar types and are build onto the converting constructors and the following explicit conversion method:

template<typename T>
T AnyValue::As() const
Returns:

The underlying value, cast to T.

Throws:

InvalidConversionException – When value couldn’t be converted to T.

Specializations of this function template are explicitly declared and perform the required conversions:

  • Booleans to numeric values: zero for false and one for true.

  • Numeric values to booleans: false if zero, true otherwise.

  • Numeric values to numeric values: standard conversion if the destination type can hold the source value.

All other scalar conversions are not allowed and throw an exception.

The AnyValue::As() method also supports a trivial cast to AnyValue, which just returns a copy of the object.

The following examples show the usage of this conversion method:

// Construct an unsigned 32 bit integer with value 19 and cast this to different types:
AnyValue val{UnsignedInteger32Type, 19};
bool is_non_zero = val.as<boolean>();  // is_non_zero is true
int16 signed_val = val.as<int16>();  // signed_val is also 19

There is also a member function that tries to convert another AnyValue to the type of the current AnyValue:

void AnyValue::ConvertFrom(const AnyValue &other)
Parameters:

otherAnyValue object to convert from.

Throws:

InvalidConversionException – If this operation could not successfully convert between

the different types.

Element access

The AnyValue class overloads the index operators to provide a natural way to access element values of a structured value. Array values also support integer indices for element access.

The overloaded operators are:

AnyValue &AnyValue::operator[](std::string fieldname)

Try to retrieve a reference to the member that is identified by the fieldname. This fieldname can describe non-direct members by encoding the navigation to deeper lying members. A dot (.) is used to separate individual names of structure members, while square brackets are used to address array elements.

Parameters:

fieldname – String encoding the path to a specific underlying value.

Returns:

AnyValue object if member value was found.

Throws:

InvalidOperationException – For values that do not support element access (empty or scalar values) or for fieldnames that cannot be correctly parsed/interpreted (wrong format or unknown key).

const AnyValue &AnyValue::operator[](std::string fieldname) const

Const version of the previous operator overload.

AnyValue &AnyValue::operator[](std::size_t idx)

This overload is only supported for array values and is provided as a convenience, while it doesn’t require passing integer indices as string values.

const AnyValue &AnyValue::operator[](std::size_t idx) const

Const version of the previous operator overload.

Accessing direct elements of an array value can thus be achieved by passing either a string representation of the index or by passing it directly:

// Create array value of 5 booleans with default values:
AnyType my_booleans{5, BooleanType};

// Change elements with index 2 and 3 to 'true':
my_booleans["[2]"] = true;
my_booleans[3] = true;

Modifier methods

The AnyValue API provides modifier methods to extend structured or array values. These methods are mostly used for runtime creation of AnyValue objects, e.g. during parsing. Note that the effect of these methods includes a change of the underlying type. Hence these methods will throw an exception of type InvalidOperationException when called on an element of an array or one of its descendents.

AnyValue &AnyValue::AddMember(const std::string &name, const AnyValue &value)
Parameters:
  • name – Member name to use.

  • valueAnyValue object for the member value.

Returns:

Reference to this to allow chaining such calls.

Throws:

InvalidOperationException – If this operation is not supported (e.g. not a structured value or a structured array element).

Add a member value for this structured value with the given name and value. Empty values are not allowed as member values.

AnyValue &AnyValue::AddElement(const AnyValue &value)
Parameters:

valueAnyValue object to add as an element.

Returns:

Reference to this to allow chaining such calls.

Throws:

InvalidOperationException – If this operation is not supported (not an array value or an element of an array of arrays).

Add an element to the end of this array value with the given value.

Comparison operators

Simple comparison of AnyValue objects is supported by overloading both the equality and inequality operator:

bool AnyValue::operator==(const AnyValue &other) const
Parameters:

other – Other AnyValue object to compare with the current.

Returns:

true when equal, false otherwise.

bool AnyValue::operator!=(const AnyValue &other) const
Parameters:

other – Other AnyValue object to compare with the current.

Returns:

true when not equal, false otherwise.

Note

Equality in the context of AnyValue objects requires compatibility of the embedded value leafs, rather than the more strict equality defined for AnyType objects:

  • Empty values are only equal to other empty values.

  • Scalar values are only equal when a successfull conversion of one value to the other’s type is exactly equal to the other’s value and the other way around (to assure symmetry).

  • Structured values are only equal to other structured values with the same type name, member names and values that compare equal. The order of members is also taken into account.

  • Array values are only equal to other array values with the same name, number of elements and elements that compare equal.

In cases where a strict equality test is required, one could test equality of both types and values.

A custom comparison function is also provided that handles arithmetic types (excluding ‘bool’, ‘char8’ and ‘string’):

CompareResult Compare(const AnyValue &lhs, const AnyValue &rhs)
Parameters:
  • lhsAnyValue object on the left hand side.

  • rhsAnyValue object on the right hand side.

Returns:

Enumerator indicating if lhs is less, greater, equivalent to rhs or if they are

unordered.

If one of the types is floating point, the comparison will use the largest floating point type among the given types as the common type (‘float64’ if there is one, ‘float32’ otherwise). Integral comparisons are only supported between types that are both signed or both unsigned. In these cases, the widest signed or unsigned integral type is used (‘int64’ or ‘uint64’). For all comparisons that are not supported (for example ‘string’, structures, signed with unsigned, etc.), the function returns ‘Unordered’.

Global functions

Besides the serialization/parsing functions, the software module provides the following global functions:

bool TryConvert(AnyValue &dest, const AnyValue &src)
Parameters:
  • destAnyValue object to assign to.

  • srcAnyValue object to convert from.

Returns:

true on successful conversion.

Try to convert an AnyValue to another AnyValue. When conversion fails, the destination is left unchanged and false is returned. This is basically a non-throwing version of void AnyValue::ConvertFrom(const AnyValue&).

bool TryAssign(AnyValue &dest, const AnyValue &src)
Parameters:
  • destAnyValue object to assign to.

  • srcAnyValue object to assign from.

Returns:

true on successful assignment.

Try to assign an AnyValue to another AnyValue. When assignment or conversion fails, the destination is left unchanged and false is returned. This is basically a non-throwing version of AnyValue& AnyValue::operator=(const AnyValue&)().

bool Increment(AnyValue &value)
Parameters:

valueAnyValue object to increment.

Returns:

true on successful increment.

Try to increment (add 1) an AnyValue. This is only supported for arithmetic types (excluding ‘bool’, ‘char8’ and ‘string’). In case of integer types, the behavior is defined to wrap around the maximum value to the minimum value (also for signed types).

bool Decrement(AnyValue &value)
Parameters:

valueAnyValue object to decrement.

Returns:

true on successful decrement.

Try to decrement (subtract 1) an AnyValue. This is only supported for arithmetic types (excluding ‘bool’, ‘char8’ and ‘string’). In case of integer types, the behavior is defined to wrap around the minimum value to the maximum value (also for signed types).