提升凤凰可变参数函数解析

时间:2013-11-05 08:47:43

标签: c++ boost-spirit variadic-functions

对于函数“TakeOne”,我有一个解析器代码如下所示。 TakeOne函数就像返回第一个不等于'%null%'的参数一样工作 例如:

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

现在我想将此功能修改为

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

是否可以在不使用C ++ 11功能的情况下执行此操作? 感谢

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

2 个答案:

答案 0 :(得分:1)

这是解决(可能是)你可能试图解决的XY问题的第二个答案。

正如我在评论中所指出的,您的示例中有一些code smells [1] 。让我解释一下我的意思。


目标

让我们考虑一下该计划的目标是什么:您正在进行输入解析

解析的结果应该是数据,最好是具有强类型信息的C ++数据类型,因此您可以避免使用奇怪的(可能无效的)可变文本表示并专注于业务逻辑。


闻起来

现在闻到了气味:

  • 您定义&#34;抽象数据类型&#34; (比如NumValue)但是你不能始终如一地使用它们:

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

    更加一致,并使您的代码反映 design

    namespace ast {
        typedef double                         Number;
        typedef std::wstring                   String;
        typedef boost::variant<Number, String> Value;
    }
    
  • 您使用解析器生成器进行解析, 您还在调用

    • boost::lexical_cast<double> on ... strings
    • wostringstream,您(忘记std::ios::skipws ...)提取&#34; a&#34;串
    • boost::iequals比较字符串,这些字符串应该已经被解析为强类型的AST类型,与letter-case无关。
    • 您有static_visitor对变体类型采取行动, 依赖于字符串化(使用wostringstream)。事实上,您只是在该变种 iff 上呼叫访问者,您已经知道它是一个数字:

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

      这有点好笑,因为在这种情况下,您可以使用boost::get<NumValue>(val)来获取已知类型的值。

      

    专业提示:使用&#34;低级&#34;使用高级解析器生成器解析/流操作时,代码异味

  • 您的通用值变体表明您的语法支持 两个 类型的值。然而,您的语法定义清楚地表明您有第三种类型的值:%null%

    有证据表明你自己有点困惑,因为我们可以看到解析器

    • 将文字%null%(或%NULL%等)解析为...某种神奇的数字。
    • 因此,我们知道iff %null%已解析, 始终 在您的AST中为NumValue
    • 我们也知道字符串将 始终 解析为wstring子类型GenericValue
    • 尚未 ,我们可以看到您将任何GenericValue视为可能为空

    总而言之,这导致了一个相当令人惊讶的......

      

    摘要:您(具有讽刺意味)似乎正在使用AsNumValue来确定String是否可能实际为Null

    提示:String永远无法代表%null%开头,将随机字符串转换为数字和随机的魔术数值&#39;是没有意义的。不应该首先用来代表Null

  • 你的语法不平衡地使用语义动作

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

    我们注意到您同时

    • 使用SA手动执行自动属性传播应该执行的操作([_val = _1]
    • 使用单个分支进行&#34;魔术&#34;目的(这是您需要Null AST数据类型的地方)

    在我建议的解决方案中,规则变为:

    factor_ = null_ | string_ | double_ | function_call_;
    

    那就是它。

      

    专业提示:谨慎使用语义操作(另请参阅Boost Spirit: "Semantic actions are evil"?


解决方案

总而言之,有足够的空间来简化和清理。在AST部门,

  • 使用明确的Null子类型
  • 扩展Value变体
  • 重命名类型并移入命名空间以提高可读性
  • 删除无用的AsNumValue函数。相反,让IsNull访问者仅针对true值报告Null
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;  }
    };
}

在语法部门,

  • 将语法分解为与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_;
    
  • 这使您的语法易于维护和推理:

    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_))
    
  • 注意它也使调试输出更具信息性

  • 我冒昧地将TakeOne重命名为Coalesce [2]

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

    这仍然使用我在其他答案中所展示的方法,只是,实现变得更加简单,因为对于什么可能是Null已经不再那么混乱了

      

    Take Away: Null的值只是...... Null!

删除现在未使用的标头包含,并添加一组测试输入:

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

我们现在可以测试解析,评估和错误处理:

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

查看 Live On Coliru

如果没有额外的测试用例,则需要 about 77 lines of code ,小于原始代码的一半


完整代码

供将来参考

//#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] 原产地? http://c2.com/cgi/wiki?CodeSmell(也许是Kent Beck?)

[2] Coalesce引用某些编程语言中的相应函数

答案 1 :(得分:0)

  

更新:我刚刚解决了其他一些问题in a second answer。代码下降到77 lines,同时变得更简单,更健壮(并回答您的问题)。

对你凤凰问题的直接回答是肯定的:

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

它的行为基本相同(尽管您需要将默认值作为第一个参数传递):

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

然而,正如你所看到的,它不是那么灵活!变体可能你想要什么,因为变量法意味着静态已知的参数数量。依赖于运行时输入(被解析)的东西永远不能适合“静态已知”类别。所以我建议:

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

所以你传递的是std::vector<GenericValue>。现在,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)

加成:

为了简化,以下是访客重新设想的内容:

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)

单独“修复” 可以节省~51 LoC(112 vs. 163) 。见 Live on Coliru