C++でスライス(2回目)

C++ではカンマ演算子オーバーロードできる。前の記事では残念ながらC++に負けてしまったが、今度こそC++でスライスすべくカンマ演算子オーバーロードしたプロキシクラスを書いてC++に打ち勝つことにする。

前に作った失敗作はこいつだ。ここにあるsliceable君は続投してもらい、今回は整数型のプロキシクラスを作っていこうと思う。

ouchiminh.hatenablog.com

追加するコード

整数型のプロキシクラスは何も難しいことをしない。粛々と整数値を格納し、淡々とカンマ演算子オーバーロードするだけだ。カンマ演算子によって二つのintegerオブジェクトはstd::pairにまとめられる。

#include <iostream>
#include <string_view>
#include <algorithm>
#include <type_traits>

template<class T>
bool concept Integral = std::is_integral_v<T>;

template<Integral Int>
struct integer{
    using value_type = std::remove_reference_t<Int>;
    integer(Int i)
        : value {i}
    {}
    value_type value; 
    friend std::pair<integer, integer> operator, (const integer& a, const integer& b) {
        return std::make_pair(a, b);
    }
};

integer<unsigned long long> operator""_i (unsigned long long v){
    return integer<unsigned long long>(v);
}

sliceable君と合体

こんな感じになる。N_iなどと書かなければintegerのカンマ演算子に解決されないのがしんどいが、まあ前よりは簡単になったと信じたい。

wandbox.org

C++でスライスをするのはやはり他の言語よりは記述量が多くなりそうだ。

どうせ誰も見てないので柄でもないことをやっているということを話す。

はてブアクセス解析でどれくらいアクセスがあるのか見ることができる。なんと私のブログはほぼ誰も見ていない!!!!まあ別に広く見てほしくて書いているわけではないので全く問題ない。

どうせ誰も見てないので普段の私からは想像もつかないような変なことをしていることを告白しようと思う。パブリックでやっているので気付いている人もいるかもしれないが、実はC言語の入門みたいなものを書いている。当然誰も見ていないので何のフィードバックも得られない。多少なりともいいものにしたいので是非ともフィードバックが欲しいのだ。

リポジトリはこちら。書きかけなので一緒に書いてくれる人もいてほしい。

github.com

tokenizerとcsvパーサを作った。

いずれ作らなければならないと思っていたものをついに作った。私の大学では2年でC++をやる。C++erの私としては非常にうれしいことである。ただ、大学のPCはBoostが入らない*1ので、自分でBoostのような汎用ライブラリを作る以外に手はない。

知り合いの先輩から入手した情報によると少なくとも2年前からcsvをパースする課題が出ているようだ。これはもちろんboost::tokenizerを使えば一瞬だが、先の理由によりBoostは使えない。仕方ないのでcsvパーサを作ることにしたのだ。

私は1年前からouchilibという名前の汎用ライブラリを作っている。コードの墓場という表現の方が正しいかもしれないが、まあまあ使えるものが多い。今回はそいつにtokenizerとcsvパーサを追加すべくちゃんと書いた。

https://github.com/ouchiminh/ouchilib/tree/master/include/ouchilib/tokenizer

tokenizer

tokenizer君は本題ではないのでパパっと終わらせたい。tokenizer君は文字列をワードとセパレータの二種類に分ける。後から出てくるcsv_parser君はその中からワードだけを取り出すという要領だ。

using namespace ouchi::tokenizer;
separator<char> sep{ ",\t" };  // separatorの登録。','と'\t'をセパレータとして処理する。
tokenizer<char> tok{ "csv,csv,csv", sep };
for(auto[type, str] : tok){
    std::cout << str << '\n';
}
/** output
csv
,
csv
,
csv
**/

最低限の機能しか持たないが、これだけでも非常に役に立つ。

csv_parser

次のcsv_parserも最低限の機能以外は持たない。やることはさっきのtokenizerの出力を見て二次元配列にワードをぶち込むことだけだ。 以下のようなcsvファイルdata.csvがあるとき、

a,b,c,d
,,,target
std::ifstream fs("data.csv");
ouchi::parser::csv<char> pcsv;
pcsv.parse(fs);
std::cout << pcsv.at("d", 1) << '\n';  // 出力はtarget

このようにしてcsvのデータを取得できる。尚、parse()はパースに失敗した場合例外を投げる。

at()には行, 列の順ではなく、x, yの順でキーを指定する。キーは文字列または数値だ。

ここに一つの巨大なcsvファイルがある。気象庁が発表している毎年の地方ごとの梅雨入り, 梅雨明けの日にちの統計データだ。

https://www.data.jma.go.jp/fcd/yoho/baiu/tsuyu_iriake.csv

関東甲信地方の2000年の梅雨入りの月がしりたいときは以下のようなコードになる。

std::ifstream fs("tsuyu_iriake.csv");
ouchi::parser::csv<char> pcsv;
pcsv.parse(fs);

std::cout << pcsv.at(u8"関東甲信地方", u8"2000") << '\n';

*1:学生が使えるストレージ容量は1GBしかない。様々なコンフィグファイルやアドオンなどを差し引いたら数百MBとなり自由に使える分はない

C++でスライスみたいなやつ

Twitterを見ているとTLの者がよく「C++でスライスしてぇ~~~」と言っている。私はたまたまC++が書けるので、なんとなくスライスのようなものを実装してみようと思う。

まずスライスの文法からおさらいする。RustやGoではスライスが言語機能なのでかなり簡便に書ける。例えばlist[..]とかlist[:]みたいに。ではこのことをC++でやろうとすればどのように書けるだろうか? [..]とか[:]という文法はC++ではどう頑張ってもコンパイルエラーになってしまうため、別の手段が必要だ。かといって[0,100]なんて書いてしまえば、[100]と同じ意味になってしまう。何とかしてoperator[]に二つの値を渡したいが、簡便に記述したい。 欲を言えば最初の要素から最後の要素までスライスするなら引数を省略したい。

う~~~~ん、、、正直何も思いつかないがstd::pairを使うのが一番いい気がする。

for(auto& i : list[{ 0, 100 }]

みたいに書ければいいのかなぁ。あんまりきれいではないが、C++ではこれが限界か。

実装

#include <type_traits>
#include <iostream>
#include <vector>

template<class Container>
concept bool Iteratable = requires() {
    typename Container::iterator;
    typename Container::const_iterator;
};

template<Iteratable Container>
class sliceable {
    Container& co;
public:
    sliceable(Container& c)
        : co(c)
    {}
    struct sliced {
        using iterator = Container::iterator;
        iterator first;
        iterator last;
        sliced(iterator first, iterator last)
            : first{ first }
            , last{ last }
        {}
        iterator begin() const { return first; }
        iterator end() const { return last; }
    };

    sliced operator[] (const std::pair<typename Container::size_type, typename Container::size_type>& range) {
        auto second = std::min(co.size(), range.second);
        auto first = std::min(second, range.first);
        return sliced{ std::next(co.begin(), first),
                       std::next(co.begin(), second) };
    }
};

int main() {
    static_assert(Iteratable<std::vector<int>>);
    std::vector<int> v(10, 10);
    sliceable<std::vector<int>> s{v};
    for(auto& i : s[{0,3}])
        std::cout << i << '\n';
}

C++でスライスはやはり若干の無理があるようだ。C++20以降でoperator[]が複数の引数を取れたり、引数を省略できるようになるのを待とう。

C++でforループを短く書く

#define REP(i, n) for(unsigned i = 0; i < (n); ++i)みたいなマクロはとても有名だ。N回のループをとても短く書くことができる。だが、これはあまりにもC++的ではない。nsignedな変数を渡した瞬間コンパイラが警告を出すし.....まあとにかくマクロを使わずに書けるところはマクロを使いたくない。マクロはバグの温床なんだ。

じゃあどうすれば短く書けるのか。そもそも私はどんな風にコードを書きたかったのかを考えると、やはりこういうのがC++的で美しいだろう。

for(auto i : repeat(3)) { }

こんなコードが書ければ万々歳。最高のC++ライフへまた一歩近づくことになる。

実装してみよう!

for(auto i : repeat(3))というのはrepeat tmp(3); for(auto i = tmp.begin(); i != tmp.end(); ++i)の糖衣構文みたいなものだ。実際にはどうなのか知らないが多分そんなものだ。 なのでイテレータっぽいものとbegin endを準備してやればこのコードは動いてくれる。

template<class T>
class repeat {
    static_assert(std::is_integral_v<T>);

    T first_;
    T last_;
public:
    constexpr repeat(T first, T last)
        : first_{ first }
        , last_{ last }
    {}
    constexpr explicit repeat(T count)
        : repeat{ 0, count }
    {}
    struct iterator {
        using difference_type = T;
        using value_type = T;
        using pointer = std::add_pointer_t<T>;
        using reference = std::add_lvalue_reference_t<T>;
        using iterator_category = std::input_iterator_tag;

        T val_;
        constexpr iterator(T val)
            : val_{ val }
        {}
        T operator*() const { return val_; }
        const T* operator->() const noexcept { return &val_; }
        T* operator->() noexcept { return &val_; }

        constexpr friend bool operator<(const iterator& a, const iterator& b) { return a.val_ < b.val_; }
        constexpr friend bool operator==(const iterator& a, const iterator& b) { return !(a < b) && !(b < a); }
        constexpr friend bool operator!=(const iterator& a, const iterator& b) { return !(a == b); }
        const iterator& operator++()
        {
            ++val_;
            return *this;
        }
        iterator operator++(int)
        {
            auto copy = *this;
            ++val_;
            return copy;
        }
    };
    
    constexpr iterator begin() const noexcept { return first_; };
    constexpr iterator end() const noexcept { return last_; }
};

できた。

圧倒的にREPマクロのほうが簡単でコードの量も少ないし多分高速だ。REPマクロを使おう。