lambdac++17exprtk

ExprTk custom lambda function and default arguments


There was a random function added to ExprTk table before:

exprtk::symbol_table<double> symbolTable;
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<double> dis(0.0, 1.0);

auto randomFunc = []() {
    return dis(gen);
};
symbolTable.add_function("random", randomFunc);

exprtk::expression<double> expression;
expression.register_symbol_table(symbolTable);

Now I modified it to take minimum and maximum args:

exprtk::symbol_table<double> symbolTable;
std::random_device rd;
std::mt19937 gen(rd());

auto randomFunc = [](double min = 0, double max = 1) {
    std::uniform_real_distribution<double> dis(min, max);
    return dis(gen);
};
symbolTable.add_function("random", randomFunc);

exprtk::expression<double> expression;
expression.register_symbol_table(symbolTable);

It works, except for keeping compatibility with the old argument-less random usage, which is desired. Despite having default args, ExprTk can't compile random or random(). I'm assuming the library just handles the number of arguments and doesn't check if there are any default values.

I think I would need to instead switch to ivararg_function to support this. Or is there a simplier way I'm missing?


Solution

  • Having default values specified for the function parameters does not change the fact that the function you are registering is a two input parameter function, requiring two parameters be passed when invoking it in an expression/program.

    In short what you're looking for is called: function overloading and this can be achieved by using an exprtk::igeneric_function.

    The two overloads you're interested in are:

    1. Zero parameters implying a min/max of 0 and 1
    2. Two scalar parameters where min is parameter 0 and max is parameter 1

    The following demonstrates how such a exprtk::igeneric_function can be implemented:

    template <typename T>
    struct unirandom_dist final : public exprtk::igeneric_function<T>
    {
       typedef exprtk::igeneric_function<T>           igenfunct_t;
       typedef typename igenfunct_t::generic_type     generic_t;
       typedef typename igenfunct_t::parameter_list_t parameter_list_t;
       typedef typename generic_t::scalar_view        scalar_t;
    
       unirandom_dist()
       : exprtk::igeneric_function<T>("Z|TT")
          /*
             Overloads:
             0. Z  - Zero parameters, range is [0,1)
             1. TT - min and max, range is [min,max)
          */
       {
          std::random_device device;
          std::array<unsigned int,std::mt19937::state_size> seed;
          std::generate_n(seed.data(),seed.size(),std::ref(device));
          std::seed_seq seq(std::begin(seed),std::end(seed));
          generator.seed(seq);
       }
    
       using igenfunct_t::operator();
    
       T operator() (const std::size_t& ps_index, parameter_list_t parameters) override
       {
          const T min = (ps_index == 0) ? T(0) : scalar_t(parameters[0])();
          const T max = (ps_index == 0) ? T(1) : scalar_t(parameters[1])();
    
          if (min <= max)
          {
             std::uniform_real_distribution<T> unidist(min,max);
             return unidist(generator);
          }
    
          return std::numeric_limits<T>::quiet_NaN();
       }
    
       std::mt19937 generator;
    };
    

    The following is how all of it can be brought together and used in an expression:

    template <typename T>
    void foo()
    {
       typedef exprtk::symbol_table<T> symbol_table_t;
       typedef exprtk::expression<T>   expression_t;
       typedef exprtk::parser<T>       parser_t;
    
       unirandom_dist<T> unirnd;
       exprtk::rtl::io::package<T> io_package;
    
       symbol_table_t symbol_table;
       symbol_table.add_function("random" , unirnd);
       symbol_table.add_package (io_package);
    
       expression_t expression;
       expression.register_symbol_table(symbol_table);
    
       parser_t parser;
    
       const std::string random_program =
          " println('random:        ', random       ); "
          " println('random():      ', random()     ); "
          " println('random(-1,+1): ', random(-1,+1)); ";
    
       parser.compile(random_program,expression);
    
       expression.value();
    }
    
    int main()
    {
       foo<double>();
       return 0;
    }
    

    Point of note is that now the function has a few overloads that can be invoked in the following ways:

    1. random
    2. random()
    3. random(min,max)

    As a side note the reason you wouldn't use ivararg_function is because it doesn't come with type checking and will require the implementation of unirandom_dist to determine in the critpath the number of params being passed and their nature etc.