TPL: 一個新的正則表達式(regex)庫
許式偉
2008-5-29
概要
C++ 中正則表達式(regex)庫已經很多。光 boost 中就有3個:regex、spirit、xpressive。那麼我們爲什麼還需要一個新的呢?
多數正則表達式庫都需要一個編譯(compile)過程。即:通過解釋一個正則表達式的字符串(pattern)來生成該正則表達式的內部表示(字節碼)。例如 boost regex 就是這樣。這類我們稱之爲動態正則表達式庫。
spirit、xpressive 例外。他們直接通過重載 C++ 的操作符來表達一個正則表達式。在你用C++語法描述完一個正則表達式,它已經是內部表示(被C++編譯器編譯成了機器碼)。這一類我們稱之爲靜態正則表達式庫。
靜態正則表達式庫的好處主要有二:
- 性能好。由於匹配代碼直接編譯成爲了機器碼,故此通常性能會好過動態的正則表達式。
- 與 C++ 語言可形成良好的互動。可以非常容易在正則表達式中獲得執行C++代碼的時機。
缺點:
- 正則表達式必須在編譯期確定。如果你希望用戶可以輸入一個正則表達式,那麼靜態正則表達式庫不能直接滿足你的需求。
TPL 屬於靜態正則表達式庫。本文也不準備討論動態正則表達式。需要指出,xpressive 既支持動態正則表達式,也支持靜態的正則表達式,但是我們並不考慮其動態正則表達式部分。
TPL 全稱爲 Text Processing Library(文本處理庫)。spirit、xpressive 是很好的東西,實現 TPL 庫中對這兩者有所借鑑。
說起來開發 TPL 庫的理由看起來挺好笑的:原因是 spirit、xpressive 太慢。不是執行慢,而是編譯慢。我的機器算起來也不算差,但是每次修改一點點代碼,編譯過程都等待半天,實在受不了這樣的開發效率。
從機理上講,TPL 並無特別讓人振奮之處。該有的 spirit、xpressive 相信都有了。三者都基於“表達式模板(Expression Templates)” 這樣的技術。
閒話少說,這裏給幾個實際的樣例讓大家感受下:
樣例一:識別以空格分隔的浮點數並放入vector中
代碼:tpl/test/testtpl/Simplest.cpp
#include <vector>
#include <tpl/RegExp.h>
using namespace tpl;
// What we use:
// * Rules: /assign(), %, real(), ws()
// * Matching: tpl::simple::match()
void simplest()
{
std::vector<double> values; // you can change vector to other stl containers.
if ( simple::match(
"-.1 -0.1 +32. -22323.2e+12",
real()/assign(values) % ws()) )
{
for (
std::vector<double>::iterator it = values.begin();
it != values.end(); ++it)
{
std::cout << *it << "/n";
}
}
}
輸出:
-0.1
-0.1
-32
-2.23232e+016
解釋:
以上代碼我相信比較難以理解的是 / 和 % 算符。
/ 符號我稱之爲“約束”或“動作”。它是在一個規則(Rule)匹配成功後執行的額外操作。這個額外的操作可能是:
- 使用另一個Rule進行進一步的數據合法性檢查。
- 賦值(本例就是)。
- 打印調試信息(正則表達式匹配比較難以跟蹤,故此 Debug 能力也是 TPL 的一個關注點)。
- 其他用戶自定義動作。
% 符號是列表算符(非常有用)。A % B 等價於 A (B A)* 這樣的正則表達式。可匹配 ABABAB..A 這樣的串。一個典型案例是用它匹配函數參數列表。
樣例二:識別以逗號分隔的浮點數並放入vector中
代碼:tpl/test/testtpl/SimpleGrammar.cpp
// A simple grammar example.
// What we use:
// * Rules: /assign(), %, real(), gr(','), skipws()
// * Matching: tpl::simple::match()
void simple_grammar()
{
simple::Allocator alloc;
std::vector<double> values; // you can change vector to other stl containers.
if ( simple::match(
" -.1 , -0.1 , +32. , -22323.2e+12 ",
real()/assign(values) % gr(','), skipws(), alloc) )
{
for (
std::vector<double>::iterator it = values.begin();
it != values.end(); ++it)
{
std::cout << *it << "/n";
}
}
}
輸出:與樣例一相同。
解釋:儘管看起來好像沒有發生太大的變化。但是這兩個樣例本質上是不同的。主要體現在:
- 正則表達式的類型不同。real()/assign(values) % ws() 是一個Rule。而 real()/assign(values) % gr(',') 是一個 Grammar。簡單來說,Rule 可以認爲是詞法級別的東西。Grammar 是語法級別的東西。Grammar 的特點在於,它匹配一個語法單元前,總會先調用一個名爲Skipper的特殊Rule。上例中 Skipper 爲 skipws()。
- 兩個 match 的原型不同。第一個match的原型是:match(Source, Rule), 第二個match的原型是:match(Source, Grammar, Skipper, Allocator)。
第二個例子如果用 Rule 而不是用 Grammar 寫,看起來是這樣的:
if ( simple::match(
" -.1 , -0.1 , +32. , -22323.2e+12 ",
(skipws() + real()/assign(values)) % (skipws() + ',')) ) ...
你可能認爲這並不複雜。單對這個例子而言,確實看起來如此。但是如果你這樣想,不妨用 Rule 做下下面這個例子。
樣例三:運算器(Calculator)
功能:可處理+-*/四則運算、()、函數調用(sin, cos, pow)。代碼:tpl/test/testtpl/Calculator2.cpp (呵呵,只有60行代碼哦!)
#include <stack>
#include <tpl/RegExp.h>
#include <tpl/ext/Calculator.h>
#include <cmath>
using namespace tpl;
void calculate2()
{
typedef SimpleImplementation impl;
// ---- define rules ----
impl::Allocator alloc;
std::stack<double> stk;
impl::Grammar::Var rFactor;
impl::Grammar rMul( alloc, '*' + rFactor/calc<std::multiplies>(stk) );
impl::Grammar rDiv( alloc, '/' + rFactor/calc<std::divides>(stk) );
impl::Grammar rTerm( alloc, rFactor + *(rMul | rDiv) );
impl::Grammar rAdd( alloc, '+' + rTerm/calc<std::plus>(stk) );
impl::Grammar rSub( alloc, '-' + rTerm/calc<std::minus>(stk) );
impl::Grammar rExpr( alloc, rTerm + *(rAdd | rSub) );
impl::Rule rFun( alloc,
"sin"/calc(stk, sin) | "cos"/calc(stk, cos) | "pow"/calc(stk, pow) );
rFactor.assign( alloc,
real()/assign(stk) |
'-' + rFactor/calc<std::negate>(stk) |
'(' + rExpr + ')' |
(gr(c_symbol()) + '(' + rExpr % ',' + ')')/(gr(rFun) + '(') |
'+' + rFactor );
// ---- do match ----
for (;;)
{
std::string strExp;
std::cout << "input an expression (q to quit): ";
if (!std::getline(std::cin, strExp) || strExp == "q") {
std::cout << '/n';
break;
}
try {
while ( !stk.empty() )
stk.pop();
if ( !impl::match(strExp.c_str(), rExpr + eos(), skipws(), alloc) )
std::cout << ">>> ERROR: invalid expression!/n";
else
std::cout << stk.top() << "/n";
}
catch (const std::logic_error& e) {
std::cout << ">>> ERROR: " << e.what() << "/n";
}
}
}
// -------------------------------------------------------------------------
解釋:
- Grammar::Var 用於定義一個未賦值即被引用的Grammar。相應地,我們也有 Rule::Var。
- gr(Rule) 是將一個 Rule 轉換爲 Grammar。
- SimpleImplementation 是什麼?嗯,這個下回聊。
- <tpl/ext/Calculator.h> 並不屬於 tpl regex 庫。代碼也不多。參見:tpl/ext/Calculator.h