13  Embedded Programming

The following section discusses topics related to using TinyExpr++ in an embedded environment.

Performance

TinyExpr++ is fairly fast compared to compiled C when the expression is short or does hard calculations (e.g., exponentiation). TinyExpr++ is slower compared to C when the expression is long and involves only basic arithmetic.

Here are some example benchmarks:

Expression TinyExpr++ Native C Comparison
sqrt(a1.5+a2.5) 1,707 ns 58.25 ns 29% slower
a+5 535 ns 0.67 ns 798% slower
(1/(a+1)+2/(a+2)+3/(a+3)) 3,388 ns 3.941 ns 859% slower

Note that TinyExpr++ is slower compared to TinyExpr because of additional type safety checks (e.g., the use of std::variant instead of unions), case insensitivity, and bookkeeping operations.

Refer to compile-time options for flags that can provide optimization.

Volatility

If needing to use a te_parser object as volatile (e.g., accessing it in a system interrupt), then you will need to do the following.

First, declare your te_parser as a non-volatile object outside of the interrupt function (e.g., globally):

te_parser tep;

Then, in your interrupt function, create a volatile reference to it:

void OnInterrupt()
    {
    volatile te_parser& vTep = tep;
    }

Functions in te_parser which have volatile overloads can then be called directly:

void OnInterrupt()
    {
    volatile te_parser& vTep = tep;
    vTep.set_list_separator(',');
    vTep.set_decimal_separator('.');
    }

The following functions in the te_parser class have volatile overloads:

  • get_result()
  • success()
  • get_last_error_position()
  • get_decimal_separator()
  • set_decimal_separator()
  • get_list_separator()
  • set_list_separator()

For any other functions, use const_cast<> to remove the parser reference’s volatility:

void OnInterrupt()
    {
    volatile te_parser& vTep = tep;

    // Use 'const_cast<te_parser&>(vTep)' to access
    // non-volatile functions.
    const_cast<te_parser&>(vTep).set_variables_and_functions(
         { {"STRESS_L", 10.1},
           {"P_LEVEL", .5} });
    const_cast<te_parser&>(vTep).compile(("STRESS_L*P_LEVEL"));

    if (vTep.success())
        {
        auto res = vTep.get_result();
        // Do something else...
        }
    }

Note that it is required to make the initial declaration of your te_parser non-volatile; otherwise, the const_cast<> to the volatile reference will cause undefined behavior.

Floating-point Numbers

double is the default data type used for the parser’s variable types, parameters, and return types. For embedded environments that require float, compile with TE_FLOAT defined to use float instead.

When using this option, it is recommended to use the helper typedef te_type. This will map to either float or double (depending on whether TE_FLOAT is defined). By defining your functions and variables with te_type, you won’t need to replace double and float if needing to change this compiler flag.

For example, a custom function would be written as such:

// Regular function.
te_type my_sum(te_type a, te_type b)
    {
    return a + b;
    }

te_parser tep;
tep.set_variables_and_functions(
    {
    { "mysum", my_sum } // function pointer
    });

// Lambda.
tep.set_variables_and_functions({
    { "mysum2",
        [](te_type a, te_type b) noexcept
            { return a + b; } }
    });

Exception Handling

TinyExpr++ requires exception handling, although it does attempt to minimize the use of exceptions (e.g., noexcept is used extensively). Syntax errors will be reported without the use of exceptions; issues such as division by zero or arithmetic overflows, however, will internally use exceptions. The parser will trap these exceptions and return NaN (not a number) as the result.

Exceptions can also be thrown when defining custom functions or variables which do not follow the proper naming convention. (Function and variable names may only contain the characters a-z, A-Z, 0-9, ., and _, and must begin with a letter.)

Finally, specifying an illegal character for a list or decimal separator will also throw.

The following functions in te_parser can throw and should be wrapped in try/catch blocks:

  • compile()
  • evaluate()
  • set_variables_and_functions()
  • add_variable_or_function()
  • set_decimal_separator()
  • set_list_separator()

The caught std::runtime_error exception will provide a description of the error in its what() method.

Virtual Functions

TinyExpr++ does not use virtual functions or derived classes, unless you create a custom class derived from te_expr yourself (refer to “Binding to Custom Classes” for an example). (te_expr defines a virtual destructor that may be implicitly optimized to final if no derived classes are defined.)