How I use `variant` in the Leaf compiler

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).


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( ) );

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:

    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( ) argument provides.

But what about other types? For example, an argument mismatch call involves more information:

    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( ), 
    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<
        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;
        shared_ptr<intr_type const>, //MUST not be an instance!
    > value;
    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.

2 replies »

  1. Thank you for the post. It is always good to see the `variant` described in real-life usage (as opposed to toy examples). In response to your request, I also used variant in real code, and described it in this post:

    One thing that I found surprising was when I conducted a benchmark, it turned out that using `variant` (whether boost or std version) was slower than the alternative design using virtual functions. Somehow indirection via pointer is faster than the manual switch on the type.

    • Whether I use virtual or not depends on what the semantics involved are. In both of the cases in this article I think `variant` is the better choice — they aren’t exposing different behaviour, just different types.

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s