处理框架以从脚本调用不同类型函数的 C++ 方法是什么?

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

我尝试了不同的方法,并询问了几个关于我的要求的子问题的具体问题。但是我的解决方案并没有真正按预期工作,所以我退后一步,从更一般的角度在这里提问。请记住,我不是 C++ 专业人士。也不是初学者,但我还在学习这门语言。

所以,我有以下需求。我需要读入文本文件,其中包含 "Greater" 或 "Equals" 等条件,所有这些都类似于 return 布尔值的函数。文本文件还包括这些条件的参数。请注意,这些参数可以是不同类型(整数、小数等),并且每个这样的条件可以采用不同数量的参数(例如 "Equals" 需要 2 个参数,而 "Between" 需要 3 个参数) .因此该文件可能看起来像这样:

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

读入该文件并对其进行解析的逻辑已经完成。现在我需要 "concatenate" 所有这些布尔函数及其参数,并检查它们是否全部为真。

所以我想我会用静态方法创建函数或 class 来表示这些布尔测试函数,然后创建指向这些函数的函数指针映射,并按它们的名称进行映射。在运行时,我会读入文件,调用相应的函数指针并传入参数。这对我来说似乎很容易,但实际上我最纠结的是这些布尔函数可以采用不同数量的参数,并且这些参数可以是不同的类型。

您能推荐一种在 C++ 中解决该要求的方法吗?我不是要一个完整的解决方案,而是要一个合适的 C++ 方法,或者我可以遵循的指南。提前致谢!

你的根本问题是 C++ 是一种静态类型的语言。通用编程语言往往分为两大类:静态类型和动态类型。在动态类型语言(如 Perl)中,对象的类型是在运行时确定的。在静态类型语言中,如 C++,对象的类型在编译时指定。

这并不意味着这在 C++ 中无法以类型安全的方式实现。是的,但它需要一些工作。

通常的方法是将所有类型封装到 classes 中,这些 classes 派生自一些定义虚拟方法的基 class,子 classes 实现它们。让我们只使用 ints 和 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.

};

现在,您可以执行基本操作了。在 Number 基础 class 中,您定义转换方法:

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

在每个子class中,你将以明显的方式实现这些,返回*this,如果它是相同的类型,或者构造另一个子class并返回新的-构造class.

现在,您可以执行基本操作了。比如说,等于:

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

现在,您可以在每个子class 中实现这个虚拟方法。例如,在 FloatNumber::equals() 中,您将调用 other.asFloat(),并将其 val 与其自身的 val 进行比较。 IntNumber::equals() 同上。如果比较的两个 Number 都是同一类型,则直接比较两个值;否则会发生自动类型转换。

现在,这不是一个完美的方法,因为如果第一个数字是 IntNumberFloatNumber 最终会向下转换为 int,而你真的希望转换以另一种方式进行。也有 class 以类型安全的方式解决这个问题的合理方法。但首先,您应该先实现这个基本方法,然后再考虑处理各种极端情况。

通过这种方式,您可以继续构建一个 class 层次结构,实现对数字的通用操作。这可能比您预期的要多,但这是在 C++ 中以完全类型安全的方式正确执行此类操作的方法。现代 C++ 编译器非常高效,最终结果将非常小且紧凑。

您的最后一步是读取文件、解析值并进行简单查找 table 将 "Equals" 映射到 "equals" 方法,等等。 ..

boost 变体是恕我直言的最简单方法:

#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;

}

预期输出:

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

这里是显示的输入的快速和粗略的 Spirit 语法。

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";
    }
}

打印:

--- 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;