Variants are data types that can store different types of values in them, as opposed to one fixed type. In contrast to a generic object
or an untyped
variable, there is a limited number of storable types. I use these in my Leaf compiler in two key places: logging, and type information.
I’m still using boost::variant as opposed to std::variant. I’m waiting for widespread deployment of C++17 compilers before upgrading (such as with new long-term Ubuntu releases).
Logging
I recently added a new logging system to Leaf. The caller provides an error reference and contextual data when making a log entry. variant
is used to capture different types of contextual information. I previously used variant
this way in my low latency logging system.
logger.error( "cerr-unknown-identifier", logger::item_symbol( symbol.name ) );
cerr-unknown-identifier
is an error message identifier. It’s a key to an error message stored in a YAML file, this particular error is:
cerr-unknown-identifier: text: Unknown identifier `{symbol}`
Note the placeholder {symbol}
there. Somehow the logger needs to replace that with a symbol. That’s what the logger::item_symbol( symbol.name )
argument provides.
But what about other types? For example, an argument mismatch call involves more information:
mismatch-argcount: text: The function `{symbol}` expects {expect} argument(s), you provided {actual}.
The caller passes more arguments to error
than before.
logger.error( 'mismatch-argcount', logger::item_symbol( symbol.name ), logger::item_expect( func_arg_count ), logger::item_actual( call_args.size() ) );
Different items carry different types of information. The function calls mask item construction, but each of these item_
functions returns a logger::item
.
typedef boost::variant< std::string, int64_t, source_location> item_variant_t; struct item { item_type_t type; item_variant_t value; };
The type
carries the semantic meaning, such as i_symbol
or i_actual
. The value
contains the data associated with this item. It’s a variant
type that allows either a string
, an int64_t
, or a source_location
.
The
error
function has a few variations accepting lists of items and individual items. A macro is most often used to make these calls as it adds items for line and source information.
Type Traits
Leaf’s type system has two parallel systems: concrete types and type specifiers. Type specifiers allow incomplete types and type constraints. We see the specifiers in leaf source code.
var pine : optional integer 32bit var cone : optional = 25 var twig : float high = 1/2
optional integer 32bit
contains three parts, stored in a type_spec::part
structure. The type_spec
doesn’t know much about what these parts mean, only how to store them. It has a std::vector<part>
list of parts.
As the value type of each part varies significantly, I use a variant to store all the possibilities.
struct part { part_type_t type; boost::variant< bool, extr_type, extr_type::reference_t, int, intr_type_compat::type_t, intr_type::fun_class_t, intr_type_function::access_t, intr_type_tuple::pack_t, shared_ptr<intr_type const>, //MUST not be an instance! std::string, type_spec_symbol, intr_type_function::convention_t > value; //sub-parametrics std::vector<shared_ptr<type_spec const>> sub; //attached expression shared_ptr<node const> node_expr; shared_ptr<expression const> expr; };
Because working with a variant
can be burdensome, and also wanting stronger typing, users of type_spec
don’t use the variant directly. All access goes through template functions.
// creating the `optional integer 32bit` specifier type_spec ts. ts.set<pt_data_bitsize>( 32 ); ts.set<pt_optional>( true ); ts.set<pt_fun_class>( intr_type::integer );
The set
call prevents associating the wrong type of information with the part. It has this signature:
template<part_type_t PT> part & set( typename part_type_descriptor<PT>::type value )
I’m mapping an enum value to type information with specialized templates. The setup is hidden behind macros, but here’s the pt_optional
one for example:
template<> struct part_type_descriptor<pt_optional> { typedef bool type; };
There’s a matching if_get
function, which returns the value of a particular part if it exists. This approach adds strong-typing and hides the complexity of working with variant
inside the type_spec
class.
template<part_type_t PT> boost::optional<typename part_type_descriptor<PT>::type> if_get() const {
I like C++’s ability to map an enum value into a concrete type. It, along with variant, is the key to making
type_spec
type-safe while holding variable types of information. This template flexibility is one of the things that attracts me to C++.
What about you?
Let me know how you use variant
in your code? Even if it’s not C++, the concept exists in other languages. Alternatively, tell me about how you’d like to use variant
but have only a generic object type available.