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 instub_input_output
we get a significant increase to 2500 bytes. - The
readdigit_errno
implementation which uses a global variableerrno
needs 2540 bytes - The
readdigit_optional_variant
implementation which uses theoptional_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 |