简体   繁体   English

Boost phoenix可变参数函数解析

[英]Boost phoenix variadic function parse

I have a parser code as below for a function "TakeOne". 我有一个函数“ TakeOne”的解析器代码,如下所示。 TakeOne function works like it returns the first parameter which is not equal to '%null%' Ex: TakeOne函数的工作方式类似于返回不等于'%null%'的第一个参数,例如:

TakeOne( %null% , '3', 'defaultVal'); -->  result = 3
TakeOne( 5 , 'asd', 'defaultVal');   -> result = 5

Now I want to modify this function to 现在我想将此功能修改为

TakeOne(parm1, parm2, ... , defaultValue);

Is it possible to do this without using C++11 features? 不使用C ++ 11功能就可以做到这一点吗? Thanks 谢谢

#include <string>
#include <fstream>
#include <algorithm>
#include "sstream"
#include <locale.h>
#include <iomanip>

#define BOOST_SPIRIT_USE_PHOENIX_V3

#include <boost/functional/hash.hpp>
#include <boost/variant.hpp>
#include <boost/smart_ptr.hpp>
#include <boost/algorithm/string.hpp>
#include <boost/numeric/conversion/cast.hpp>
#include <boost/tokenizer.hpp>
#include <boost/spirit/include/qi.hpp>
#include <boost/spirit/include/phoenix.hpp>
#include <boost/phoenix/function/adapt_function.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/math/constants/constants.hpp>
#include <boost/math/special_functions/round.hpp>
#include <boost/exception/diagnostic_information.hpp> 
#include <boost/algorithm/string.hpp> 

namespace qi    = boost::spirit::qi;
namespace phx   = boost::phoenix;

typedef double NumValue;
typedef boost::variant<double, std::wstring> GenericValue;

const std::wstring ParserNullChar = L"%null%";
const double NumValueDoubleNull = std::numeric_limits<double>::infinity();

//Convert string to numeric values
struct AsNumValue : boost::static_visitor<double>
{
    double operator()(double d)              const { return d; }
    double operator()(std::wstring const& s) const
    { 
        if(boost::iequals(s, ParserNullChar))
        {
            return NumValueDoubleNull;
        }
        try { return boost::lexical_cast<double>(s); } 
        catch(...) 
        {
            throw;
        }
    }
};

double Num(GenericValue const& val)
{ 
    return boost::apply_visitor(AsNumValue(), val);
}

bool CheckIfNumValueIsNull(double num)
{
    if(num == NumValueDoubleNull)
        return true;
    else
        return false;
}


bool CheckIfGenericValIsNull(const GenericValue& val)
{
    std::wostringstream woss;
    woss << val;

    if(boost::iequals(woss.str(), ParserNullChar))
    {
        return true;
    }
    else if(val.which() != 1 && CheckIfNumValueIsNull(Num(val)))
    {
        return true;
    }
    else
        return false;
}


GenericValue TakeOne(GenericValue val1, GenericValue val2, GenericValue def)
{
  if(!CheckIfGenericValIsNull(val1))
    {
         return val1;
    }
    else if(!CheckIfGenericValIsNull(val2))
    {
        return val2;
    }
    else
        return def;
}



BOOST_PHOENIX_ADAPT_FUNCTION(GenericValue, TakeOne_, TakeOne, 3)

template <typename It, typename Skipper = qi::space_type >
struct MapFunctionParser : qi::grammar<It, GenericValue(), Skipper>
{
    MapFunctionParser() : MapFunctionParser::base_type(expr_)
    {
        using namespace qi;

        function_call_ = 
          (no_case[L"TakeOne"] > '(' > expr_ > ',' > expr_ > ',' > expr_ > ')') 
            [_val = TakeOne_(_1, _2, _3) ];


        string_ =   (L'"' > *~char_('"') > L'"')
            | (L"'" > *~char_("'") > L"'"); 


        factor_ =
    (no_case[ParserNullChar])     [_val = NumValueDoubleNull]
        |   double_                    [ _val = _1]
        |   string_              [ _val = _1]
        |   function_call_       [ _val = _1]
        ;


        expr_ = factor_;

        on_error<fail> ( expr_, std::cout
            << phx::val("Error! Expecting ") << _4 << phx::val(" here: \"")
            << phx::construct<std::string>(_3, _2) << phx::val("\"\n"));

#ifdef _DEBUG 
        BOOST_SPIRIT_DEBUG_NODE(function_call_);
        BOOST_SPIRIT_DEBUG_NODE(expr_);
        BOOST_SPIRIT_DEBUG_NODE(string_);
        BOOST_SPIRIT_DEBUG_NODE(factor_);
#endif
    }

private:
    qi::rule<It, std::wstring()> string_;
    qi::rule<It, GenericValue(), Skipper> function_call_, expr_, factor_;

};


int main()
{
    std::wstringstream wss;
    typedef std::wstring::const_iterator AttIter;
    MapFunctionParser<AttIter , boost::spirit::qi::space_type> mapFunctionParser;
    bool ret;
    GenericValue result;

    std::wstring functionStr = L"TakeOne(%null%, 5, 'default')";
    std::wstring::const_iterator beginExpression(functionStr.begin());
    std::wstring::const_iterator endExpression(functionStr.end());

    ret = boost::spirit::qi::phrase_parse(beginExpression,endExpression,mapFunctionParser,boost::spirit::qi::space,result);

    std::wcout << result << std::endl;
    return 0;
}

Here's a second answer to address (some of the) the XY-problem(s) you are probably trying to solve. 这是第二个答案,用于解决您可能试图解决的XY问题。

As I noted in a comment there are a number of code smells [1] in your example. 正如我在评论中指出的那样,您的示例中有许多代码气味 [1] Let me explain what I mean. 让我解释一下我的意思。


The Goal 目标

Let's consider what the goal of the program is: You're doing input parsing . 让我们考虑一下程序的目标是什么您正在执行输入解析

The result of parsing should be data, preferrably in C++ datatypes with strong type information, so you can avoid working with quirky (possibly invalid) variable text representations and focus on business logic. 解析的结果应该是数据,最好是具有强类型信息的C ++数据类型,因此可以避免使用古怪的(可能无效的)可变文本表示形式,而将精力放在业务逻辑上。


The Smells 气味

Now on to the smells: 现在来闻一下:

  • You define "abstract datatypes" (like NumValue ) but then you fail to use them consistently: 您定义了“抽象数据类型”(如NumValue ),但随后却无法一致地使用它们:

     typedef double NumValue; typedef boost::variant<double, std::wstring> GenericValue; // ^--- should be NumValue 

    Be more consistent, and make your code reflect the design : 更加一致,并使您的代码反映设计

     namespace ast { typedef double Number; typedef std::wstring String; typedef boost::variant<Number, String> Value; } 
  • You use a parser generator for the parsing, yet you are also invoking 您使用解析器生成器进行解析, 但是您也正在调用

    • boost::lexical_cast<double> on ... strings 在字符串上使用boost::lexical_cast<double>
    • wostringstream , from which you (forgetting std::ios::skipws ...) extract "a" string wostringstream ,从中(忘记std::ios::skipws ...)提取“ a”字符串
    • boost::iequals to compare strings, that should already have been parsed into their strongly-typed AST types, independent of letter-case. boost::iequals比较字符串,该字符串应该已经被解析为其强类型的AST类型,而与字母大小写无关。
    • You have a static_visitor to act on variant types, yet you rely on stringification (using wostringstream ). 您有一个static_visitor可以处理变量类型, 但是您依赖于字符串化(使用wostringstream )。 In fact, you only ever call the visitor on that variant iff you already know that it's a number: 事实上,你永远只能调用该变种访问者当且仅当你已经知道,这是一个数字:

       else if(val.which() != 1 && CheckIfNumValueIsNull(Num(val))) 

      That's a bit funny, because in this case you could just have used boost::get<NumValue>(val) to get the known-type value out. 这有点有趣,因为在这种情况下,您可以使用boost::get<NumValue>(val)来获取已知类型的值。

    Pro Tip: Using "lowlevel" parsing/streaming operations while using a high-level parser-generator, is a code smell 专家提示:在使用高级解析器生成器的同时使用“低级”解析/流操作是代码的味道

  • Your generic value variant suggests that your grammar supports two kind of values. 您的通用值变体建议您的语法支持两种值。 Yet, your grammar definition clearly shows that you have a third type of value: %null% . 但是,您的语法定义清楚地表明您具有第三种类型的值: %null%

    There's evidence that you got somewhat confused by this yourself, as we can see the parser 有证据表明您自己对此有些困惑,因为我们可以看到解析器

    • parsing the literal %null% (or %NULL% etc) into ... some kind of magic number. 将文字%null% (或%NULL%等)解析为...某种幻数。
    • therefore, we know that iff %null% was parsed, it will always be a NumValue in your AST 因此,我们知道仅在对%null%进行了解析时,它始终是AST中的NumValue
    • we also know that strings will always be parsed into a wstring subtype GenericValue 我们还知道,字符串将始终被解析为wstring子类型GenericValue
    • yet , we can see you treat any GenericValue as potentially null ? 但是 ,我们可以看到您将任何GenericValue视为可能为null

    All in all it leads to a rather surprising... 总而言之,这导致了令人惊讶的...

    Summary : You have AsNumValue which you (ironically) appear to be using to find out whether a String might actually be Null 摘要 :您具有(讽刺地)似乎正在使用AsNumValue来查找String是否实际上为Null

    Hint: a String could never represent the %null% to begin with, it makes no sense to transform random strings into numbers, and random 'magic numeric values' should not have been used to represent Null in the first place. 提示: String永远不能代表%null% ,将随机字符串转换为数字没有任何意义,并且首先不应该使用随机的“魔术数值”来表示Null

  • Your grammar makes unbalanced use of semantic actions : 您的语法不平衡地使用了语义动作

     factor_ = (no_case[ParserNullChar]) [_val = NumValueDoubleNull] | double_ [ _val = _1] | string_ [ _val = _1] | function_call_ [ _val = _1] ; 

    We notice that you are simultaneously 我们注意到您同时

    • using SA's to manually perform what automatic attribute propagation is supposed to do ( [_val = _1] ) 使用SA手动执行应该执行的自动属性传播( [_val = _1]
    • using a single branch for "magic" purposes (this is where you require a Null AST datatype) 为“魔术”目的使用单个分支(这是您需要Null AST数据类型的地方)

    In my suggested solution below, the rule becomes: 在下面的建议解决方案中,规则变为:

     factor_ = null_ | string_ | double_ | function_call_; 

    That's it. 而已。

    Pro Tip: Using Semantic Actions sparingly (see also Boost Spirit: "Semantic actions are evil"? ) 专家提示:谨慎使用语义动作(另请参阅Boost Spirit:“语义动作是邪恶的”?


The Solution 解决方案

All in all, plenty of room to simplify and cleanup. 总而言之,有足够的空间来简化和清理。 In the AST department, 在AST部门,

  • Extend the Value variant with an explicit Null subtype 用显式的Null子类型扩展Value变量
  • Rename types and move into a namespace for readability 重命名类型并进入命名空间以提高可读性
  • Drop the AsNumValue function which has no purpose. 删除没有目的的AsNumValue函数。 Instead, have an IsNull visitor that just reports true for Null values. 取而代之的是一个IsNull访问者,它仅对Null值报告为true
namespace ast {
    typedef double       Number;
    typedef std::wstring String;
    struct               Null {};

    typedef boost::variant<Null, Number, String> Value;

    struct IsNull
    {
        typedef bool result_type;
        template <typename... T>
        constexpr result_type operator()(T const&...) const { return false; }
        constexpr result_type operator()(Null const&) const { return true;  }
    };
}

In the Grammar department, 在语法部门

  • Factor the grammar into rules that match the AST nodes 将语法分解为与AST节点匹配的规则

     qi::rule<It, ast::String()> string_; qi::rule<It, ast::Number()> number_; qi::rule<It, ast::Null()> null_; qi::rule<It, ast::Value(), Skipper> factor_, expr_, function_call_; 
  • This makes your grammar simple to maintain and to reason about: 这使您的语法易于维护和推理:

     string_ = (L'"' > *~char_('"') > L'"') | (L"'" > *~char_("'") > L"'") ; number_ = double_; null_ = no_case["%null%"] > attr(ast::Null()); factor_ = null_ | string_ | double_ | function_call_; expr_ = factor_; BOOST_SPIRIT_DEBUG_NODES((expr_)(factor_)(null_)(string_)(number_)(function_call_)) 
  • Note it makes debug output more informative too 注意,它也使调试输出更加有用

  • I've taken the liberty to rename TakeOne to Coalesce [2] : 我冒昧地将TakeOne重命名为Coalesce [2]

     function_call_ = no_case[L"Coalesce"] > ('(' > expr_ % ',' > ')') [ _val = Coalesce_(_1) ]; 

    This is still using the approach like I showed in the other answer, only, the implementation has become much simpler because there is no longer so much confusion about what could be Null 这仍然使用了我在其他答案中所示的方法,只是,实现变得更加简单,因为不再对可能为Null的内容感到困惑

    Take Away: Values that are Null are just... Null! 拿走: 空值就是...空值!

Removing the now unused header includes, and adding a load of test inputs: 删除现在未使用的标头包括,并添加大量测试输入:

int main()
{
    typedef std::wstring::const_iterator It;
    MapFunctionParser<It, boost::spirit::qi::space_type> parser;

    for (std::wstring input : { 
            L"Coalesce()",
            L"Coalesce('simple')",
            L"CoALesce(99)",
            L"CoalESCe(%null%, 'default')",
            L"coalesce(%null%, -inf)",
            L"COALESCE(%null%, 3e-1)",
            L"Coalesce(%null%, \"3e-1\")",
            L"COALESCE(%null%, 5, 'default')",
            L"COALESCE(%NULL%, %null%, %nuLl%, %NULL%, %null%, %null%, %nUll%, %null%, %NULL%, %nUll%, %null%, \n"
                    L"%NULL%, %NULL%, %null%, %null%, %nUll%, %null%, %nUll%, %nULl%, %null%, %null%, %null%, \n"
                    L"%null%, %nUll%, %NULL%, %null%, %null%, %null%, %null%, %null%, %null%, %nUll%, %nulL%, \n"
                    L"%null%, %null%, %nUll%, %NULL%, 'this is the first nonnull',    %nUll%, %null%, %Null%, \n"
                    L"%NULL%, %null%, %null%, %null%, %NULL%, %null%, %null%, %null%, %NULL%, %NULL%, %nuLl%, \n"
                    L"%null%, %null%, %nUll%, %nuLl%, 'this is the default')",
        })
    {
        It begin(input.begin()), end(input.end());

        ast::Value result;
        bool ret = phrase_parse(begin, end, parser, qi::space, result);

        std::wcout << std::boolalpha << ret << ":\t" << result << std::endl;
    }
}

We can now test the parsing, evaluation and error-handling: 现在,我们可以测试解析,评估和错误处理:

Error! Expecting <list><expr_>"," here: ")"
false:  %null%
true:   simple
true:   99
true:   default
true:   -inf
true:   0.3
true:   3e-1
true:   5
true:   this is the first nonnull

See it Live On Coliru 在Coliru上实时观看

Without the extra test cases it takes about 77 lines of code , less than half of your original code. 如果没有额外的测试用例,则大约需要77行代码 ,不到原始代码的一半


Full Code 完整代码

For future reference 备查

//#define BOOST_SPIRIT_DEBUG
#define BOOST_SPIRIT_USE_PHOENIX_V3
#include <boost/spirit/include/qi.hpp>
#include <boost/spirit/include/phoenix.hpp>
#include <boost/phoenix/function/adapt_function.hpp>

namespace qi  = boost::spirit::qi;
namespace phx = boost::phoenix;

namespace ast {
    typedef double       Number;
    typedef std::wstring String;
    struct  Null { 
        friend std::wostream& operator<<(std::wostream& os, Null) { return os << L"%null%"; } 
        friend std:: ostream& operator<<(std:: ostream& os, Null) { return os <<  "%null%"; } 
    };

    typedef boost::variant<Null, Number, String> Value;

    struct IsNull
    {
        typedef bool result_type;
        template <typename... T>
        constexpr result_type operator()(T const&...) const { return false; }
        constexpr result_type operator()(Null const&) const { return true;  }
    };

    Value Coalesce(std::vector<Value> const& arglist) {
        for (auto& v : arglist)
            if (!boost::apply_visitor(IsNull(), v))
                return v;
        //
        if (arglist.empty())
            return Value(Null());
        else
            return arglist.back(); // last is the default, even if Null
    }
}

BOOST_PHOENIX_ADAPT_FUNCTION(ast::Value, Coalesce_, ast::Coalesce, 1)

template <typename It, typename Skipper = qi::space_type >
struct MapFunctionParser : qi::grammar<It, ast::Value(), Skipper>
{
    MapFunctionParser() : MapFunctionParser::base_type(expr_)
    {
        using namespace qi;

        function_call_ = no_case[L"Coalesce"] 
            > ('(' > expr_ % ',' > ')') [ _val = Coalesce_(_1) ];

        string_ =   
                (L'"' > *~char_('"') > L'"')
            | (L"'" > *~char_("'") > L"'"); 

        number_ = double_;

        null_   = no_case["%null%"] > attr(ast::Null());

        factor_ = null_ | string_ | double_ | function_call_;

        expr_   = factor_;

        on_error<fail> (expr_, std::cout
            << phx::val("Error! Expecting ") << _4 << phx::val(" here: \"")
            << phx::construct<std::string>(_3, _2) << phx::val("\"\n"));

        BOOST_SPIRIT_DEBUG_NODES((expr_)(factor_)(null_)(string_)(number_)(function_call_))
    }

private:
    qi::rule<It, ast::String()>         string_;
    qi::rule<It, ast::Number()>         number_;
    qi::rule<It, ast::Null()>           null_;
    qi::rule<It, ast::Value(), Skipper> factor_, expr_, function_call_;
};

int main()
{
    typedef std::wstring::const_iterator It;
    MapFunctionParser<It, boost::spirit::qi::space_type> parser;

    for (std::wstring input : { 
            L"Coalesce()",
            L"Coalesce('simple')",
            L"CoALesce(99)",
            L"CoalESCe(%null%, 'default')",
            L"coalesce(%null%, -inf)",
            L"COALESCE(%null%, 3e-1)",
            L"Coalesce(%null%, \"3e-1\")",
            L"COALESCE(%null%, 5, 'default')",
            L"COALESCE(%NULL%, %null%, %nuLl%, %NULL%, %null%, %null%, %nUll%, %null%, %NULL%, %nUll%, %null%, \n"
                    L"%NULL%, %NULL%, %null%, %null%, %nUll%, %null%, %nUll%, %nULl%, %null%, %null%, %null%, \n"
                    L"%null%, %nUll%, %NULL%, %null%, %null%, %null%, %null%, %null%, %null%, %nUll%, %nulL%, \n"
                    L"%null%, %null%, %nUll%, %NULL%, 'this is the first nonnull',    %nUll%, %null%, %Null%, \n"
                    L"%NULL%, %null%, %null%, %null%, %NULL%, %null%, %null%, %null%, %NULL%, %NULL%, %nuLl%, \n"
                    L"%null%, %null%, %nUll%, %nuLl%, 'this is the default')",
        })
    {
        It begin(input.begin()), end(input.end());

        ast::Value result;
        bool ret = phrase_parse(begin, end, parser, qi::space, result);

        std::wcout << std::boolalpha << ret << ":\t" << result << std::endl;
    }
}

[1] Origin? [1]起源? http://c2.com/cgi/wiki?CodeSmell (maybe it was Kent Beck?) http://c2.com/cgi/wiki?CodeSmell (也许是肯特·贝克?)

[2] Coalesce referring to corresponding functions in some programming languages [2]在某些编程语言中Coalesce引用相应的功能

Update : I just addressed some other concerns in a second answer . 更新 :我只是在第二个回答中解决了其他一些问题。 Code went down to 77 lines while getting simpler and more robust (and answering your question too). 代码减少到77行,同时变得更加简单和强大(并且也回答了您的问题)。

The direct answer to your Phoenix question would be yes: 您的Phoenix问题的直接答案是:

struct TakeOne
{
    template <typename...> struct result { typedef GenericValue type; };

    template <typename... Args> 
        GenericValue operator()(Args const&... args) const {
            return first(args...);
        }

private:
    GenericValue first(GenericValue const& def) const {
        return def;
    }
    template <typename... Args> GenericValue first(GenericValue const& def, GenericValue const& head, Args const&... tail) const {
        if (CheckIfGenericValIsNull(head)) 
            return first(def, tail...);
        else 
            return head;
    }
};

static const boost::phoenix::function<TakeOne> TakeOne_;

And it would behave largely the same (although you'd need to pass the default as the first argument): 而且它的行为大致相同(尽管您需要将默认值作为第一个参数传递):

function_call_ = no_case[L"TakeOne"] > (
        ('(' > expr_ > ',' > expr_ > ')'                      ) [_val=TakeOne_(_2,_1)]
      | ('(' > expr_ > ',' > expr_ > ',' > expr_ > ')'        ) [_val=TakeOne_(_3,_1,_2)]
      | ('(' > expr_ > ',' > expr_ > ',' > expr_ > expr_ > ')') [_val=TakeOne_(_4,_1,_2,_3)]
      // ... etc
      );

However, as you can see, it's not so flexible! 但是,如您所见,它并不那么灵活! Variadics are likely not what you wanted, because variadics imply statically known numbers of arguments. 可变参数可能不是您想要的,因为可变参数隐含着静态已知的参数数量。 Something that depends on runtime input (being parsed) can never fit into the 'statically known' category. 某些依赖于运行时输入(已解析)的内容永远无法放入“静态已知”类别。 So I'd suggest this: 所以我建议这样做:

    function_call_ = 
        no_case[L"TakeOne"] > 
        ('(' > expr_ % ',' > ')') [_val=TakeOne_(_1)];

So you're passing a std::vector<GenericValue> instead. 因此,您传递的是std::vector<GenericValue> Now, TakeOne becomes a breeze: 现在, TakeOne变得轻而易举:

GenericValue TakeOne(std::vector<GenericValue> const& arglist) {
    assert(!arglist.empty());
    for (auto& v : arglist)
        if (!CheckIfGenericValIsNull(v))
            return v;
    return arglist.back(); // last is the default
}
BOOST_PHOENIX_ADAPT_FUNCTION(GenericValue, TakeOne_, TakeOne, 1)

Bonus: 奖金:

To simplify, here's the visitor re-imagined: 为简化起见,以下是访客的重新构想:

struct IsNull : boost::static_visitor<bool> {
    bool operator()(double num) const { 
        return (num == NumValueDoubleNull);
    }
    bool operator()(std::wstring const& s) const { 
        return boost::iequals(s, ParserNullChar);
    }
};

GenericValue TakeOne(std::vector<GenericValue> const& arglist) {
    assert(!arglist.empty());
    for (auto& v : arglist)
        if (!boost::apply_visitor(IsNull(), v))
            return v;
    return arglist.back(); // last is the default
}
BOOST_PHOENIX_ADAPT_FUNCTION(GenericValue, TakeOne_, TakeOne, 1)

That "fix" alone saves you ~51 LoC (112 vs. 163) . 仅此一个“修复”即可为您节省〜51 LoC(112对163) See it Live on Coliru 在Coliru上实时观看

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM