Variants and optionals in microcontrollers

The article about futures we have an interesting construct: a structure which may or may not hold a value.

When dealing with errors we have a similar situation. A function may or may not return a value or it may return an error value. We can make a structure optional_variant which can hold all of these values.

This results in three states for optional_variant:

  • nothing
  • value
  • error

for an object which holds the corresponding data and two different types of data - a variant.

A possible usage is a function which returns an option

// Read a character.
optional_variant<char, int> get_char()
{
    // Read the character which might fail.
    char c = ... read character here ...
    int errorvalue = ... read errorvalue here ...

    if( errorvalue == 0 ) {

        // Ok, we have a character.
        return make_value<char, int>( c );

    } else if( errorvalue == 1 ) {

        // We have no character.
        return make_nothing<char, int>();

    } else {

        // An error occured.
        return make_error<char, int>( errorvalue );

    }
}

and code which evaluates the optional/variant

optional_variant<char,int> co = get_char();

switch( co.what() )
{
    case co.e_value:
        Serial.println( "Character: " );
        Serial.println( co.get_value() );
    break;

    case co.e_error:
        Serial.print( "Error: " );
        Serial.println( co.get_error(),  );
    break;

    case co.e_nothing:
        Serial.println( "Nothing." );
    break;
}

.

Optionals and variants

In [1], there is boost::optional (it will likely be included in C++17) which maybe holds an value.

The class boost::variant is kind of an union of a tuple which holds one value of a list of different types. [2]

What we will implement here to hold nothing, errors or values is a small version of a mixture of both.

In pseudocode it would look like this

template< typename... types_t >
class variant
{
    // Number of which type is currently stored.
    uint8_t _what;

    // Data area where we store the data.
    uint8_t* _storage;

public:

    // Return the type.
    uint8_t what();

    // Get the value.
    template<int n>
    get_n<n, types_t...>::type get_value();

    // Initialize empty.
    variant();

    // Initialize with value.
    template<int n>
    variant( get_n<n, types_t...>::type value );
};

we define our states as

// Alias for the first three values.
enum {
    e_nothing = 0;
    e_value = 1;
    e_error = 2;
};

Our three state variant would consist of the types void for nothing, char for the value, int for the error.

The usage in the make_...-functions would be

variant<void,char,int>::variant<0> v_nothing();
variant<void,char,int>::variant<1> v_value( the_value );
variant<void,char,int>::variant<2> v_error( the_error );

except in current C++ it is not possible to call the constructor directly with an template argument. We'll ignore that fact for now and use a workaround [3].

In order to implement a new class optional_variant we have the following options

  • reimplement variant,
  • inherit from variant,
  • use a variant instance as a member.

The third option seems the smallest and most flexible on a microcontroller as it avoids inheritance and code duplication.

The optional_variant is defined as

template< class value_t, typename... rest_t >
class optional_variant
{
    variant< void, value_t, rest_t... > _v;

public:
    // Return the type.
    uint8_t what() {
        return _v.what();
    }

    // Return the value.
    value_t get_value() {
        return _v.get_value<1>();
    }

    // Return the error.
    get_n<2, types_t...>::type get_value() {
        return _v.get_value<2>();
    }


    // Get the value.
    template<int n>
    get_n<n, types_t...>::type get_value();

    // Initialize empty.
    optional_variant()
        : _v()
    {}

    // Initialize with value.
    optional_variant( value_t value )
        : _v<n>( value )
    {}

    // Initialize with status and value.
    template<int n>
    optional_variant( get_n<n, types_t...>::type value )
        : _v<n>( value )
    {}
};

. The template parameter value_t is always necessary while the parameter pack rest_t... can be empty. This means the error parameter is optional.

The member variant _v has void added as first template parameter.

The optional_variant can be used as

optional_variant<char> just_optional();

without error value and with error value as

optional_variant<char, int> error_optional();

.

One difference from most implementations of variant is that in our case a variant is identified by the number instead of the type.

Usually the data could be retrieved by a method like

template<typename... types_t>
class type_variant {
...
    template< class T >
    T get_value_as();
...
};

which specializes the by the data type.

This is not a suitable implementation for us since the value type and the error type could be identical.

Due to the definition by number

optional_variant<int, int> error_optional();

is also valid and a call to error_optional.what() gives the corresponding number which can be used to distinguish between the first int (value) and the second :code`int` (error).

Note:

While the what()-method returns the type number at runtime, a call to get_value() in optional_variant or get_value<n>() in the variant return a specific type which must known at compile time. Therefore it is not possible to pass a run-time-value as template parameter to get_value<n>() for the different cases. Instead, a switch/case construct is used in the example above to resolve all possible types/states.

Library and Examples

While the above pseudo-code is useful to illustrate, it won't compile due to some subtleties of the C++ language.

The library i wrote, therefore uses a slightly different implementation.

You can download the library here.

Code size

One interesting topic is how well the optional_variant fares compared to alternative implementations of error handling. The example is a function which reads a digit and returns nothing if no character is available, the digit (0-9) as an int or an error.

  • The basline forms the empty_sketch with a size of 450 bytes.
  • When including the Serial in- and output functions in stub_input_output we get a significant increase to 2500 bytes.
  • The readdigit_errno implementation which uses a global variable errno needs 2540 bytes
  • The readdigit_optional_variant implementation which uses the optional_variant uses 2564 bytes.
  • The implementation with exceptions doesn't work since exception support on avr-gcc is not complete [4]. It would likely need much more space which is a reason why exceptions are disabled in the first place.

The extra code size therefore is 40 bytes when doing C-style error handling versus 64 bytes by using the C++ approach.

Chapter conclusion

Optionals and variants are a versatile data structure for error handling and can be implemente efficiently in microcontrollers.

[1]https://boost.org
[2]The C union does only work well with POD-types. For more complex data types one enters a muddy area of pointer casting riddled with compiler-defined (or undefined) behavior.
[3]In order to pass a template parameter to a constructor, this must be done via function arguments. A helper class template<int n> class int_helper { static const int value = n; }; can be used for a constructor template<int n> classname( int_helper<n> helper, ... ). This then can be called as classname varname( int_helper<1>(), ... ); passing the template parameter as function parameter.
[4]http://www.nongnu.org/avr-libc/user-manual/FAQ.html#faq_cplusplus