c++ StrtokNeoを作ったよ。

やあ子供たち。おじさんは古い人間なので、文字列をトークンに分解しようと思った時、どうしてもC言語のstrtokを使いたくなってしまうわけだが、strtokの仕様上、区切り文字が連続できた場合は丸ごと読み飛ばしてしまうということがあり、これがためにCSVを読み込む処理を簡単に書くことができないという何とも不器用な現実があった。(CSVでは空のカラムがあった場合、カンマが連続することがあるからね)。昨今でC++で文字列のトークン分解というと、決まってstringstreamとgetlineを使うのがお決まりとなっているが、例えば10万行、100万行のCSVファイルを、できるだけ、何事もなかったかのような時間で処理したいというケースではやはりそういう手段はとりたくないというケースがあるわけなんだね。
とはいえ、strtokはあまりに不器用だ。ならば大した処理ではないし、作ってしまおうというわけで、StrtokNeoを紹介するぞ。ソースは以下のようだよ。
(VisualStudio 2017, 2019、そして、codepadで、動作を確認済み。codepadの場合はnullptrをNULLにしたり、使用例コードmainの中の各種ラムダを関数化する必要あり。)

//
//  strtok_neo.hpp by @nursmaul
//      [https://nurs.hatenablog.com/entry/2020/07/08/024652]
//
#define  _CRT_SECURE_NO_WARNINGS
#include <iostream>


class StrtokNeo
{
public:
    // aa,,bb,,,0,,
    StrtokNeo(char* line, const char* dlmtrs)
    {
        _q = nullptr;
        _p = line;
        _dlmtrs = new char[strlen(dlmtrs)+1];
        strcpy(_dlmtrs, dlmtrs);
        _tmp='\0';
    }
    ~StrtokNeo(void)
    {
        delete [] _dlmtrs;
    }
    const char* get_token()
    {
        if (_q != nullptr) {
            *_q = _tmp;
        }
        if (_p == nullptr) {
            return nullptr;
        }
        _q = _p;
        const char* result = _p;
        while (!0)
        {
            if (*_q == '\0')
                break;
            bool thats_it = false;
            for( const char* ich = _dlmtrs; *ich!='\0'; ++ich)
            {
                if (*_q == *ich) {
                    thats_it = true;
                    break;
                }
            }
            if (thats_it)
                break;
            ++_q;
        }// while
        if (*_q != '\0')
            _p = _q + 1;
        else
            _p = nullptr;
        _tmp = *_q;
        *_q = '\0';
        return result;
    }
    char* _p;
    char* _dlmtrs;
    char _tmp;
    char* _q;
};

使い方は以下のようだ。

int main(void)
{
    auto StrtokNeoHelper = [](char* ss)
    {
        // 検索対象文字列と、デリミタ文字列とを指定
        StrtokNeo sn(ss, ",+=\"");
        // あとはget_token()を順次呼び出して各トークンを取得。(NULLがかえれば終了)
        while (const char* p = sn.get_token())
            std::cout << "-->" << p << "<--" << std::endl;
        std::cout << std::endl;
    };

    //トークン切り出し結果の出力
    char aa[] = "aaa,+=,COOH+AAA=OH,bb,,do you really think so?\"I doubt that\"";
    auto tak = [](const char* s)
    {
        std::cout << "// "<< s << std::endl;
    };
    tak("解析対象文字列");
    std::cout << aa << std::endl << std::endl;
    tak("StrtokNeo 出力");
    StrtokNeoHelper(aa);
    tak("使用後の対象文字列もインタクトに保持");
    std::cout << aa << std::endl;
    getchar();
    return 0;
}

とっても簡単だね。
これの出力結果は以下のようだよ。

// 解析対象文字列
aaa,+=,COOH+AAA=OH,bb,,do you really think so?"I doubt that"

// StrtokNeo 出力
-->aaa<--
--><--
--><--
--><--
-->COOH<--
-->AAA<--
-->OH<--
-->bb<--
--><--
-->do you really think so?<--
-->I doubt that<--
--><--

// 使用後の対象文字列もインタクトに保持
aaa,+=,COOH+AAA=OH,bb,,do you really think so?"I doubt that"

ま最低限のフィロソフィーだけ述べておくと、

  • StrtokNeoは、strtok同様、デリミタ文字を複数指定することが可能である。
  • StrtokNeoは、strtokのように、絶対内部で変な状態値を持ってんだろみたいな謎な実装ではない。関数ではなく素直にクラスとして実装しており、スレッド安全である。
  • StrtokNeoでは、strtokのように入力の文字列を破壊(デリミタを\0で置換し、もとの文字列を\0の穴だらけにする)したりすることはない。(ただし、非constな文字列を要求する点は変わらない。)
  • StrtokNeoは、strtokとは違い、デリミタが連続していてもそのデリミタ間に「空文字」の存在を明示的にパースして返す。このため植木算を「地で行く」動作仕様となっており、入力文字列を(デリミタの数+1)個のトークンに素直に分解するので、CSVファイル中の空カラムで、カンマが連続して出現するケースなどを高速にパースするのにも最適。
  • StrtokNeoは、iostreamヘッダにしか依存していない。stringすら使っていないため、多くの開発環境でかつ超高速処理が必要なシーンでも迷わず安心して導入することができそうな予感。
  • StrtokNeoのような当たり前に使いやすくて便利なものがC++でこんなに簡単に作れてしまうだのに、ただのC言語から進化して、クラスが定義可能になったC++になったタイミングで何故用意されていないのか、なぜstrtokの壁にぶち当たっただけで、みんなstringstreamやgetlineに流れたり、「C++は難しい」状況を放置してきてしまったのか、そういう不器用さがC#pythonなどに昨今プログラミング言語としての座を明け渡すことになった理由の、あれではないのかと。もっと便利な仕様の関数体系を作ればよかっただけなのではないかと、今までそうしてこなかったんだったらこれからどんどんやればいいじゃんと。。。って止まらなくなりそうなので

今回はこの辺で。
チャオ!