热门标签 | HotTags
当前位置:  开发笔记 > 前端 > 正文

详解C++元编程之ParserCombinator

借助C++的constexpr能力,可以轻而易举的构造ParserCombinator,对用户定义的字符串(Userdefinedliteral)释放了巨大的潜力。

引子

前不久在CppCon上看到一个Talk:[constexpr All the things](https://www.youtube.com/watch?v=PJwd4JLYJJY),这个演讲技术令我非常震惊,在编译期解析json字符串,进而提出了编译期构造正则表达式(编译期构建FSM),现场掌声一片,而背后依靠的是C++强大的constexpr特性,从而大大提高了编译期计算威力。

早在C++11的时候就有constexpr特性,那时候约束比较多,只能有一条return语句,能做的事情只有简单的递归实现一些数学、hash函数;而到了C++14的时候这个约束放开了,允许像普通函数那样,进而社区产生了一系列constexpr库;而在C++17,更加泛化了constexpr,允许`if constexpr`来代替元编程的SFINAE手法,STL库的一些算法支持constexpr,甚至连lambda都默认是constexpr的了;到C++20,更加难以想象,居然支持了constexpr new,STL的vector都是constexpr的了,若用constexpr allocator和constexpr destructor,那么就能统一所有constexpr容器了。

借助C++的constexpr能力,可以轻而易举的构造Parser Combinator,实现一个Parser也没那么繁杂了,对用户定义的字符串(User defined literal)释放了巨大的潜力,这也是本文的重点。

什么是Parser

Parser是一个解析器函数,输入一个字符串,输出解析后的类型值集合,函数签名如下:

Parser a:: String -> [(a, String)]

简单起见,这里我们考虑只输出零或一个类型值结果,而不是集合,那么签名如下:

Parser a:: String -> Maybe (a, String)

举个例子,一个数字Parser,解析输入字符串`"123456"`,输出结果为`Just (1, "23456")`,即得到了数字1和剩余字符串`"23456"`,从而可以供下一个Parser使用;若解析失败,输出`None`。

对应C++的函数签名,如下:

// Parser a :: String -> Maybe (a, String)
using ParserInput = std::string_view;
template 
using ParserResult = std::optional>;
template 
using Parser = auto(*)(ParserInput) -> ParserResult;

这就是Parser的定义了。

根据定义可以实现几个最基本的Parser,例如匹配给定的字符:

constexpr auto makeCharParser(char c) {
    // CharParser :: Parser Char
    return [=](ParserInput s) -> ParserResult {
        if (s.empty() || c != s[0]) return std::nullopt;
        return std::make_pair(s[0], ParserInput(s.begin() + 1, s.size() - 1));
    };
};

`makeCharParser`相当于一个工厂,给定字符`c`,创建匹配`c`的Parser。

匹配给定集合中的字符:

constexpr auto oneOf(std::string_view chars) {
    // OneOf :: Parser Char
    return [=](ParserInput s) -> ParserResult {
        if (s.empty() || chars.find(s[0]) == std::string::npos) return std::nullopt;
        return std::make_pair(s[0], ParserInput(s.begin() + 1, s.size() - 1));
    };
}

什么是Parser Combinator

Parser是可组合的最小单元,Parser与Parser之间可以组合成任意复杂的Parser,而Parser Combinator就是一个高阶函数,输入一系列Parser,输出复合后的新Parser。

根据定义,可以实现一个Combinator组合两个Parser,同时根据两个Parser的结果计算出新的结果,从而得到新的Parser:

// combine :: Parser a -> Parser b -> (a -> b -> c) -> Parser c
template, Parser_t>>
constexpr auto combine(P1&& p1, P2&& p2, F&& f) {
    return [=](ParserInput s) -> ParserResult {
        auto r1 = p1(s);
        if (!r1) return std::nullopt;
        auto r2 = p2(r1->second);
        if (!r2) return std::nullopt;
        return std::make_pair(f(r1->first, r2->first), r2->second);
    };
}

由于C++支持操作符重载,那么可以重载一个二元操作符来组合两个Parser,比如从两个Parser里取出其中一个Parser的结果产生新的Parser:

取左边Parser的结果:

// operator> :: Parser a -> Parser b -> Parser a
template
constexpr auto operator>(P1&& p1, P2&& p2) {
    return combine(std::forward(p1),
                   std::forward(p2),
                   [](auto&& l, auto) { return l; });
};

取右边Parser的结果:

// operator<:: Parser a -> Parser b -> Parser b
template
constexpr auto operator<(P1&& p1, P2&& p2) {
    return combine(std::forward(p1),
                   std::forward(p2),
                   [](auto, auto&& r) { return r; });
};

有时候需要对同一个Parser进行多次匹配,类似正则表达式的`*`操作,这个操作可以看做是`fold`,执行多次Parser直到匹配失败,每次结果传递给一个函数运算:

// foldL :: Parser a -> b -> (b -> a -> b) -> ParserInput -> ParserResult b
template
constexpr auto foldL(P&& p, R acc, F&& f, ParserInput in) -> ParserResult {
    while (true) {
        auto r = p(in);
        if (!r) return std::make_pair(acc, in);
        acc = f(acc, r->first);
        in = r->second;
    }
};

有了`fold`函数,那么可以很容易实现函数来匹配任意多次`many`,匹配至少一次`atLeast`:

// many :: Parser a -> Parser monostate
template
constexpr auto many(P&& p) {
    return [p=std::forward

(p)](ParserInput s) -> ParserResult { return detail::FoldL(p, std::monostate{}, [](auto acc, auto) { return acc; }, s); }; }; // atLeast :: Parser a -> b -> (b -> a -> b) -> Parser b template constexpr auto atLeast(P&& p, R&& init, F&& f) { static_assert(std::is_same_v>, R>, "type mismatch!"); return [p=std::forward

(p), f=std::forward(f), init=std::forward(init)](ParserInput s) -> ParserResult { auto r = p(s); if (!r) return std::nullopt; return detail::foldL(p, f(init, r->first), f, r->second); }; };

还有种操作是匹配零到一次,类似于正则表达式的`&#63;`操作,这里我定义为`option`操作:

// option :: Parser a -> a -> Parser a
template>
constexpr auto option(P&& p, R&& defaultV) {
    return [=](ParserInput s) -> ParserResult {
        auto r = p(s);
        if (! r) return make_pair(defaultV, s);
        return r;
    };
};

有了以上基本操作,接下来看看如何运用。

实战

解析数值

项目中模板元编程比较多,而C++17之前模板Dependent type(非类型参数)不支持double,得C++20才支持double,临时方案就是用`template struct NumWrapper {};`模拟double的类型,而需要获取其值的时候,就需要解析字符串了,这些工作应该在编译期确定。

首先是匹配符号`+/-`,若没有符号,则认为是`+`:

constexpr auto sign = Option(OneOf("+-"), '+');

其次是整数部分,也可能没有,若没有,则认为是0:

constexpr auto number = AtLeast(OneOf("1234567890"), 0l, [](long acc, char c) -> long {
    return acc * 10 + (c - '0');
});
constexpr auto integer = Option(number, 0l);

然后是小数点`.`,若没有小数点,为了不丢失精度,则返回一个`long`值。

constexpr auto point = MakeCharParser('.');
// integer
if (! (sign  R {
        return sign == '+' &#63; number : -number;
    })(in);
}

若有小数点,认为是浮点数,返回其`double`值。

// floating
constexpr auto decimal = point  double {
    double d = 0.0;
    while (decimal) {
        d = (d + (decimal % 10)) * 0.1;
        decimal /= 10;
    }
    return integer + d;
});
return Combine(sign, value, [](char sign, double d) -> R { return sign == '+' &#63; d : -d; })(in);
```
由于该Parser可能返回`long`或者`double`类型,所以可以统一成和类型`std::variant`:
```cpp
constexpr auto ParseNum() {
    using R = std::variant;
    return [](ParserInput in) -> ParserResult {
        // ...
    };
}

最后我们的`NumWrapper`实现如下,从而可以混入模板类型体系:

template
constexpr std::array ToArr = {Cs...};
template
class NumberWrapper {
public:
    constexpr static auto numStr = ToArr;
    constexpr static auto res = ParseNum()(std::string_view(numStr.begin(), numStr.size()));
    static_assert(res.has_value() && res->second.empty(), "parse failed!");
public:
    constexpr static auto value = std::getfirst.index()>(res->first); // long or double
}

如果仅仅是用于解析数字,那也杀鸡用牛刀了,因为在`Parser Combinator`之前的版本,我就是在一个普通的`constexpr`函数中完成解析的,代码很无趣,但现在我可能想回退代码了。

Json解析导读

这次的CppCon主题是编译期解析`json`字符串,当然直接用`string_view`承载字符串即可。然后构造一些constexpr容器,例如固定长度的constexpr vector,由于是17年的talk了,在还不支持constexpr new的情况下,只能这么做。有了constexpr vector,进而可以构造map容器,也是很简单的pair vector集合。

进而提出Parser Combinator,解析字符串,`fmap`到json数据结构中。

最初实现的时候,json数据结构也是一个大的`template struct Json_Value;`模板承载,导致只能指定最大递归层数,那就不够实用了。然后talker想了个很巧妙的办法去掉层数约束,就是先递归`sizes()`扫描一遍,计算出所有值个数,这样就能确定需要多少个`Value`容器来存储,其次计算出字符串长度,由于`UTF8`、转义字符串的影响,最终要解析的长度其实是可能小于输入长度的。有了确定空间后,进行第二遍递归`value_recur::value_parser()`扫描,每次解析完整值时候填一下`Value`数据结构。而由于数组和对象类似,可能嵌套,这时候进行第三遍递归`extent_recur<>::value_parser()`扫描,做一次宽度优先搜索,确定最外层的元素个数,从而依次解析填值。

以上就是详解C++元编程之Parser Combinator的详细内容,更多关于C++元编程之Parser Combinator的资料请关注其它相关文章!


推荐阅读
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • 本文介绍了Java的集合及其实现类,包括数据结构、抽象类和具体实现类的关系,详细介绍了List接口及其实现类ArrayList的基本操作和特点。文章通过提供相关参考文档和链接,帮助读者更好地理解和使用Java的集合类。 ... [详细]
  • 使用nodejs爬取b站番剧数据,计算最佳追番推荐
    本文介绍了如何使用nodejs爬取b站番剧数据,并通过计算得出最佳追番推荐。通过调用相关接口获取番剧数据和评分数据,以及使用相应的算法进行计算。该方法可以帮助用户找到适合自己的番剧进行观看。 ... [详细]
  • 本文介绍了Java工具类库Hutool,该工具包封装了对文件、流、加密解密、转码、正则、线程、XML等JDK方法的封装,并提供了各种Util工具类。同时,还介绍了Hutool的组件,包括动态代理、布隆过滤、缓存、定时任务等功能。该工具包可以简化Java代码,提高开发效率。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • t-io 2.0.0发布-法网天眼第一版的回顾和更新说明
    本文回顾了t-io 1.x版本的工程结构和性能数据,并介绍了t-io在码云上的成绩和用户反馈。同时,还提到了@openSeLi同学发布的t-io 30W长连接并发压力测试报告。最后,详细介绍了t-io 2.0.0版本的更新内容,包括更简洁的使用方式和内置的httpsession功能。 ... [详细]
  • 本文介绍了Hyperledger Fabric外部链码构建与运行的相关知识,包括在Hyperledger Fabric 2.0版本之前链码构建和运行的困难性,外部构建模式的实现原理以及外部构建和运行API的使用方法。通过本文的介绍,读者可以了解到如何利用外部构建和运行的方式来实现链码的构建和运行,并且不再受限于特定的语言和部署环境。 ... [详细]
  • 使用在线工具jsonschema2pojo根据json生成java对象
    本文介绍了使用在线工具jsonschema2pojo根据json生成java对象的方法。通过该工具,用户只需将json字符串复制到输入框中,即可自动将其转换成java对象。该工具还能解析列表式的json数据,并将嵌套在内层的对象也解析出来。本文以请求github的api为例,展示了使用该工具的步骤和效果。 ... [详细]
  • 本文介绍了PhysioNet网站提供的生理信号处理工具箱WFDB Toolbox for Matlab的安装和使用方法。通过下载并添加到Matlab路径中或直接在Matlab中输入相关内容,即可完成安装。该工具箱提供了一系列函数,可以方便地处理生理信号数据。详细的安装和使用方法可以参考本文内容。 ... [详细]
  • 无损压缩算法专题——LZSS算法实现
    本文介绍了基于无损压缩算法专题的LZSS算法实现。通过Python和C两种语言的代码实现了对任意文件的压缩和解压功能。详细介绍了LZSS算法的原理和实现过程,以及代码中的注释。 ... [详细]
  • 本文介绍了数模国赛的报名参加方法,包括学校报名和自己报名的途径。同时给出了建模竞赛的建议,重在历练的同时掌握方法以及弥补自己的短板。此外,还分享了论文的结构和模型求解部分的注意事项,包括数学命题的表述规范和计算方法的原理等。 ... [详细]
  • 本文介绍了高校天文共享平台的开发过程中的思考和规划。该平台旨在为高校学生提供天象预报、科普知识、观测活动、图片分享等功能。文章分析了项目的技术栈选择、网站前端布局、业务流程、数据库结构等方面,并总结了项目存在的问题,如前后端未分离、代码混乱等。作者表示希望通过记录和规划,能够理清思路,进一步完善该平台。 ... [详细]
  • 单点登录原理及实现方案详解
    本文详细介绍了单点登录的原理及实现方案,其中包括共享Session的方式,以及基于Redis的Session共享方案。同时,还分享了作者在应用环境中所遇到的问题和经验,希望对读者有所帮助。 ... [详细]
  • 使用正则表达式爬取36Kr网站首页新闻的操作步骤和代码示例
    本文介绍了使用正则表达式来爬取36Kr网站首页所有新闻的操作步骤和代码示例。通过访问网站、查找关键词、编写代码等步骤,可以获取到网站首页的新闻数据。代码示例使用Python编写,并使用正则表达式来提取所需的数据。详细的操作步骤和代码示例可以参考本文内容。 ... [详细]
  • 本文介绍了前端人员必须知道的三个问题,即前端都做哪些事、前端都需要哪些技术,以及前端的发展阶段。初级阶段包括HTML、CSS、JavaScript和jQuery的基础知识。进阶阶段涵盖了面向对象编程、响应式设计、Ajax、HTML5等新兴技术。高级阶段包括架构基础、模块化开发、预编译和前沿规范等内容。此外,还介绍了一些后端服务,如Node.js。 ... [详细]
author-avatar
猪猪情系qq
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有