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

288 views Asked by At

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!

3

There are 3 answers

7
sehe On BEST ANSWER

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; 
0
Sam Varshavchik On

Your fundamental issue is that C++ is a statically-typed language. General-purpose programming languages tend to fall into two rough categories: statically and dynamically typed. In a dynamically-typed language, like Perl, the type of an object is determined at runtime. In a statically-typed language, like C++, the type of an object is specified at compile-time.

That doesn't mean that this is not possible in C++ in a type-safe manner. It is, but it requires some work.

The usual approach is to encapsulate all types into classes that are derived from some base class that defines virtual methods, with the subclasses implementing them. Let's use just ints and floats.

// Forward declarations

class FloatNumber;
class IntNumber;

class Number {

    // virtual methods to be defined later.

};

class FloatNumber : public Number {

     float value;

    // Implements the virtual methods for float values.

};

class IntNumber : public Number {

     int value;

    // Implements the virtual methods for int values.

};

Now, you can implement basic operations. In the Number base class, you define the conversion methods:

virtual FloatNumber asFloat() const = 0;
virtual IntNumber asInt() const = 0;

In each subclass you will implement these in the obvious manner, returning either *this, if it's the same type, or constructing the other subclass and returning the newly-constructed class.

Now, you can implement basic operations. Say, equals:

virtual bool equals(const Number &other) const =0;

Now, you get to implement this virtual method in each subclass. In FloatNumber::equals(), for example, you would call other.asFloat(), and compare its val to its own val. Ditto for IntNumber::equals(). If both Numbers that are compared are of the same type, this ends up comparing the two values directly; otherwise an automatic type conversion takes place.

Now, this is not a perfect approach, since if the first number is an IntNumber, the FloatNumber ends up getting down-converted to an int, and you really want the conversion to go the other way. There are classical ways to solve this also, in a type-safe manner. But first, you should get this basic approach implemented first, and then worry about handling the various corner-cases.

In this manner you can proceed and build up a class hierarchy that implements generic operation on numbers. This is likely more work than you expected, but this is how to properly do this kind of thing in C++, in a completely type-safe manner. Modern C++ compilers are quite efficient, and the end result will be quite small, and compact.

The final step for you is to read the file, parse the values, and have a simple lookup table that maps "Equals" to the "equals" method, and so on...

0
Richard Hodges On

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