1

我有一个如下的解析器代码,用于函数“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;
}
4

2 回答 2

1

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

正如我在评论中指出的那样,您的示例中有许多代码异味[1]。让我解释一下我的意思。


目标

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

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


气味

现在谈谈气味:

  • 您定义了“抽象数据类型”(如NumValue),但您未能始终如一地使用它们:

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

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

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

    • boost::lexical_cast<double>在 ... 字符串上
    • wostringstream,您(忘记std::ios::skipws...)从中提取“a”字符串
    • boost::iequals比较字符串,应该已经被解析为强类型的 AST 类型,独立于字母大小写。
    • 您必须static_visitor对变体类型采取行动,您依赖于字符串化(使用wostringstream)。事实上,只有当你已经知道它是一个数字时,你才会在那个变体上调用访问者

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

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

    专业提示:在使用高级解析器生成器的同时使用“低级”解析/流式操作是一种代码异味

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

    有证据表明您自己对此感到有些困惑,因为我们可以看到解析器

    • %null%将文字(或等)解析%NULL%为……某种幻数。
    • 因此,我们知道 iff%null%已被解析,它在您的 AST 中始终是 aNumValue
    • 我们也知道字符串总是会被解析成一个wstring子类型GenericValue
    • 但是,我们可以看到您将任何GenericValue视为可能为空

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

    摘要:您拥有AsNumValue(具有讽刺意味的是)您似乎正在使用它来确定 aString是否实际上是Null

    提示:aString永远不能代表a %null%,将随机字符串转换为数字是没有意义的,并且一开始就不应该使用随机的“神奇数值”来表示Null

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

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

    我们注意到您同时

    • 使用 SA 手动执行应该执行的自动属性传播 ( [_val = _1])
    • 将单个分支用于“魔术”目的(这是您需要NullAST 数据类型的地方)

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

    factor_ = null_ | string_ | double_ | function_call_;
    

    而已。

    专业提示:谨慎使用语义动作(另请参阅Boost Spirit:“语义动作是邪恶的”?


解决方案

总而言之,有足够的空间来简化和清理。在 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_))
    
  • 请注意,它也使调试输出提供更多信息

  • 我冒昧地重命名TakeOneCoalesce[2]

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

    这仍然使用我在另一个答案中展示的方法,只是,实现变得更加简单,因为不再有太多关于什么可能是 Null 的混淆

    带走: 为 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

在Coliru现场观看

如果没有额外的测试用例,大约需要 77 行代码,不到原始代码的一半


完整代码

备查

//#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(也许是肯特贝克?)

[2] Coalesce指一些编程语言中的对应函数

于 2013-11-05T21:41:07.083 回答
0

更新:我刚刚在第二个答案中解决了其他一些问题。代码减少到77 行,同时变得更简单、更健壮(并且也回答了您的问题)。

您的凤凰问题的直接答案是肯定的:

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

所以你正在传递 a 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)在 Coliru 上看到它

于 2013-11-05T15:47:34.850 回答