2015-01-20

C++の正規表現ライブラリ: std::regex

いまさらながら、C++の正規表現ライブラリを調べている。

C++の正規表現ライブラリ、std::regexは、boost::regexを土台に設計されている。boost::regexの正規表現の文法は、perlなのに対し、std::regexは、ECMAScriptである。この理由は、しっかりと正規表現の文法が定義されていて、外部規格として参照できる品質のものが、perlには存在しないためだ。std::regexはposixと拡張posixとawkとgrepとegrepの正規表現にも対応している。

本記事では、ECMAScriptの正規表現を使う。また、参考のためのECMAScriptのコードも使う。

全体一致

文字列全体が正規表現に一致するかどうかを調べたいとする

var re = /1234/ ;
var text = "1234" ;

var result = re.test( text ) ;

これを、std::regexを使って書くと以下のようになる。

#include <regex>

int main()
{
    std::regex re("1234") ;
    std::string text("1234") ;

    bool result = std::regex_match( text, re ) ;
}

C++には、正規表現リテラルはないので、文字列リテラルで正規表現を記述する。

もうすこし正規表現らしいことをしてみよう。連続した数字に一致する正規表現を使ってみる。

var re = /\d+/ ;
var text = "1234" ;

var result = re.test( text ) ;

残念ながら、C++の文字列リテラルで/\d+/を書こうとすると、エスケープシーケンスのためにエラーになる。

std::regex r1 = "\d+" ; // エラー
std::regex r2 = "\\d+" ; // OK

"\\d+"と書けば動くのだが、これはとても書きづらい。C++11で追加された生文字列リテラルを使えば、自然に書けるようになる。

std::regex re = R"(\d+)" ;

外側の括弧は生文字列リテラルの文法なので紛らわしいが、バックスラッシュを二重に書くよりはマシだろう。

部分一致の検索

以下のようなECMAScriptのコードをC++で書きたいとする

var re = /\d+/ ;
var text = "There are 99 bottles." ;

var a = re.exec( text ) ;
var result = a[0] ; // "99"

C++では、以下のように書ける。

int main()
{
    std::regex re( R"(\d+)" ) ;
    std::string text="There are 99 bottles." ;

    std::smatch m ; // std::match_results<string::const_iterator>
    std::regex_search( text, m, re ) ;

    std::cout << m.str() << std::endl ;
}

部分一致を検索するには、regex_searchを使う。結果は、match_resultsで受け取ることができる。

部分一致をすべて検索。

以下は、部分一致をすべて検索するECMAScriptのコードである。

var re = /\d+/g ;
var text = "123 456 789" ;

// ["123", "456", "789"]
var result = text.match( re ) ;

C++では、イテレーターを使ってregex_searchを繰り返し呼び出すことで書くことができる。

std::vector< std::string >
search( std::string const & text, std::regex const & re )
{
    std::vector< std::string > result ;

    auto iter = text.cbegin() ;
    auto end = text.cend() ;

    std::smatch m ;

    while ( std::regex_search( iter, end, m, re ) )
    {
        result.push_back( m.str() ) ;
        iter = m[0].second ;
    }

    return result ;
}

int main()
{
    std::regex re( R"(\d+)" ) ;
    std::string text="123 456 789" ;

    auto result = search( text, re ) ;

    for ( auto & elem : result )
    {
        std::cout << elem << std::endl ;
    }
}

とはいえ、これは面倒だ。

C++には、このように部分一致を複数見つける用途に、regex_iteratorを用意している。これを使えば、以下のように書くことができる。

std::vector< std::string >
search( std::string const & text, std::regex const & re )
{
    std::vector< std::string > result ;

    std::sregex_iterator iter( text.cbegin(), text.cend(), re ) ;
    std::sregex_iterator end ;

    for ( ; iter != end ; ++iter )
    {
        result.push_back( iter->str() ) ;
    }

    return result ;
}

置換

以下のように、DogをCatに置き換えたいとする。

var re = /Dog/g ;
var text = "Dog is nice. Dog is cute. Dog is awesome." ; 

// "Cat is nice. Cat is cute. Cat is awesome."
var result = text.replace( re, "Cat" ) ;

C++では、regex_replaceを使って書くことができる。


int main()
{
    std::regex re( R"(Dog)" ) ;
    std::string text("Dog is nice. Dog is cute. Dog is awesome.") ;

    // "Cat is nice. Cat is cute. Cat is awesome."
    auto result = std::regex_replace( text, re, "Cat" ) ;
}

以下のように最初の一致だけを書き換えたい場合、

var re = /Dog/ ;
var text = "Dog is better than dog." ; 

// "Cat is better than dog."
var result = text.replace( re, "Cat" ) ;

以下のように書くことができる。

int main()
{
    std::regex re( R"(Dog)" ) ;
    std::string text="Dog is better than dog." ;

    // "Cat is better than dog."
    auto result = std::regex_replace( text, re, "Cat",
        std::regex_constants::format_first_only ) ;
}

置換はECMAScriptの文法と同じだ。

// ECMAScript
var text = "123 456"
var re = /(\d+) (\d+)/ ;

// "456 123"
var result = text.replace( re, "$2 $1" ) ;
// C++
std::string text("123 456") ;
std::regex re(R"((\d+) (\d+))") ;

// "456 123"
auto result = std::regex_replace( text, re, "$2 $1" ) ;

サブマッチ

正規表現は括弧でキャプチャをすることができる。

var re = /(\d+) (\d+) (\d+)/ ;
var text = "123 456 789" ;

// ["123 456 789", "123", "456", "789"]
var result = text.match( re ) ;

C++では、match_resultsから得られるsub_matchをたどることで、同等のことができる。


std::vector< std::string >
match( std::string const & text, std::regex const & re )
{
    std::vector< std::string > result ;

    std::smatch m ; // match_results

    std::regex_match( text, m, re ) ;

    for ( auto && elem : m )
    {// elemはsub_match
        result.push_back( elem.str() ) ;
    }

    return result ;
}

int main()
{
    std::regex re( R"((\d+) (\d+) (\d+))" ) ;
    std::string text="123 456 789" ;

    // {"123 456 789", "123", "456", "789"}
    auto result = match( text, re ) ;

    for ( auto && elem : result )
    {
        std::cout << elem << std::endl ;
    }
}

std::regexを一通り触ってみた感想としては、Boost.Regexのうち、規格として定義できる部分だけ定義したという感じだ。

std::regexと現行の主要なライブラリ(libstdc++とlibc++)の実装を調べると、ASCII文字か、あるいは単なるバイト列に対して使うことが出来る正規表現でしかなかった。charとwchar_tには対応している。ただし、ASCII文字以外には対応していない。UTF-8を使えばバイト列として一致させることはできた。つまり、

// 通常の文字列リテラルのエンコードはUTF-8の環境
std::regex re(R"(いろは にほへ)") ;
std::string text("いろは にほへ") ;

std::regex_match( text, re ) ;

これは動く。ただし、wchar_tは動かない。

std::wregex re(R"(いろは にほへ)") ;
std::wstring text(L"いろは にほへ") ;

// 動かない
std::regex_match( text, re ) ;

UTF-8は単なるバイト列だと考えるしかない。例えば以下は動かない。

// 通常の文字列リテラルのエンコードはUTF-8の環境
std::regex re(R"(.)") ;
std::string text("あ") ;

// 動かない
std::regex_match( text, re ) ;

char16_t、char32_tは、libstdc++とlibc++では、コンパイルすら通らない。

というわけで、現状の規格と実装状況から言えば、ASCII文字だけに限定していいのならば、std::regexとstd::wregexは使える。Unicodeが扱いたければ、規格外の正規表現ライブラリを使うべきだ。

ドワンゴ広告

そろそろC++のブログ記事を増やしていきたい。

ドワンゴは本物のC++プログラマーを募集しています。

採用情報|株式会社ドワンゴ

CC BY-ND 4.0: Creative Commons — Attribution-NoDerivatives 4.0 International — CC BY-ND 4.0

3 comments:

Anonymous said...

正規表現は覚えたいと思っているのですが、どうにも複雑ですね。
正規表現をライブラリに入れる前にユニコードストリングの整備が先だったんじゃないでしょうか。
自分も覚えて簡易パーサくらいは書けるようになりたいです。

Anonymous said...

> 文字列全体が正規表現に一致するかどうか

これの ECMAScript 側は

/^1234$/

だと思う。

Unknown said...

wchar_t 版は wregex の引数が違いますね。
re(LR"(いろは にほへ)")
です。
ちゃんと動きましたよ。
http://melpon.org/wandbox/permlink/ENiq5Bt2ZWk8i2Bo
Linux + gcc or clang であれば、wchar_t は UTF-32 だと思うので、そこそこ使えると思います。