Variant.cpp
// Variant.cpp
//
// Author David Barrett-Lennard
// (C)opyright Cedanet Pty Ltd 2009
@import "Ceda/cxObject/IObjectVisitor.h"
@import "Ceda/cxObject/PrintReflectedType.h"
@import "Ceda/cxObject/PrintReflectedVariable.h"
@import "ExampleUtils.h"
@import "ValidateSerialisation.h"
namespace Variant_Introduction
{
/*
X is a value type. A variable of type X is able to hold either a char8, int32,
float64 or string8 value. It is 'tagged' meaning that it is possible to determine
which of these 4 types of values a variable of type X currently holds.
X is implemented using a 32 bit tag value, plus sufficient space to take up the
largest of the types to union over.
In this case one of the types (string8) has non-trivial copy constructor, copy
assignment and destructor methods. Nevertheless it can appear in the variant.
For example, when a variable of type X destructs, it will call the string destructor
if it holds a string value.
*/
$variant+ X
{
ceda::char8;
ceda::int32;
ceda::float64;
ceda::string8;
};
/*
In the following we provide 3 different implementations of a function that writes
an X to an xostream.
These examples illustates the use of GetTag(), VARIANT_TAG(), is<T>(), as<T>().
*/
void Write1(ceda::xostream& os, const X& x)
{
switch(x.GetTag())
{
case 0 : os << x.as<ceda::char8>(); break;
case 1 : os << x.as<ceda::int32>(); break;
case 2 : os << x.as<ceda::float64>(); break;
case 3 : os << x.as<ceda::string8>(); break;
default : cxAssert(0);
}
}
void Write2(ceda::xostream& os, const X& x)
{
switch(x.GetTag())
{
case X::VARIANT_TAG(ceda::char8) : os << x.as<ceda::char8>(); break;
case X::VARIANT_TAG(ceda::int32) : os << x.as<ceda::int32>(); break;
case X::VARIANT_TAG(ceda::float64) : os << x.as<ceda::float64>(); break;
case X::VARIANT_TAG(ceda::string8) : os << x.as<ceda::string8>(); break;
default : cxAssert(0);
}
}
void Write3(ceda::xostream& os, const X& x)
{
if (x.is<ceda::char8>()) os << x.as<ceda::char8>();
else if (x.is<ceda::int32>()) os << x.as<ceda::int32>();
else if (x.is<ceda::float64>()) os << x.as<ceda::float64>();
else if (x.is<ceda::string8>()) os << x.as<ceda::string8>();
else cxAssert(0);
}
// So including the automatically generated function, we have 4 ways of writing an X
void WriteInFourWays(ceda::xostream& os, const X& x)
{
os << x;
os << " = "; Write1(os,x);
os << " = "; Write2(os,x);
os << " = "; Write3(os,x);
}
void Run()
{
ceda::TraceGroup g("Variant: Introduction");
// In this case the string8 type is the largest of the 4 types.
// sizeof(string8) is 12 bytes, so the size of X will be 16 bytes
Tracer() << "Size of X = " << sizeof(X) << '\n';
cxAssert(sizeof(X) == sizeof(ceda::int32) + sizeof(ceda::string8));
/*
X has no defined default value. Therefore there is no default constructor
available.
This limits the usability of X. For example, it cannot be used in arrays
and vectors.
If uncommented the following generates the following compiler error:
error C2512: 'X' : no appropriate default constructor available
*/
//X x;
//X x[10];
//ceda::xvector<X> L(3);
/*
Specifying values
-----------------
In order to specify values, X defines a number of constructors that allow for
implicit conversions from each of the types in the variant.
*/
Tracer() << "X('z') = " << X('z') << '\n';
Tracer() << "X(10) = " << X(10) << '\n';
Tracer() << "X(3.14) = " << X(3.14) << '\n';
Tracer() << "X(\"hello\") = " << X("hello") << '\n';
X x(0);
// The implicit conversions means we can also perform assignments...
{
ceda::TracerX os;
x = 'z'; os << "x = "; WriteInFourWays(os,x); os << '\n';
x = 10; os << "x = "; WriteInFourWays(os,x); os << '\n';
x = 3.14; os << "x = "; WriteInFourWays(os,x); os << '\n';
x = ceda::string8("hello"); os << "x = "; WriteInFourWays(os,x); os << '\n';
}
/*
Testing the type of a value
---------------------------
The template function:
bool is<T>() const
is defined for each type T in the variant. This returns true if the value is of
that type.
*/
if (x.is<ceda::int32>())
{
Tracer() << "x is an int32\n";
}
else if (x.is<ceda::string8>())
{
Tracer() << "x is a string8\n";
}
/*
Downcasting a variant
---------------------
The template functions
const T& as<T>() const;
T& as<T>();
are defined for each T, allowing for "downcasts" to type T as both an l-value and
an r-value. In debug builds an assertion fails if the variant isn't exactly of
that type. In release builds you get undefined behaviour
*/
x.as<ceda::string8>() += " world"; // as l-value
ceda::string8 s = x.as<ceda::string8>(); // as r-value
Tracer() << "s = " << s << '\n';
/*
Comparing two variants
operator==() is always defined on variants. Two variant values compare equal
if and only if they have the exact same type and the same value of that type.
*/
X x1 = 65;
X x2 = 66;
X x3 = 'A';
X x4 = ceda::int32('A');
cxAssert( x1 != x2 );
cxAssert( x1 != x3 );
cxAssert( x1 == x4 );
cxAssert( x1 == 65 );
/*
Using reflection
*/
ceda::TracerX os;
const ceda::ReflectedVariant& rv = ceda::FindReflectedVariant("Variant_Introduction::X");
ceda::PrintReflectedVariant(os, rv);
const ceda::ReflectedVariant& rv2 = ceda::GetReflectedVariant<X>();
cxAssert(&rv == &rv2);
os << "x1 = ";
ceda::PrintReflectedVariantVariable(os,rv, &x1);
os << '\n';
os << "x3 = ";
ceda::PrintReflectedVariantVariable(os,rv, &x3);
os << '\n';
}
}
/*
In the following example we show how a default value can be specified
*/
namespace Variant_Default
{
$variant X
{
// If a default value is to be specified then it must appear at the top of the
// variant definition.
// In this case the default is int32(23)
default 23;
ceda::int32;
ceda::float64;
};
void Run()
{
ceda::TraceGroup g("Variant: Defining a default value");
// In this case the string8 type is the largest of the 4 types.
// sizeof(string8) is 12 bytes, so the size of X will be 16 bytes
Tracer() << "Size of X = " << sizeof(X) << '\n';
cxAssert(sizeof(X) == sizeof(ceda::int32) + sizeof(ceda::float64));
X x;
Tracer() << "Default constructed x = " << x << '\n';
cxAssert( x.is<ceda::int32>() );
cxAssert( x.as<ceda::int32>() == 23 );
}
}
/*
In the following example, one of the types is 'void'.
A curious feature of doing so is that it defines a default constructor, so it is
not possible to define a default value with the 'default' keyword.
*/
namespace Variant_WithVoid
{
$variant X
{
// Trying to specify a default value causes a compiler error
//default int16(100);
void;
ceda::int16;
};
void Run()
{
ceda::TraceGroup g("Variant: Using the void type");
Tracer() << "Size of X = " << sizeof(X) << '\n';
// This assertion fails because of padding
//cxAssert(sizeof(X) == sizeof(int32) + sizeof(int16));
X x;
Tracer() << "Default constructed x = " << x << '\n';
// is<void>() can be used to test whether x holds the 'void' value
cxAssert( x.is<void>() );
/*
It doesn't make sense to cast the variant to type void, so the following if
uncommented will generate a compiler error:
*/
//x.as<void>();
}
}
/*
Arrays of the form
T[SIZE]
are mapped into the template std::array<T,SIZE]. This latter type must be used
for the is<> and as<> template functions
*/
namespace Variant_Arrays
{
$variant X
{
ceda::int32;
ceda::char8[10];
};
void Run()
{
ceda::TraceGroup g("Variant: Using arrays");
Tracer() << "Size of X = " << sizeof(X) << '\n';
cxAssert(sizeof(X) >= sizeof(ceda::int32) + sizeof(ceda::char8[10]));
X x(10);
Tracer() << "x = " << x << '\n';
cxAssert( x.is<ceda::int32>() );
cxAssert( x.as<ceda::int32>() == 10 );
typedef std::array<ceda::char8,10> array;
array s;
for (int i=0 ; i < 10 ; ++i) s[i] = ceda::char8('a' + i);
x = X(s);
Tracer() << "x = " << x << '\n';
cxAssert( x.is<array>() );
cxAssert( x.as<array>()[1] == 'b' );
}
}
/*
Any of the types within the variant can be specified as "heap allocated" by preceding
the type with an asterisk.
This causes the implementation to only record a pointer to the value within the space
shared by all the types in the variant.
However, this is really just an implementation detail. Semantically the variant
still records a value of one of the types. All the implicit conversions are supported
and the is<>() and as<>() template functions are defined without regard for the
indirection used in the physical implementation.
One reason for creating an indirection could be deal with a significant mismatch in
sizes of the types.
E.g.
$variant X
{
ceda::int32;
*ceda::char8[1000]; // heap allocated so sizeof(X) = 8, so that when int32's are
// stored it isn't so wasteful.
}
*/
namespace Variant_HeapAllocatedTypes
{
$variant X
{
default 0;
ceda::int32;
*ceda::float64; // Asterisk means the float64 is heap allocated
};
void Run()
{
ceda::TraceGroup g("Variant: Specifying heap allocated types");
Tracer() << "Size of X = " << sizeof(X) << '\n';
X x1 = 10;
cxAssert(x1.is<ceda::int32>());
cxAssert(!x1.is<ceda::float64>());
Tracer() << "x1 = " << x1 << '\n';
X x2 = 3.14;
cxAssert(!x2.is<ceda::int32>());
cxAssert(x2.is<ceda::float64>());
Tracer() << "x2 = " << x2 << '\n';
ceda::xvector<X> L;
L.push_back( 10 );
L.push_back( 20 );
L.push_back( 3.14 );
ValididateSerialisation(L);
}
}
/*
So far we have only show examples where the types in the variant aren't named.
In actual fact any of the types within a variant can be named. This name becomes the
name of a nested type within the implementation of the variant.
*/
namespace Variant_NamedTypes
{
// X can record an int32 tagged with type X::a, or an int32 tagged with type X::b.
$variant X
{
// A default can be specified by using the constructors defined by nested
// types X::a and X::b
default a(10);
ceda::int32 a;
ceda::int32 b;
};
void Run()
{
ceda::TraceGroup g("Variant: Example 1");
Tracer() << "sizeof(X) = " << sizeof(X) << '\n';
// Since a default was provided, the default constructor is available.
X x;
Tracer() << "x = " << x << '\n';
// X defines nested types : X::a and X::b.
// These types can be used in the is<> and as<> template functions
cxAssert(x.is<X::a>());
cxAssert(x.as<X::a>() == 10 );
// as<> can return an l-value, so we can use operator++()
x.as<X::a>()++;
Tracer() << "x = " << x << '\n';
// The nested types X::a, X::b can be used to specify values of X
X x1 = X::a(100);
X x2 = X::b(100);
cxAssert(x1 != x2); // Not equal because they have different tags
ceda::xvector<X> L;
L.push_back( X::a(10) );
L.push_back( X::b(20) );
L.push_back( X::b(30) );
ValididateSerialisation(L);
}
}
namespace Variant_LikeEnum
{
// When all the types are void a variant is like an enum
$variant Colour
{
default red();
void red;
void green;
void blue;
void white;
};
void Run()
{
ceda::TraceGroup g("Variant: Use as a simple enum");
Tracer() << "sizeof(Colour) = " << sizeof(Colour) << '\n';
Colour x = Colour::red();
Tracer() << "x = " << x << '\n';
ceda::xvector<Colour> L1;
L1.push_back(Colour::red());
L1.push_back(x);
L1.push_back(Colour::green());
Tracer() << "L1 = " << L1 << '\n';
ceda::xvector<Colour> L2 = L1;
Tracer() << "L2 = " << L2 << '\n';
ValididateSerialisation(L1);
}
}
namespace Variant_EmbeddedTuples
{
// Tuple type definitions can be embedded. The name always comes after the list
// of members.
// An asterisk can be used to force an indirection for that type in the normal way.
$variant X
{
void;
{ ceda::int32 a; ceda::int32 b; } x;
*{ bool b; ceda::string8 s; } y;
// This would generate an error: A name must be specified
//{ ceda::int32 s; };
};
void Run()
{
ceda::TraceGroup g("Variant: Using named embedded tuple types");
Tracer() << "sizeof(X) = " << sizeof(X) << '\n';
X x1;
Tracer() << "x = " << x1 << '\n';
cxAssert(x1.is<void>());
// X::x is a nested type which looks like the tuple
// struct x { int32 a; int32 b; };
X x2 = X::x(10,20);
Tracer() << "x1 = " << x2 << '\n';
X x3 = X::y(true,"hello");
Tracer() << "x2 = " << x3 << '\n';
// is<> can be used for X::x and X::y
cxAssert(x2.is<X::x>());
cxAssert(x3.is<X::y>());
// as<> can be used for X::x and X::y
x2.as<X::x>().a += 1000;
ceda::xvector<X> L;
L.push_back(x1);
L.push_back(x2);
L.push_back(x3);
ValididateSerialisation(L);
}
}
/*
The following example shows how we can mix anonymous and named types in a variant
in arbitrary ways.
*/
namespace Variant_Mixtures
{
$variant X1
{
void;
void a;
ceda::int32 xx;
ceda::int32;
ceda::float64;
*ceda::string8;
{ceda::int32 a; ceda::int64 b; } u;
*{ceda::int32 a; ceda::int64 b; X1 c; } v;
};
void Write(ceda::xostream& os, const X1& x)
{
switch(x.GetTag())
{
case X1::VARIANT_TAG(void) : cxAssert(x.is<void>()); break;
case X1::VARIANT_TAG(X1::a) : cxAssert(x.is<X1::a>()); break;
case X1::VARIANT_TAG(X1::xx) : cxAssert(x.is<X1::xx>()); os << x.as<X1::xx>(); break;
case X1::VARIANT_TAG(ceda::int32) : cxAssert(x.is<ceda::int32>()); os << x.as<ceda::int32>(); break;
case X1::VARIANT_TAG(ceda::float64) : cxAssert(x.is<ceda::float64>()); os << x.as<ceda::float64>(); break;
case X1::VARIANT_TAG(ceda::string8) : cxAssert(x.is<ceda::string8>()); os << x.as<ceda::string8>(); break;
case X1::VARIANT_TAG(X1::u) : cxAssert(x.is<X1::u>()); os << x.as<X1::u>(); break;
case X1::VARIANT_TAG(X1::v) : cxAssert(x.is<X1::v>()); os << x.as<X1::v>(); break;
default : cxAssert(0);
}
}
$variant X2
{
default a();
void a;
ceda::int32 b;
*ceda::int32 c;
{ceda::int32 f; ceda::int64 g; } d;
*{ceda::int32 f; ceda::int64 g; X2 x; } e;
};
}
namespace Variant_List
{
$variant Colour <<-reflect>>
{
default red();
void red;
void green;
void blue;
};
$variant X
{
default 0;
Colour;
ceda::int32;
ceda::string8;
};
$variant List
{
void;
* {X head; List tail;} node;
};
// Recurse down through the tree, printing all the head values
void Write(ceda::xostream& os, const List& n)
{
if (n.is<List::node>())
{
os << n.as<List::node>().head;
os << ' ';
Write(os, n.as<List::node>().tail);
}
}
void Run()
{
ceda::TraceGroup g("Variant: List example");
Tracer() << "sizeof(List) = " << sizeof(List) << '\n';
cxAssert(sizeof(List) == sizeof(ceda::int32) + sizeof(void*));
List x = List::node(X(Colour::red()), List::node(ceda::string8("hello"), List::node(30, List())));
Write(Tracer(), x);
Tracer() << "\nx = " << x << '\n';
}
}
/*
Using variants to represent trees
---------------------------------
Variants support recursion, and therefore can be used to express algebraic data types.
|
|
/ \
10 \
/ \
20 null
With normal approach in C++
struct Tree
{
virtual ~Tree() {}
};
struct Leaf : public Tree
{
int value;
};
struct Node : public Tree
{
Tree* left;
Tree* right;
};
size of Leaf = 8 (vtable ptr + value)
size of Node = 12 (vtable ptr + left ptr + right ptr)
_______[4] (Tree*)
|
|______ [12]
/ \
[8]---10 \______ [12]
/ \
[8]---- 20 null
total bytes = 4 + 12 + 12 + 8 + 8 = 44 bytes
heap allocations = 4
With variants:
$variant Tree
{
void;
int32;
* {Tree left; Tree right;} node;
};
Size of variant = 8 bytes (tag , leaf value or node ptr)
Size of node = 8 + 8 = 16 bytes
____ [8]
|
| ______ [16]
/ \
10 \ ______ [16]
/ \
20 null
total bytes = 8 + 16 + 16 = 40 bytes
heap allocations = 2
*/
namespace Variant_Tree
{
$variant Tree
{
void;
ceda::int32;
* {Tree left; Tree right;} node;
};
// Recurse down through the tree, printing all the 'leaf' values
void Write(ceda::xostream& os, const Tree& n)
{
if (n.is<ceda::int32>())
{
os << n.as<ceda::int32>();
}
else if (n.is<Tree::node>())
{
Write(os, n.as<Tree::node>().left);
os << ' ';
Write(os, n.as<Tree::node>().right);
}
}
void Run()
{
ceda::TraceGroup g("Variant: Tree example");
Tracer() << "sizeof(Tree) = " << sizeof(Tree) << '\n';
Tree w = Tree::node(10, Tree::node(20, Tree()) );
Write(Tracer(), w);
Tracer() << "\nw = " << w << '\n';
Tree x;
Tree y = 100;
Tree z = Tree::node(Tree(),25);
Tracer() << "x = " << x << '\n';
Tracer() << "y = " << y << '\n';
Tracer() << "z = " << z << '\n';
ceda::xvector<Tree> L1;
L1.push_back(w);
L1.push_back(x);
L1.push_back(y);
L1.push_back(z);
Tracer() << "L1 = " << L1 << '\n';
ceda::xvector<Tree> L2 = L1;
Tracer() << "L2 = " << L2 << '\n';
ValididateSerialisation(L1);
}
}
/*
For illustration purposes, we show the tree example again, this time using attribute
names for each type in the variant.
*/
namespace Variant_Tree_AllAttribs
{
$variant Tree
{
default null();
void null;
ceda::int32 leaf;
* {Tree left; Tree right;} node;
};
void Write(ceda::xostream& os, const Tree& n)
{
if (n.is<Tree::leaf>())
{
os << n.as<Tree::leaf>();
}
else if (n.is<Tree::node>())
{
Write(os, n.as<Tree::node>().left);
os << ' ';
Write(os, n.as<Tree::node>().right);
}
}
void Run()
{
ceda::TraceGroup g("Variant: Tree example - using all attributes");
Tracer() << "sizeof(Tree) = " << sizeof(Tree) << '\n';
Tree w = Tree::node( Tree::leaf(10), Tree::node(Tree::leaf(20), Tree::null()) );
Write(Tracer(), w);
Tracer() << "\nw = " << w << '\n';
Tree x = Tree::null();
Tree y = Tree::leaf(100);
Tree z = Tree::node(Tree::null(),Tree::leaf(25));
Tracer() << "x = " << x << '\n';
Tracer() << "y = " << y << '\n';
Tracer() << "z = " << z << '\n';
ceda::xvector<Tree> L1;
L1.push_back(w);
L1.push_back(x);
L1.push_back(y);
L1.push_back(z);
Tracer() << "L1 = " << L1 << '\n';
ceda::xvector<Tree> L2 = L1;
Tracer() << "L2 = " << L2 << '\n';
ValididateSerialisation(L1);
}
}
namespace Variant_In_Model
{
$variant BasicValue
{
default 0;
ceda::int8;
ceda::int16;
ceda::int32;
ceda::int64;
ceda::uint8;
ceda::uint16;
ceda::uint32;
ceda::uint64;
ceda::float32;
ceda::float64;
ceda::xstring;
};
$struct S1 :
model
{
BasicValue B[3];
}
{
};
$model M
{
BasicValue B[3];
};
$struct S2 : model M
{
};
void Run()
{
ceda::TraceGroup g("Variant in model example");
Tracer() << "sizeof(BasicValue) = " << sizeof(BasicValue) << '\n';
Tracer() << "sizeof(M) = " << sizeof(M) << '\n';
Tracer() << "sizeof(S1) = " << sizeof(S1) << '\n';
Tracer() << "sizeof(S2) = " << sizeof(S2) << '\n';
{
S1 s1;
s1.B[0] = ceda::xstring("hello");
s1.B[1] = 'x';
s1.B[2] = 10.3;
Tracer() << "s1 = " << s1 << '\n';
}
{
S2 s2;
s2.B[0] = ceda::xstring("hello");
s2.B[1] = 'x';
s2.B[2] = 10.3;
Tracer() << "s2 = " << s2 << '\n';
}
}
}
namespace Variant_Ptr
{
// Wraps a pointer, with the purpose of making it print the value in it
$struct+ P <<-ar os>>
{
P(ceda::ptr<ceda::IObject> p = ceda::null) : p(p) {}
bool operator==(const P& rhs) const { return p == rhs.p; }
bool operator!=(const P& rhs) const { return p != rhs.p; }
bool operator<(const P& rhs) const { return p < rhs.p; }
void Write(ceda::xostream& os) const
{
ceda::PrintInstance(os,p);
}
ceda::ptr<ceda::IObject> p;
};
/*
Note that a variant containing a ptr<> will not be serialisable to an Archive
*/
$variant+ Variant
{
default 0;
ceda::int32;
P;
};
/*
Since S contains a variant that can't be serialised, S can't be serialised
to an Archive either.
*/
$struct+ S isa ceda::IObject :
model
{
Variant V1;
Variant V2;
}
{
};
void Run()
{
ceda::TraceGroup g("Variant: ptr<T> example");
Tracer() << "sizeof(Variant) = " << sizeof(Variant) << '\n';
Tracer() << "sizeof(S) = " << sizeof(S) << '\n';
S s1;
s1.V1 = 10;
s1.V2 = 20;
S s2;
s2.V1 = 30;
s2.V2 = P(&s1);
Tracer() << "s2 = " << s2 << '\n';
}
}
/*
todo:
* cxOperation, cxPersistStore properly handle a variant with pref fields
irrespective of where they appear.
* Allow for polymorphic functions, even supporting multiple dispatch
* Look into open unions
* xcpp generates reflection information for unions and variants
* Python bindings work with variants
* Allow for deprecated fields
* Allow for nested variant to optionally share the one int32 tag field. This would only be
recommended on nested variants than will never change, or else won't be persisted in
a database that would otherwise need to support schema evolution.
* Allow for reflection of nested tuples
todo:
The plan is to support schema evolution in a similar fashion to enums:
* Types can be deprecated but not deleted. They are marked with '-'
* New types in the union can only be appended at the end
It is proposed that no space is reserved for the deprecated types, so in that sense it is
better to say they have been dropped. No cases need be generated in switch statements
for dropped types.
The important concept is to allow for reading a dropped type and map into to a supported
type. For example, we could imagine initially supporting string8, then dropping it in
favour of string16. In that case we need to convert a string8 to a string16.
E.g.
$variant Shape
{
-Point; // has been dropped
Rect;
Circle;
Polygon;
Hexagon;
default Circle(Point(0,0),1);
};
is a valid evolution of the previous Shape definition.
I've wondered what it means to to generalise a union type:
$variant Shape2
{
Shape;
Triangle;
Pentagon;
};
Note that Shape2 is a supertype of Shape (as well as each of the various
specialisations of shapes).
*/
///////////////////////////////////////////////////////////////////////////////////////////////////
namespace Variant
{
void Run()
{
Variant_Introduction::Run();
Variant_Default::Run();
Variant_WithVoid::Run();
Variant_Arrays::Run();
Variant_HeapAllocatedTypes::Run();
Variant_NamedTypes::Run();
Variant_LikeEnum::Run();
Variant_EmbeddedTuples::Run();
Variant_List::Run();
Variant_Tree_AllAttribs::Run();
Variant_Tree::Run();
Variant_In_Model::Run();
Variant_Ptr::Run();
}
}