boost spirit x3在拆分后的奇怪语义行为

Strange semantic behaviour of boost spirit x3 after splitting

本文关键字:语义 spirit x3 拆分 boost      更新时间:2023-10-16

在我将语法拆分为推荐的parser.hppparser_def.hppparser.cpp文件后,我遇到了boost spirit x3的一个奇怪行为。我的示例gramar解析了一些简单的枚举:

enum = "enum" > identifier > "{" > identifier % "," > "}

这是我的枚举语法。当我不将枚举和标识符解析器拆分为推荐的文件时,一切都很好,尤其是字符串"enum {foo, bar}"如预期的那样抛出预期失败。这个例子可以在这里找到:未拆分的工作例子

但是,当我将完全相同的语法拆分为不同的文件时,解析器会抛出

terminate called after throwing an instance of 'std::logic_error'
what():  basic_string::_M_construct null not valid

尝试解析相同的字符串"enum {foo, bar}"

这个例子可以在这里找到:分裂的奇怪的例子

  1. ast.hpp

    #pragma once
    #include <vector>
    #include <string>
    #include <boost/fusion/include/adapt_struct.hpp>
    
    namespace ast{
    namespace x3 = boost::spirit::x3;
    struct Enum {
    std::string _name;
    std::vector<std::string> _elements;
    };
    
    }
    BOOST_FUSION_ADAPT_STRUCT(ast::Enum, _name, _elements)
    
  2. 配置.hpp

    #pragma once 
    #include <boost/spirit/home/x3.hpp>
    namespace parser{
    namespace x3 = boost::spirit::x3;
    typedef std::string::const_iterator iterator_type;
    typedef x3::phrase_parse_context<x3::ascii::space_type>::type context_type;
    }
    
  3. enum.cpp

    #include "enum_def.hpp"
    #include "config.hpp"
    namespace parser { namespace impl {
    BOOST_SPIRIT_INSTANTIATE(enum_type, iterator_type, context_type)
    }}
    namespace parser {
    const impl::enum_type& enum_parser()
    {
    return impl::enum_parser;
    }
    }
    
  4. enum_def.hpp

    #pragma once
    #include "identifier.hpp"
    #include "enum.hpp"
    #include "ast.hpp"
    namespace parser{ namespace impl{
    namespace x3=boost::spirit::x3;
    const enum_type enum_parser = "enum";
    namespace{
    const auto& identifier = parser::identifier();
    }
    auto const enum_parser_def =
    "enum"
    > identifier
    > "{"
    > identifier % ","
    >"}";
    BOOST_SPIRIT_DEFINE(enum_parser)
    }}
    
  5. enum.hpp

    #pragma once
    #include <boost/spirit/home/x3.hpp>
    #include "ast.hpp"
    namespace parser{ namespace impl{
    namespace x3=boost::spirit::x3;
    typedef x3::rule<class enum_class, ast::Enum> enum_type;
    BOOST_SPIRIT_DECLARE(enum_type)
    }}
    namespace parser{
    const impl::enum_type& enum_parser();
    }
    
  6. 识别器.cpp

    #include "identifier_def.hpp"
    #include "config.hpp"
    namespace parser { namespace impl {
    BOOST_SPIRIT_INSTANTIATE(identifier_type, iterator_type, context_type)
    }}
    namespace parser {
    const impl::identifier_type& identifier()
    {
    return impl::identifier;
    }
    }
    
  7. identifier_def.hpp

    #pragma once
    #include <boost/spirit/home/x3.hpp>
    #include "identifier.hpp"
    namespace parser{ namespace impl{
    namespace x3=boost::spirit::x3;
    const identifier_type identifier = "identifier";    
    auto const identifier_def = x3::lexeme[
    ((x3::alpha | '_') >> *(x3::alnum | '_'))
    ];
    BOOST_SPIRIT_DEFINE(identifier)
    }}
    
  8. 识别器.hpp

    #pragma once
    #include <boost/spirit/home/x3.hpp>
    namespace parser{ namespace impl{
    namespace x3=boost::spirit::x3;
    typedef x3::rule<class identifier_class, std::string> identifier_type;
    BOOST_SPIRIT_DECLARE(identifier_type)
    }}
    
    namespace parser{
    const impl::identifier_type& identifier();
    }
    
  9. main.cpp

    #include <boost/spirit/home/x3.hpp>
    #include "ast.hpp"
    #include "enum.hpp"
    namespace x3 = boost::spirit::x3;
    template<typename Parser, typename Attribute>
    bool test(const std::string& str, Parser&& p, Attribute&& attr)
    {
    using iterator_type = std::string::const_iterator;
    iterator_type in = str.begin();
    iterator_type end = str.end();
    bool ret = x3::phrase_parse(in, end, p, x3::ascii::space, attr);
    ret &= (in == end);
    return ret;
    }
    int main(){
    ast::Enum attr;
    test("enum foo{foo,bar}", parser::enum_parser(), attr);
    test("enum {foo,bar}", parser::enum_parser(), attr);    
    }
    

这是一个错误,是我遗漏了什么,还是预期的行为?

编辑:这是我的repo,其中有一个抛出std::logic_error而不是expectation_failure的例子

我已经找到了错误的原因。

错误在于expect指令按值接受其主题解析器,这是在parser::impl::identifier初始值设定项运行之前。

要进行可视化,请想象parser::impl::enum_parser的静态初始值设定项在parser::impl::identifier之前运行。这对编译器来说是有效的。

因此,副本有一个未初始化的name字段,一旦期望点尝试用which_成员构造x3::expectation_failure,该字段就会失败,因为从nullptr构造std::string是非法的。

总而言之,我担心这里的根本原因是静态初始化顺序Fiasco。我会看看我是否可以修复它并提交一份PR。

解决方法:

一个即时的解决方法是反向列出源文件的顺序,以便在定义之后使用:

set(SOURCE_FILES 
identifier.cpp
enum.cpp 
main.cpp 
)

请注意,如果这在您的编译器上修复了它(在我的编译器上也修复了它),那么它就是实现定义的。该标准没有指定跨编译单元的静态初始化顺序。

这是一个对我有效的解决方案,而上面的工作循环没有。

假设您有文件a.cppa.ha_def.hppb.cppb.hb_def.hpp。。。正如BoostSpirit X3文档所建议的那样。

基本思想是将*.cpp*_def.hpp文件组合为每组一个文件。*.h文件可以而且应该保留。

  1. ls *_def.hpp > parser_def.hpp(假设parser_def.hpp还不存在),并按正确顺序编辑parser_def.hpp#include文件。删除多余的行(添加标头保护等)parser_def.hpp的目标是以正确的顺序包含其他文件
  2. cat *.cpp > parser.cppparser.cpp编辑为语法正确,并将所有#include <*_def.hpp>行替换为顶部的单个#include <parser_def.hpp>。在构建文件(例如make或cmake)中,将*.cpp文件的编译替换为单个parser.cpp
  3. 您可以删除旧的*.cpp文件

您失去了单独编译文件的便利性,但它将避免静态初始化顺序Fiasco。