Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the C++ way of handling a framework to call functions of different types from a script?

I have tried different approaches and asked several specific questions regarding sub-problems of my requirement here. But my solution doesn't really work as expected, so I am taking a step back and ask here from a more general perspective. Please bear in mind that I am not a C++ pro. Not a beginner either, but I am still learning the language.

So, I have the following requirement. I need to read-in text files, which contain conditions, like "Greater" or "Equals", all like functions that return boolean values. The text files also include parameters for these conditions. Note that these parameters can be of different types (integer, decimal, etc.), and each such condition can take different number of parameters (e.g. "Equals" takes 2 paramters, while "Between" would take 3 parameters). Thus the file could look something like this:

Greater, 2, 3
Greater, 2.4, 1.0
Equals, true, true
Between, 20, 10, 30

The logic to read-in that file and parsing it is already done. Now I need to "concatenate" all these boolean functions with their parameters and check if all of them are true.

So I thought I would create functions or a class with static methods to represent these boolean test functions, then create a map of function pointers to these functions, mapped by their name. At runtime I would then read in the file, call the respective function pointer and pass-in the paramteres. That seemed easy to me, but in reality I am struggling mostly with the fact that these boolean functions can take a different number of parameters, and that these can be of different types.

Can you recommend a way to tackle that requirement in C++? I am not asking for a complete solution, but for a suitable C++ approach, or a guideline I could follow. Thanks in advance!

like image 250
Matthias Avatar asked Dec 16 '16 15:12

Matthias


2 Answers

Here's a quick&dirty Spirit grammar for the input shown.

UPDATE

Now added invocation and implementation of the predicate functions (GreaterImpl and EqualsImpl).

I tried to be smart allowing comparisons between mixed arithmetic types (but not e.g. Greater(bool,string). If you compare incompatible types you will get a std::runtime_error exception that provides type feedback to the caller.

Live On Coliru

#include <deque>
#include <boost/spirit/include/qi.hpp>
#include <boost/fusion/adapted/struct.hpp>

namespace qi = boost::spirit::qi;

namespace Ast {
    using Value       = boost::variant<int, double, bool, std::string>;

    using BinaryPred  = std::function<bool(Value, Value)>;
    using TernaryPred = std::function<bool(Value, Value, Value)>;
    using Pred        = boost::variant<BinaryPred, TernaryPred>;
    using Values      = std::vector<Value>;

    struct Invocation { Pred pred; Values args; };
    using Invocations = std::vector<Invocation>;
}

BOOST_FUSION_ADAPT_STRUCT(Ast::Invocation, pred, args)

namespace Predicates {
    using Ast::Value;

    struct Greater : boost::static_visitor<bool> {
        bool operator()(Value const& a, Value const& b) const { 
            return boost::apply_visitor(*this, a, b);
        }

        template <typename T> bool operator()(T const& a, T const& b) const { return std::greater<T>{}(a, b); }

        template <typename T, typename U>
            typename std::enable_if<std::is_arithmetic<T>() && std::is_arithmetic<U>(), bool>::type 
            operator()(T const& a, U const& b) const { return a > b; }

        template <typename T, typename U>
            typename std::enable_if<not (std::is_arithmetic<T>() && std::is_arithmetic<U>()), bool>::type 
            operator()(T const&, U const&) const { throw std::runtime_error("Type Mismatch"); }
    };

    struct Equals : boost::static_visitor<bool> {
        bool operator()(Value const& a, Value const& b) const { 
            return boost::apply_visitor(*this, a, b);
        }

        template <typename T> bool operator()(T const& a, T const& b) const { return std::equal_to<T>{}(a, b); }

        template <typename T, typename U, typename enable = typename std::enable_if<std::is_arithmetic<T>() && std::is_arithmetic<U>()>::type >
            bool operator()(T const& a, U const& b) const { return a == b; }

        template <typename T, typename U>
            typename std::enable_if<not (std::is_arithmetic<T>() && std::is_arithmetic<U>()), bool>::type 
            operator()(T const&, U const&) const { throw std::runtime_error("Type Mismatch"); }
    };

    struct Between {
        bool operator()(Value const& v, Value const& lower, Value const& upper) const {
            return Greater{}(v,lower) && Greater{}(upper,v); 
        }
    };

}

static inline bool evaluate(Ast::Invocation const& i) {
    struct Invoker {
        using result_type = bool;
        Ast::Values const& args;

        result_type operator()(Ast::BinaryPred const& p) const {
            if (args.size() != 2) throw std::runtime_error("Arity Mismatch");
            return p(args.at(0), args.at(1));
        }
        result_type operator()(Ast::TernaryPred const& p) const {
            if (args.size() != 3) throw std::runtime_error("Arity Mismatch");
            return p(args.at(0), args.at(1), args.at(2));
        }
    };

    return boost::apply_visitor(Invoker{i.args}, i.pred);
}

template <typename It>
struct Grammar : qi::grammar<It, Ast::Invocations()> {

    Grammar() : Grammar::base_type(start) {
        using namespace qi;

        start      = skip(blank) [ invocation % eol ];
        invocation = pred >> -("," >> args);
        args       = arg % ",";
        arg        = my_double_ | qi::int_ | qi::bool_ | lexeme['"' > *~char_('"') > '"'];
    }

  private:
    struct pred_t : qi::symbols<char, Ast::Pred> {
        pred_t() {
            this->add
                ("Greater", Predicates::Greater{})
                ("Equals",  Predicates::Equals{})
                ("Between", Predicates::Between{})
                ;
        }
    } const pred;
    qi::rule<It, Ast::Invocations()> start;
    qi::rule<It, Ast::Invocation(), qi::blank_type> invocation;
    qi::rule<It, Ast::Values(), qi::blank_type> args;
    qi::rule<It, Ast::Value(),  qi::blank_type> arg;
    qi::real_parser<double, qi::strict_real_policies<double> > my_double_;
};

#include <sstream>

int main() {
    using It = boost::spirit::istream_iterator;

    std::deque<std::string> testcases {
        // one multiline case:
        "Between, 20, 10, 30\n"
        "Between, NaN, NaN, NaN\n"
        "Between, \"q\", \"a\", \"z\""
    };
    
    // many single line cases for easy test reporting
    for (std::string op : {"Greater","Equals"})
    for (auto rhs : { "42", "0.0", "true", "\"hello\"" }) 
    for (auto lhs : { "41", "-0.0", "false", "\"bye\"" }) {
        testcases.push_front(op + ", " + lhs + ", " + rhs);
    }

    for (auto testcase : testcases) {
        std::cout << "--- Testcase '" << testcase << "' -> ";

        std::istringstream iss(testcase);
        It f(iss >> std::noskipws), l; 
        Ast::Invocations parsed;

        if (qi::parse(f, l, Grammar<It>(), parsed)) {
            for (auto& invocation : parsed) {
                try {
                    std::cout << std::boolalpha << evaluate(invocation) << "; ";
                } catch(std::exception const& e) {
                    std::cout << e.what() << "; ";
                }
            }
            std::cout << "\n";
        } else {
            std::cout << "Parse failed\n";
        }

        if (f != l)
            std::cout << "Remaining unparsed input: '" << std::string(f,l) << "'\n";
    }
}

Prints:

--- Testcase 'Equals, "bye", "hello"' -> false; 
--- Testcase 'Equals, false, "hello"' -> Type Mismatch; 
--- Testcase 'Equals, -0.0, "hello"' -> Type Mismatch; 
--- Testcase 'Equals, 41, "hello"' -> Type Mismatch; 
--- Testcase 'Equals, "bye", true' -> Type Mismatch; 
--- Testcase 'Equals, false, true' -> false; 
--- Testcase 'Equals, -0.0, true' -> false; 
--- Testcase 'Equals, 41, true' -> false; 
--- Testcase 'Equals, "bye", 0.0' -> Type Mismatch; 
--- Testcase 'Equals, false, 0.0' -> true; 
--- Testcase 'Equals, -0.0, 0.0' -> true; 
--- Testcase 'Equals, 41, 0.0' -> false; 
--- Testcase 'Equals, "bye", 42' -> Type Mismatch; 
--- Testcase 'Equals, false, 42' -> false; 
--- Testcase 'Equals, -0.0, 42' -> false; 
--- Testcase 'Equals, 41, 42' -> false; 
--- Testcase 'Greater, "bye", "hello"' -> false; 
--- Testcase 'Greater, false, "hello"' -> Type Mismatch; 
--- Testcase 'Greater, -0.0, "hello"' -> Type Mismatch; 
--- Testcase 'Greater, 41, "hello"' -> Type Mismatch; 
--- Testcase 'Greater, "bye", true' -> Type Mismatch; 
--- Testcase 'Greater, false, true' -> false; 
--- Testcase 'Greater, -0.0, true' -> false; 
--- Testcase 'Greater, 41, true' -> true; 
--- Testcase 'Greater, "bye", 0.0' -> Type Mismatch; 
--- Testcase 'Greater, false, 0.0' -> false; 
--- Testcase 'Greater, -0.0, 0.0' -> false; 
--- Testcase 'Greater, 41, 0.0' -> true; 
--- Testcase 'Greater, "bye", 42' -> Type Mismatch; 
--- Testcase 'Greater, false, 42' -> false; 
--- Testcase 'Greater, -0.0, 42' -> false; 
--- Testcase 'Greater, 41, 42' -> false; 
--- Testcase 'Between, 20, 10, 30
Between, NaN, NaN, NaN
Between, "q", "a", "z"' -> true; false; true; 
like image 153
sehe Avatar answered Nov 15 '22 05:11

sehe


boost variant is the easiest way IMHO:

#include <boost/variant.hpp>
#include <boost/operators.hpp>
#include <string>
#include <iostream>
#include <iomanip>

// define the concept of equality in my scripting language
struct is_equal : boost::static_visitor<bool>
{
    // x == x is easy
    template<class T>
    bool operator()(const T& l, const T& r) const {
        return l == r;
    }

    // define the concept of comparing strings to integers
    bool operator()(const std::string& l, const int& r) const {
        return l == std::to_string(r);
    }

    // and integers to strings
    bool operator()(const int& l, const std::string& r) const {
        return (*this)(r, l);
    }
};

struct is_less : boost::static_visitor<bool>
{
    // x == x is easy
    template<class T>
    bool operator()(const T& l, const T& r) const {
        return l < r;
    }

    // define the concept of comparing strings to integers
    bool operator()(const std::string& l, const int& r) const {
        return std::stoi(l) < r;
    }

    // and integers to strings
    bool operator()(const int& l, const std::string& r) const {
        return l < std::stoi(r);
    }
};


struct emit : boost::static_visitor<std::ostream&>
{
    emit(std::ostream& os) : os_(os) {}

    // x == x is easy
    template<class T>
    std::ostream& operator()(const T& l) const {
        return os_ << l;
    }

    std::ostream& operator()(const std::string& s) const {
        return os_ << std::quoted(s);
    }

    std::ostream& os_;
};

struct scriptable_value
: boost::less_than_comparable<scriptable_value>
, boost::equality_comparable<scriptable_value>
{
    using variant_type = boost::variant<std::string, int>;

    scriptable_value(std::string v) : variant_(std::move(v)) {}
    scriptable_value(int v) : variant_(v) {}

    variant_type const& as_variant() const {
        return variant_;
    }

private:
    variant_type variant_;
};

bool operator==(scriptable_value const& l, scriptable_value const& r)
{
    return boost::apply_visitor(is_equal(), l.as_variant(), r.as_variant());
}

bool operator<(scriptable_value const& l, scriptable_value const& r)
{
    return boost::apply_visitor(is_less(), l.as_variant(), r.as_variant());
}

std::ostream& operator<<(std::ostream& os, scriptable_value const& r)
{
    return boost::apply_visitor(emit(os), r.as_variant());
}


int main()
{
    auto x = scriptable_value(10);
    auto y = scriptable_value("10");
    auto x2 = scriptable_value(9);
    auto y2 = scriptable_value("9");

    std::cout << x << " == " << y << " : " << std::boolalpha << (x == y) << std::endl;
    std::cout << x << " != " << y << " : " << std::boolalpha << (x != y) << std::endl;
    std::cout << x << " == " << y2 << " : " << std::boolalpha << (x == y2) << std::endl;
    std::cout << x << " != " << y2 << " : " << std::boolalpha << (x != y2) << std::endl;

    std::cout << x << " <  " << y << " : " << std::boolalpha << (x < y) << std::endl;
    std::cout << x << " >= " << y << " : " << std::boolalpha << (x >= y) << std::endl;
    std::cout << x << " <  " << y2 << " : " << std::boolalpha << (x < y2) << std::endl;
    std::cout << x << " >= " << y2 << " : " << std::boolalpha << (x >= y2) << std::endl;

    std::cout << x << " == " << x2 << " : " << std::boolalpha << (x == x2) << std::endl;
    std::cout << x << " != " << x2 << " : " << std::boolalpha << (x != x2) << std::endl;

}

expected output:

10 == "10" : true
10 != "10" : false
10 == "9" : false
10 != "9" : true
10 <  "10" : false
10 >= "10" : true
10 <  "9" : false
10 >= "9" : true
10 == 9 : false
10 != 9 : true
like image 39
Richard Hodges Avatar answered Nov 15 '22 06:11

Richard Hodges