使用用户定义的过滤器/运行时表达式评估过滤对象

时间:2015-09-04 01:14:17

标签: c++ parsing c++11

我想实现一个根据用户定义的标准过滤对象的系统(如下所述),老实说不知道从哪里开始。如果有现有的库,那就太好了。如果没有,那么正确方向的指针也会很好。

我有很多对象,我们称之为Cars,它们具有属性,如make,model等。我希望能够让用户为过滤器提供一个字符串,比如“car” .make ==“honda”&& car.year ==“2012”“等..

然后,在我的应用程序运行时,我希望能够运行这样的检查:if(filter(carobj) == true){ ...。请注意,我正在寻找的不同于列表理解,因为我不想过滤列表,但想要查看对象是否符合一组条件。

我认识到这有两个可能的组件,一个是解析用户的输入,另一个是构造这样的对象。我有一种感觉,那里有一些相当不错的表达式树解析器可以为前者做好工作,但对于后者我完全迷失了。

过滤器需要 fast ,因为它将在数百万个对象上运行,我也不能拥有增强依赖性。

3 个答案:

答案 0 :(得分:2)

下面 - 了解如何解决问题 - 非常轻微测试,毫无疑问隐藏了一些错误。如上所述,它仅处理std::stringint字段。它将过滤分为表达式到标记向量的步骤,然后重复使用标记来测试传递给operator()的记录。所以 - 没有高度优化,但也不应该非常缓慢。有一些有意的简化,例如您可以比较"field == 'abc'"但不能比较"'abc' == field"。还有很多可以用来验证表达式,提供有关表达式中解析或评估失败等位置的更多信息。我已经保留了调试信息,好像有人选择了它们,他们可能会想要它,既了解它是如何工作的,又是调试和发展它。

#include <iostream>
#include <iomanip>
#include <string>
#include <vector>
#include <sstream>
#include <stdexcept>

#define DBG(MSG) do { std::cerr << ':' << __LINE__ << ' ' << MSG << '\n'; } while (false)

#define NEED(WHAT, THROW_MSG) \
    do { if (WHAT) break ; \
        std::ostringstream oss; \
        oss << THROW_MSG; \
        throw std::runtime_error(oss.str()); \
    } while (false)

struct Queryable
{
    virtual int get_field_id(const std::string& field) const = 0;
    virtual void load_field(int id, std::string&, int&) const = 0;
};

class Evaluator
{
  public:
    // lexs expression, optionally proactively verifying field identifiers against *pq
    Evaluator(const std::string& expression, const Queryable* pq = nullptr)
    {
        std::istringstream iss(expression);
        char c;
        int unmatched_paren = 0;
        while (iss >> c)
        {
            switch (c)
            {
              case '(': tokens_.emplace_back(LParen); ++unmatched_paren; break;
              case ')': tokens_.emplace_back(RParen); --unmatched_paren; break;
              case '-': case '0'...'9':
              {
                iss.unget();
                int i;
                iss >> i;
                tokens_.emplace_back(i);
                break;
              }
              case '\'':
                tokens_.emplace_back(StringLit);
                iss >> std::noskipws;
                while (iss >> c)
                    if (c == '\'') goto post_lit;
                    else tokens_.back().s_ += c;
                throw std::runtime_error("unterminated string literal");
                post_lit:
                iss >> std::skipws;
                break;
              case '&':
                NEED(iss.get() == '&', "missing second '&' that'd form AND operator");
                tokens_.emplace_back(And);
                break;
              case '|':
                NEED(iss.get() == '|', "missing second '&' that'd form AND operator");
                tokens_.emplace_back(Or);
                break;
              case '<':
                if (iss.peek() == '=') { iss.ignore(); tokens_.emplace_back(LE); }
                else tokens_.emplace_back(L);
                break;
              case '>':
                if (iss.peek() == '=') { iss.ignore(); tokens_.emplace_back(GE); }
                else tokens_.emplace_back(G);
                break;
              case '!':
                if (iss.peek() == '=') { iss.ignore(); tokens_.emplace_back(NE); }
                else tokens_.emplace_back(Not);
                break;
              case '=':
                if (iss.peek() == '=') iss.ignore(); // allow = and ==
                tokens_.emplace_back(E);
                break;
              default:
                NEED(std::isalpha(c), "can't parse content in expression at "
                    << iss.tellg() << " in '" << iss.str() << "', problem text '"
                    << iss.str().substr(iss.tellg(), 20) << "'...");
                tokens_.emplace_back(Idn);
                tokens_.back().s_ += c;
                iss >> std::noskipws;
                while (iss >> c)
                    if (!std::isalnum(c)) { iss.unget(); goto post_idn; }
                    else tokens_.back().s_ += c;
                post_idn:
                tokens_.back().i_ = pq ? pq->get_field_id(tokens_.back().s_) : 0;
                iss >> std::skipws;
            }
        }
        NEED(!unmatched_paren, "unbalanced paren in expression");
        DBG("tokens parsed: " << tokens_);
    }

    bool operator()(const Queryable& q) const
    {
        size_t token_pos = 0;
        return eval(q, token_pos);
    }

  private:
    bool eval(const Queryable& q, size_t& token_pos) const
    {
        bool so_far = true;
        bool hanging_not = false;
        std::string s;
        int i;
        for ( ; token_pos < tokens_.size(); ++token_pos)
        {
            const Token& t = tokens_[token_pos];
            switch (t.type_)
            {
              case Idn:
              {
                int id = t.i_ ? t.i_ : q.get_field_id(t.s_);
                q.load_field(id, s, i);
                DBG("loaded field " << id << ':' << t.s_ << ", s '" << s << "', i " << i);
                const Token& op = tokens_.at(++token_pos);
                const Token& rhs = tokens_.at(++token_pos);
                switch(op.type_)
                {
                  case L:  so_far = id > 0 ? s <  rhs.s_ : i <  rhs.i_; break;
                  case LE: so_far = id > 0 ? s <= rhs.s_ : i <= rhs.i_; break;
                  case E:  so_far = id > 0 ? s == rhs.s_ : i == rhs.i_; break;
                  case GE: so_far = id > 0 ? s >= rhs.s_ : i >= rhs.i_; break;
                  case G:  so_far = id > 0 ? s >  rhs.s_ : i >  rhs.i_; break;
                  case NE: so_far = id > 0 ? s != rhs.s_ : i != rhs.i_; break;
                  default:
                    NEED(false, "identifier followed by " << op
                         << " but only an operator is supported");
                }
                DBG("  " << op << ' ' << rhs << " -> " << so_far);
                break;
              }
              case And:
              case Or:
                if (so_far == (t.type_ == Or))  // false && ...   true || ...
                {
                    int depth = 0;
                    while (token_pos < tokens_.size() && depth >= 0)
                        if (tokens_[++token_pos].type_ == LParen) ++depth;
                        else if (tokens_[token_pos].type_ == RParen) --depth;
                    return so_far;
                }
                break;

              case Not: hanging_not = true; break;

              case LParen:
                so_far = hanging_not ^ eval(q, ++token_pos);
                hanging_not = false;
                DBG("post LParen so_far " << so_far << ", token_pos " << token_pos);
                break;

              case RParen: return so_far;

              default:
                throw std::runtime_error("unexpect token");
            }
        }
        return so_far;
    }

    enum Type { Idn, StringLit, IntLit, LParen, RParen, Not, And, Or, L, LE, E, GE, G, NE };
    struct Token
    {
        Type type_; std::string s_; int i_;
        Token(Type type) : type_(type) { }
        Token(int i) : type_(IntLit), i_(i) { }
        Token(Type type, const std::string& s) : type_(type), s_(s) { }
        Token(Type type, const std::string&& s) : type_(type), s_(s) { }
    };
    std::vector<Token> tokens_;

    friend std::ostream& operator<<(std::ostream& os, Type t)
    {
        switch (t)
        {
          case Idn: return os << "Idn";
          case StringLit: return os << "StringLit";
          case IntLit: return os << "IntLit";
          case LParen: return os << "LParen";
          case RParen: return os << "RParen";
          case Not: return os << "Not";
          case And: return os << "And";
          case Or: return os << "Or";
          case L: return os << 'L';
          case LE: return os << "LE";
          case E: return os << 'E';
          case GE: return os << "GE";
          case G: return os << 'G';
          case NE: return os << "NE";
          default: throw std::runtime_error("invalid Token type");
       }
    }

    friend std::ostream& operator<<(std::ostream& os, const Token& t)
    {
        os << t.type_;
        if (t.type_ == Idn || t.type_ == StringLit) return os << ":'" << t.s_ << '\'';
        if (t.type_ == IntLit) return os << ':' << t.i_;
        return os;
    }

    friend std::ostream& operator<<(std::ostream& os, const std::vector<Token>& v)
    {
        os << '{';
        size_t pos = 0;
        for (const auto& t : v) os << ' ' << pos++ << ':' << t;
        return os << " }";
    }
};

样本用法:

struct Car : Queryable
{
    // negative field ids denote integral fields, positive strings, 0 is reserved
    enum Fields { Make = 1, Model, Year = -1};

    Car(const std::string& make, const std::string& model, int year)
      : make_(make), model_(model), year_(year)
    { }

    int get_field_id(const std::string& field) const override
    {
        if (field == "make") return (int)Make;
        if (field == "model") return (int)Model;
        if (field == "year") return (int)Year;
        throw std::runtime_error("attempt to lookup a field that doesn't exist");
    }

    void load_field(int id, std::string& s, int& i) const override
    {
        switch (id)
        {
          case Make: s = make_; break;
          case Model: s = model_; break;
          case Year: i = year_; break;
          default:
            throw std::runtime_error("attempt to retrieve a field using unknown field id");
        }
    }

    std::string make_, model_;
    int year_;
};

#define ASSERT_OP(X, OP, Y) \
    do { \
        const auto& x = (X); const auto& y = (Y); \
        if (x OP y) break; \
        std::cerr << "FAIL " << #X " " #OP " " #Y << " at :" << __LINE__ << '\n'; \
    } while (false)

#define ASSERT_EQ(X, Y) ASSERT_OP(X, ==, Y)
#define ASSERT(X) ASSERT_OP(X, ==, true)
#define ASSERT_NOT(X) ASSERT_OP(X, ==, false)

int main()
{
    Evaluator e("make == 'Honda' && (year == 1999 || year > 2005)");
    ASSERT(e(Car { "Honda", "Fit", 2008 }));
    ASSERT_NOT(e(Car { "Nissan", "GT-R", 2011 }));
    ASSERT(e(Car { "Honda", "NSX", 1999 }));

    // can also do field id lookups at Evaluator construction/lexing time for faster operator()...
    // (but then the Evaluator can't be used against other types with same field names but
    //  differing field ids)
    Car car { "Honda", "Civic", 2012 };
    Evaluator e2("make == 'Honda' && (year == 1999 || year > 2005)", &car);
    ASSERT(e2(car));
    ASSERT(e2(Car { "Honda", "Fit", 2008 }));
    ASSERT_NOT(e2(Car { "Nissan", "GT-R", 2011 }));
    ASSERT(e2(Car { "Honda", "NSX", 1999 }));
}

coliru.stacked-crooked.com

FWIW,任何对此问题空间感兴趣但确实有可用提升的读者可能更喜欢使用提升精神和/或使用boost::variant来处理不同类型。

答案 1 :(得分:1)

1)在编译器设计中学习大学课程或同等课程,或者自己研究LALR(1)解析器的理论。

2)为您的用户可输入的过滤语言定义正式语法,例如您给出的示例,您希望用户只需键入car.make == "honda" && car.year == "2012"即可指定搜索条件。尝试提出过滤语言的词汇和语法结构。

3)使用现有的常用工具(如lex)实现词法分析器和解析器;和yacc或它的现代堂兄GNU bison,为了实现输入你要实现的那种过滤字符串的框架,并产生某种解析结构,输入的过滤字符串,您现在可以执行并应用于现有的对象列表,以便执行过滤器。

关于它,听起来像一个有趣的项目。

当然,使用stock lex / yacc框架来实现这样的东西并不是绝对必要的。当然可以实现自己的手工编码词法分析器和语法分析器;这是我编写maildrop的方法,它实现了自己的内部词法分析器和递归下降解析器。

这不是一个人可以按下一两个按钮,并且让一个罐装解析器凭空蹦出来实现这样的东西的东西。这是一个相当复杂,复杂的计算机科学和编译器设计领域。

答案 2 :(得分:0)

一个选项可能是CLucene。如果你不能提升Boost,那也可能太大了。在这种情况下,您必须咬紧牙关并编写解析器和抽象语法树。获取编程语言概念书。