【问题标题】:Parsing a string to create a geometry解析字符串以创建几何图形
【发布时间】:2021-10-30 14:03:07
【问题描述】:

开发字符串解析器以创建几何图形的算法是什么?几何图形分两步生成:第一步,我们创建图元;其次,我们将基元组合成对象。

语法显示在下面的字符串中。

string str="[GEOMETRY]    
    PRIMITIVE1=SPHERE(RADIUS=5.5);  
    PRIMITIVE2=BOX(A=-5.2, B=7.3);  
    //...  
    OBJECT1=PRIMITIVE2*(-PRIMITIVE1);  
    //..."

class PRIMITIVE{
    int number;
public:
    Primitive& operator+ (Primitive& primitive) {}; //overloading arithmetic operations
    Primitive& operator* (Primitive& primitive) {};
    Primitive& operator- (Primitive& primitive) {};
    virtual bool check_in_point_inside_primitive = 0;
};

class SPHERE:public PRIMITIVE{
    double m_radius;
public:
    SPHERE(double radius): m_radius(radius) {};  //In which part of the parser to create objects?
    bool check_in_point_inside_sphere(Point& point){};
};

class BOX:public PRIMITIVE{
    double m_A;
    double m_B;
public:
    BOX(double A, double B): m_A(A), m_B(B) {};
    bool check_in_point_inside_box(Point& point){};
};

class OBJECT{
    int number;
    PRIMITIVE& primitive;
public:
    OBJECT(){};
    bool check_in_point_inside_object(Primitive& PRIMITIVE1, Primitive& PRIMITIVE2, Point& point){
        //>How to construct a function from an expression 'PRIMITIVE2*(-PRIMITIVE1)' when parsing?
    }
};
  1. 如何解析字符串PRIMITIVE1=SPHERE(RADIUS=5.5),给SPHERE()的构造函数传一个参数?如何识别这个名称为PRIMITIVE 1 的对象以在OBJECT 中调用它?是否可以创建pair<PRIMITIVE1,SPHERE(5.5)> 并将所有图元存储在地图中?

  2. 如何解析OBJECT1 的字符串并从OBJECT1 内的表达式PRIMITIVE2*(-PRIMITIVE1) 构造函数?在确定每个点相对于对象的位置时,将多次需要此表达式。

  3. 如何使用boost::spirit 来完成这项任务?使用boost::spirit::lex标记一个字符串,然后使用boost::spirit::qi开发规则?

【问题讨论】:

  • 您的语言(语法)似乎很正常。那么生活会很简单,你不需要解析器,只需要一个 DFA 或std::regex。对于对象的后期实例化,您应该使用抽象工厂。那么,您能否提供更多示例和解释,您的输入字符串是什么样的?而且,原语中的“-”是什么意思?
  • 你真的需要这么复杂的语法吗? WaveFront OBJ 之类的东西,或者如果做不到这一点,甚至 JSON 也会让你的生活更轻松。 OBJ 解析器是微不足道的,但可能不容易捕获您正在寻找的原始类型,因为它们专注于顶点和面而不是 CSG。然而,JSON 格式可以为您提供所有这些,并且解析器不仅易于实现,而且已经免费提供。
  • 我们可以编写一个语法,并在所示的句法后面想象一个完整的语义词。但是我有类型层次结构的问题。正如给定的那样,它们将无法编译并且无法正常工作(运算符正在返回引用?如果他们要返回副本,它怎么会不切分到抽象基类?)。我认为在构建工具来解析它之前,您需要更好地考虑您的设计。

标签: c++ boost-spirit text-parsing


【解决方案1】:

作为一个手指练习,尽管我看到所选虚拟类型层次结构存在严重问题,但让我们尝试创建一个面向值的 Primitives 容器,可以通过它们的 id (ById) 进行索引:

Live On Coliru

#include <boost/intrusive/set.hpp>
#include <boost/poly_collection/base_collection.hpp>
#include <iostream>
namespace bi = boost::intrusive;

struct Point {
};

using IndexHook = bi::set_member_hook<bi::link_mode<bi::auto_unlink>>;

class Primitive {
    int _id;

  public:
    struct ById {
        bool operator()(auto const&... oper) const { return std::less<>{}(access(oper)...); }

      private:
        static int access(int id) { return id; }
        static int access(Primitive const& p) { return p._id; }
    };

    IndexHook _index;

    Primitive(int id) : _id(id) {}
    virtual ~Primitive() = default;
    int id() const { return _id; }

    Primitive& operator+= (Primitive const& primitive) { return *this; } //overloading arithmetic operations
    Primitive& operator*= (Primitive const& primitive) { return *this; }
    Primitive& operator-= (Primitive const& primitive) { return *this; }
    virtual bool check_in_point_inside(Point const&) const = 0;
};

using Index =
    bi::set<Primitive, bi::constant_time_size<false>,
            bi::compare<Primitive::ById>,
            bi::member_hook<Primitive, IndexHook, &Primitive::_index>>;

class Sphere : public Primitive {
    double _radius;

  public:
    Sphere(int id, double radius)
        : Primitive(id)
        , _radius(radius) {} // In which part of the parser to create objects?
    bool check_in_point_inside(Point const& point) const override { return false; }
};

class Box : public Primitive {
    double _A;
    double _B;

  public:
    Box(int id, double A, double B) : Primitive(id), _A(A), _B(B) {}
    bool check_in_point_inside(Point const& point) const override { return false; }
};

class Object{
    int _id;
    Primitive& _primitive;

  public:
    Object(int id, Primitive& p) : _id(id), _primitive(p) {}

    bool check_in_point_inside_object(Primitive const& p1, Primitive const& p2,
                                      Point const& point) const
    {
        //>How to construct a function from an expression
        //'PRIMITIVE2*(-PRIMITIVE1)' when parsing?
        return false;
    }
};

using Primitives = boost::poly_collection::base_collection<Primitive>;

int main() {
    Primitives test;
    test.insert(Sphere{2, 4.0});
    test.insert(Sphere{4, 4.0});
    test.insert(Box{2, 5, 6});
    test.insert(Sphere{1, 4.0});
    test.insert(Box{3, 5, 6});

    Index idx;
    for (auto& p : test)
        if (not idx.insert(p).second)
            std::cout << "Duplicate id " << p.id() << " not indexed\n";

    for (auto& p : idx)
        std::cout << typeid(p).name() << " " << p.id() << "\n";

    std::cout << "---\n";

    for (auto& p : test)
        std::cout << typeid(p).name() << " " << p.id() << "\n";
}

打印

Duplicate id 2 not indexed
6Sphere 1
3Box 2
3Box 3
6Sphere 4
---
3Box 2
3Box 3
6Sphere 2
6Sphere 4
6Sphere 1

到目前为止一切顺利。这是在处理 Spirit 语法中的虚拟类型时防止各种痛苦的重要组成部分¹

PS:我已经放弃了 intrusive_set 的想法。它不起作用,因为 base_container 在重新分配时会移动项目,并且会取消项目与其侵入集的链接。

相反,请参阅下文,了解在解析期间不尝试解析 id 的方法。


解析原语

我们从PRIMITIVE1 获取ID。我们可以在自然解析原语本身之前将其存储在某个地方,然后在提交时为其设置 id。

让我们从为解析器定义一个 State 对象开始:

struct State {
    Ast::Id         next_id;
    Ast::Primitives primitives;
    Ast::Objects    objects;

    template <typename... T> void commit(boost::variant<T...>& val) {
        boost::apply_visitor([this](auto& obj) { commit(obj); }, val);
    }

    template <typename T> void commit(T& primitiveOrExpr) {
        auto id = std::exchange(next_id, 0);
        if constexpr (std::is_base_of_v<Ast::Primitive, T>) {
            primitiveOrExpr.id = id;
            primitives.insert(std::move(primitiveOrExpr));
        } else {
            objects.push_back(Ast::Object{id, std::move(primitiveOrExpr)});
        }
    }
};

如您所见,我们只是有一个存储原语、对象的地方。然后是我们的next_id 的临时存储空间,而我们仍在解析下一个实体。

commit 函数有助于对解析器规则的产品进行排序。碰巧,它们可以是变体,这就是为什么我们在变体上为commit 分配了apply_visitor

再一次,正如脚注¹解释的那样,Spirit 的自然属性合成有利于静态多态性。

我们现在需要的语义动作是:

static inline auto& state(auto& ctx) { return get<State>(ctx); }
auto draft = [](auto& ctx) { state(ctx).next_id = _attr(ctx); };
auto commit = [](auto& ctx) { state(ctx).commit(_attr(ctx)); };

现在让我们跳到原语:

auto sphere = as<Ast::Sphere>(eps >> "sphere" >>'(' >> param("radius") >> ')');
auto box    = as<Ast::Box>(eps >> "box" >> '(' >> param('a') >> ',' >> param('b') >> ')');
auto primitive =
    ("primitive" >> uint_[draft] >> '=' >> (sphere | box)[commit]) > ';';

这还是有点作弊,因为我使用了 param 帮助器来减少打字:

auto number = as<Ast::Number>(double_, "number");
auto param(auto name, auto p) { return eps >> omit[name] >> '=' >> p; }
auto param(auto name) { return param(name, number); }

如您所见,我已经假设大多数参数都具有数值性质。

什么是真正的对象?

看了一会儿,我得出的结论是,真正的 Object 被定义为与表达式相关联的 id 号(OBJECT1、OBJECT2...)。该表达式可以引用原语并具有一些一元和二元运算符。

让我们为此绘制一个 AST:

using Number = double;
struct RefPrimitive { Id id; };
struct Binary;
struct Unary;

using Expr = boost::variant<         //
    Number,                          //
    RefPrimitive,                    //
    boost::recursive_wrapper<Unary>, //
    boost::recursive_wrapper<Binary> //
    >;

struct Unary { char op; Expr oper; };
struct Binary { Expr lhs; char op; Expr rhs; };
struct Object { Id   id; Expr expr; };

现在解析成那个表达式 AST

每个 Ast 节点类型实际上是 1:1 规则。例如:

auto ref_prim = as<Ast::RefPrimitive>(lexeme["primitive" >> uint_]);

现在很多表达式规则都可以递归了,所以我们需要通过 BOOST_SPIRIT_DEFINE 定义的声明规则:

// object expression grammar
rule<struct simple_tag, Ast::Expr>  simple{"simple"};
rule<struct unary_tag,  Ast::Unary> unary{"unary"};
rule<struct expr_tag,   Ast::Expr>  expr{"expr"};
rule<struct term_tag,   Ast::Expr>  term{"term"};
rule<struct factor_tag, Ast::Expr>  factor{"factor"};

如您所知,其中一些与 Ast 节点不是 1:1,主要是因为递归和运算符优先级的差异(term vs factor vs. simple)。使用规则定义更容易查看:

auto unary_def  = char_("-+") >> simple;
auto simple_def = ref_prim | unary | '(' >> expr >> ")";
auto factor_def = simple;
auto term_def   = factor[assign] >> *(char_("*/") >> term)[make_binary];
auto expr_def   = term[assign] >> *(char_("-+") >> expr)[make_binary];

因为没有一个规则实际上暴露了Binary,所以自动属性传播在那里并不方便²。相反,我们使用assignmake_binary 语义动作:

auto assign = [](auto& ctx) { _val(ctx) = _attr(ctx); };
auto make_binary = [](auto& ctx) {
    using boost::fusion::at_c;
    auto& attr = _attr(ctx);
    auto  op   = at_c<0>(attr);
    auto& rhs  = at_c<1>(attr);
    _val(ctx)  = Ast::Binary { _val(ctx), op, rhs };
};

最后,让我们将定义与声明的规则联系起来(使用它们的标记类型):

BOOST_SPIRIT_DEFINE(simple, unary, expr, term, factor)

我们只需要与primitive 类似的一行:

auto object =
    ("object" >> uint_[draft] >> '=' >> (expr)[commit]) > ';';

我们可以通过将每一行定义为一个基元|对象来完成:

auto line = primitive | object;
auto file = no_case[skip(ws_comment)[*eol >> "[geometry]" >> (-line % eol) >> eoi]];

在顶层,我们期望 [GEOMETRY] 标头,指定我们希望不区分大小写并且...要跳过 ws_comment³:

auto ws_comment = +(blank | lexeme["//" >> *(char_ - eol) >> eol]);

这让我们也可以忽略// comments

现场演示时间

Live On Compiler Explorer

//#define BOOST_SPIRIT_X3_DEBUG
#include <boost/fusion/adapted.hpp>
#include <boost/poly_collection/base_collection.hpp>
#include <boost/spirit/home/x3.hpp>
#include <iostream>
#include <list>
#include <map>
namespace x3 = boost::spirit::x3;

namespace Ast {
    using Id     = uint32_t;
    struct Point { }; // ?? where does this belong?
    struct Primitive {
        Id id;
        virtual ~Primitive() = default;
    };
    struct Sphere : Primitive { double radius; };
    struct Box : Primitive { double a, b; };

    using Number = double;
    struct RefPrimitive { Id id; };
    struct Binary;
    struct Unary;

    using Expr = boost::variant<         //
        Number,                          //
        RefPrimitive,                    //
        boost::recursive_wrapper<Unary>, //
        boost::recursive_wrapper<Binary> //
        >;

    struct Unary { char op; Expr oper; };
    struct Binary { Expr lhs; char op; Expr rhs; };
    struct Object { Id   id; Expr expr; };
    using Primitives = boost::poly_collection::base_collection<Primitive>;
    using Objects    = std::list<Object>;
    using Index      = std::map<Id, std::reference_wrapper<Primitive const>>;

    std::ostream& operator<<(std::ostream& os, Primitive const& p) {
        return os << boost::core::demangle(typeid(p).name()) << " "
                  << "(id: " << p.id << ")";
    }
    std::ostream& operator<<(std::ostream& os, Object const& o) {
        return os << "object(id:" << o.id << ", expr:" << o.expr << ")";
    }
    std::ostream& operator<<(std::ostream& os, RefPrimitive ref) {
        return os << "reference(prim:" << ref.id << ")";
    }
    std::ostream& operator<<(std::ostream& os, Binary const& b) {
        return os << '(' << b.lhs << b.op << b.rhs << ')';
    }
    std::ostream& operator<<(std::ostream& os, Unary const& u) {
        return os << '(' << u.op << u.oper << ')';
    }
} // namespace Ast

BOOST_FUSION_ADAPT_STRUCT(Ast::Primitive, id)
BOOST_FUSION_ADAPT_STRUCT(Ast::Sphere, radius)
BOOST_FUSION_ADAPT_STRUCT(Ast::Box, a, b)
BOOST_FUSION_ADAPT_STRUCT(Ast::Object, id)
BOOST_FUSION_ADAPT_STRUCT(Ast::RefPrimitive, id)
BOOST_FUSION_ADAPT_STRUCT(Ast::Unary, op, oper)

namespace Parser {
    using namespace x3;

    struct State {
        Ast::Id         next_id;
        Ast::Primitives primitives;
        Ast::Objects    objects;

        template <typename... T> void commit(boost::variant<T...>& val) {
            boost::apply_visitor([this](auto& obj) { commit(obj); }, val);
        }

        template <typename T> void commit(T& val) {
            auto id = std::exchange(next_id, 0);
            if constexpr (std::is_base_of_v<Ast::Primitive, T>) {
                val.id = id;
                primitives.insert(std::move(val));
            } else {
                objects.push_back(Ast::Object{id, std::move(val)});
            }
        }
    };

    static inline auto& state(auto& ctx) { return get<State>(ctx); }
    auto draft = [](auto& ctx) { state(ctx).next_id = _attr(ctx); };
    auto commit = [](auto& ctx) { state(ctx).commit(_attr(ctx)); };

    template <typename T>
    auto as = [](auto p, char const* name = "as") {
        return rule<struct _, T>{name} = p;
    };

    auto ws_comment = +(blank | lexeme["//" >> *(char_ - eol) >> (eol | eoi)]);

    auto number = as<Ast::Number>(double_, "number");
    auto param(auto name, auto p) { return eps >> omit[name] >> '=' >> p; }
    auto param(auto name) { return param(name, number); }

    auto sphere = as<Ast::Sphere>(eps >> "sphere" >>'(' >> param("radius") >> ')');
    auto box    = as<Ast::Box>(eps >> "box" >> '(' >> param('a') >> ',' >> param('b') >> ')');
    auto primitive =
        ("primitive" >> uint_[draft] >> '=' >> (sphere | box)[commit]) > ';';
    
    auto ref_prim = as<Ast::RefPrimitive>(lexeme["primitive" >> uint_], "ref_prim");

    // object expression grammar
    rule<struct simple_tag, Ast::Expr>  simple{"simple"};
    rule<struct unary_tag,  Ast::Unary> unary{"unary"};
    rule<struct expr_tag,   Ast::Expr>  expr{"expr"};
    rule<struct term_tag,   Ast::Expr>  term{"term"};
    rule<struct factor_tag, Ast::Expr>  factor{"factor"};

    auto assign = [](auto& ctx) { _val(ctx) = _attr(ctx); };
    auto make_binary = [](auto& ctx) {
        using boost::fusion::at_c;
        auto& attr = _attr(ctx);
        auto  op   = at_c<0>(attr);
        auto& rhs  = at_c<1>(attr);
        _val(ctx)  = Ast::Binary { _val(ctx), op, rhs };
    };

    auto unary_def  = char_("-+") >> simple;
    auto simple_def = ref_prim | unary | '(' >> expr >> ")";
    auto factor_def = simple;
    auto term_def   = factor[assign] >> *(char_("*/") >> term)[make_binary];
    auto expr_def   = term[assign] >> *(char_("-+") >> expr)[make_binary];

    BOOST_SPIRIT_DEFINE(simple, unary, expr, term, factor)

    auto object =
        ("object" >> uint_[draft] >> '=' >> (expr)[commit]) > ';';
    auto line = primitive | object;
    auto file = no_case[skip(ws_comment)[*eol >> "[geometry]" >> (-line % eol) >> eoi]];
} // namespace Parser

int main() {
    for (std::string const input :
         {
             R"(
[geometry]    
    primitive1=sphere(radius=5.5);  
    primitive2=box(a=-5.2, b=7.3);  
    //...  
    object1=primitive2*(-primitive1);  
    //...)",
             R"(
[GEOMETRY]    
    PRIMITIVE1=SPHERE(RADIUS=5.5);  
    PRIMITIVE2=BOX(A=-5.2, B=7.3);  
    //...  
    OBJECT1=PRIMITIVE2*(-PRIMITIVE1);  
    //...)",
         }) //
    {
        Parser::State state;

        bool ok = parse(begin(input), end(input),
                        x3::with<Parser::State>(state)[Parser::file]);
        std::cout << "Parse success? " << std::boolalpha << ok << "\n";

        Ast::Index index;

        for (auto& p : state.primitives)
            if (auto[it,ok] = index.emplace(p.id, p); not ok) {
                std::cout << "Duplicate id " << p
                          << " (conflicts with existing " << it->second.get()
                          << ")\n";
            }

        std::cout << "Primitives by ID:\n";
        for (auto& [id, prim] : index)
            std::cout << " - " << prim << "\n";

        std::cout << "Objects in definition order:\n";
        for (auto& obj: state.objects)
            std::cout << " - " << obj << "\n";
    }
}

打印

Parse success? true
Primitives by ID:
 - Ast::Sphere (id: 1)
 - Ast::Box (id: 2)
Objects in definition order:
 - object(id:1, expr:(reference(prim:2)*(-reference(prim:1))))
Parse success? true
Primitives by ID:
 - Ast::Sphere (id: 1)
 - Ast::Box (id: 2)
Objects in definition order:
 - object(id:1, expr:(reference(prim:2)*(-reference(prim:1))))

¹How can I use polymorphic attributes with boost::spirit::qi parsers?

² 并坚持这样做会导致经典的效率低下,规则会导致大量回溯

³在词位之外

【讨论】:

  • 使用虚拟方法添加半径/A/B打印:compiler-explorer.com/z/ccW8jnKTP
  • 感谢您的广泛回答! “球体”规则的 RHS 合成属性 fusion::vector&lt;int&gt;,而 LHS 具有 &lt;Ast::Sphere&gt;,因为在原地创建了 Sphere(raduis) 的实例。如果 Sphere 结构的数据有一个matrix&lt;double&gt; mat {3,3,0} 容器而不是double radius。解析器属性param("radius") 必须分配给元素mat(3,3)。是否可以通过创建新的语义规则来做到这一点,或者是否有必要在融合容器和矩阵容器之间创建一个适配器?
  • 我总是将关注点分开,甚至与这个特定示例无关。解析和生成几何图形是根本不同的任务。通过分离关注点,您可以避免紧密耦合和维护成本。如果需要,更换一个零件总是很容易。
  • 证明你可以做任何你想做的事(显然):compiler-explorer.com/z/47Kj6oK8e我不认为这是一个好方法,因为这很可能只是冰山一角。 110% 的机会你也想设置数组的其他元素。保持简单:)
  • 使用实际 (3,3) 矩阵而不使用 libfmt 的更好示例:compiler-explorer.com/z/bnod8a9Ms
猜你喜欢
  • 2011-08-15
  • 1970-01-01
  • 2023-03-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-02-16
  • 1970-01-01
相关资源
最近更新 更多