I have written a variant class, which will be used as the main type in a dynamic language, that will ultimately allow 256 different types of value (header is an unsigned byte, only 20 are actually used). I now want to implement casting/converting between types.
My initial thought was a lookup table, but the shear amount of memory that would need makes it impractical to implement.
What are the alternatives? Right now I am considering a further three methods from research and suggestions from other people:
- Group the types into larger subsets, such as numeric or collection or other.
- Make a conversion interface that has CanCast(from, to) and Cast(Variant) methods and allow classes that implement that interface to be added to a list, that can then be checked to see if any of the conversion classes can do the cast.
- Similar to (1) but make several master types, and casting is a two step process from the original type to the master type and then again to the final type.
What would be the best system?
Edit: I have added the bounty as I am still unsure on the best system, the current answer is very good, and definitely got my +1 but there must be people out there who have done this and can say what the best method is.
My system is very “heavy” (lots of code), but very fast, and very feature rich (cross-platform C++). I’m not sure how far you would want to go with your design, but here’s the biggest parts of what I did:
DatumState– Class holding an “enum” for “type”, plus native value, which is a “union” among all the primitive types, includingvoid*. This class is uncoupled from all types, and can be used for any native/primitive types, and “reference to”void*type. Since the “enum” also has “VALUE_OF” and “REF_TO” context, this class can present as “wholly containing” afloat(or some primitive type), or “referencing-but-not-owning” afloat(or some primitive type). (I actually have “VALUE_OF“, “REF_TO“, and “PTR_TO” contexts so I can logically store a value, a reference-that-cannot-be-null, or a pointer-that-may-be-null-or-not, and which I know I need to delete-or-not.)Datum– Class wholly containing aDatumState, but which expands its interface to accommodate various “well-known” types (likeMyDate,MyColor,MyFileName, etc.) These well-known types are actually stored in thevoid*inside theDatumStatemember. However, because the “enum” portion of theDatumStatehas the “VALUE_OF” and “REF_TO” context, it can represent a “pointer-to-MyDate” or “value-of-MyDate“.DatumStateHandle– A helper template class parameterized with a (well-known) type (likeMyDate,MyColor,MyFileName, etc.) This is the accessor used byDatumto extract state from the well-known type. The default implementation works for most classes, but any class with specific semantics for access merely overrides its specific template parameterization/implementation for one or more member functions in this template class.Macros, helper functions, and some other supporting stuff– To simplify “adding” of well-known types to myDatum/Variant, I found it convenient to centralize logic into a few macros, provide some support functions like operator overloading, and establish some other conventions in my code.As a “side-effect” of this implementation, I got tons of benefits, including reference and value semantics, options for “null” on all types, and support for heterogeneous containers for all types.
For example, you can create a set of integers and index them:
Similarly, some data types are indexed by strings, or by enums:
I can store sets-of-items (natively), or sets-of-
Datums (wrapping each item). For either case, I can “unwrap” recursively:One might argue my “
REF_TO” and “VALUE_OF” semantics are overkill, but they were essential for the “set-unwrapping”.I’ve done this “
Variant” thing with nine different designs, and my current is the “heaviest” (most code), but the one I like the best (almost the fastest with a pretty small object footprint), and I’ve deprecated the other eight designs for my use.The “downsides” to my design are:
static_cast<>()from avoid*(type-safe and fairly fast, but
indirection is required; but,
side-effect is that design supports
storage of “
null“.)well-known types that are exposed
through the
Datuminterface (but youcan use
DatumStateif you do notwant well-known type APIs).
No matter your design, I’d recommend the following:
Use an “
enum” or something to tellyou the “type“, separate from the
“value“. (I know you can compress
them into one “
int” or somethingwith bit packing, but that is
slow-for-access and very tricky to
maintain as new types are
introduced.)
Lean on templates or something to centralize operations, with a
mechanism for type-specific
(override) processing (assuming you want to
handle non-trivial types).
The name-of-the-game is “simplified maintenance when adding new types” (or at least, it was for me). Like a good Term Paper, it is a very good idea if you rewrite, rewrite, rewrite, to hold-or-increase your functionality as you constantly remove the code required to maintain the system (e.g., minimize the effort required to adapt new types to your existing
Variantinfrastructure).Good luck!