タイトルの通り。
イディオムと呼ぶのか怪しい物やイディオムですらないものも含む。
行単位での文字読み込みが捗るイディオム
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <iostream> #include <string> #include <vector> int main(int argc, char** argv) { constexpr std::size_t size {42}; std::vector<std::string> input; for (std::string buffer; std::getline(std::cin, buffer) && input.size() < size; input.push_back(buffer)); return 0; } |
ただ読み取っているだけなのだが、for文は std::size_t か std::vector をイテレータでぶん回すためのものとして使っていた俺にとっては「あ、 std::getline 用のバッファはfor文の頭で書いたほうがスッキリするじゃん」とか、「for文のケツはループが終わり毎に実行されるんだからここで push_backすれば良いのか」とか、いろいろと感動だったのだ。
Cで書くときは何かとfor文の条件式部分で悪さをするのが大好きなのだが、頭やケツでも悪さが出来ると知って喜んだものだ(ワンライナー好き好き大好きマン)。
また、
std::getline() は入力ストリームへの参照を返すが
std::basic_istreamにはbool型へのキャスト演算子が定義されており、これにより入力ストリームの状態を知ることが出来る。
そのため、次のように書くことで、ファイルを行単位でベクタに分解しての読み取りが行える。
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <iostream> #include <string> #include <vector> int main(int argc, char** argv) { std::vector<std::string> input; for (std::string buffer; !std::getline(std::cin, buffer).eof(); input.push_back(buffer)); return 0; } |
標準入力の読み捨て
1 2 3 4 5 6 7 8 9 |
#include <iostream> #include <limits> int main(int argc, char** argv) { std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); return 0; } |
Cからプログラミングに入った俺はこういう場面に出くわした時に「whileで std::getchar() == '\n' になるまで何もせずループすっか」と思った。そこで「はて、じゃあこいつってC++らしいカッケェ書き方ないのかな」と調べて見つけたのがこいつ。
イディオムですらないがどうせ忘れるので書いておく。
std::cin が std::istream のインスタンスであることは知っていたものの、シフト演算子(この演算子オーバーロードはシフトの意味を失っているからあまり好きではない。かといって代替案が浮かばないので文句も言えないが)で標準入力を引っ張りこむことにしか使ってこなかったため、 std::cinのメンバ関数にとんと無頓着だったのだ。 std::basic_streambuf の拡張をしたり、シフト演算子をオーバーロードして標準出力に吐くようなクラスを作ったりしていたというのにこいつの存在を知ったのは随分経ってからだった。テンプレートの再帰展開によるコンテナの標準入出力
以下には std::vectorを用いた例を示すが、なにもベクタに限った話ではく、シフト入力演算子がオーバーロードしてあれば何にでも使える方法。
標準入力をベクタに詰めていく際、イテレータなりカウンタなりでループで回しながら……という書き方はいささか格好が悪い。記事冒頭に示した std::getline() を使った形は行が欲しい時の話である。ではスペース区切りでワード単位に入力を小分けにして受けたいときはどうするか。
標準入力ストリームはもとよりスペースと改行記号を区切りとして読み込みを行ってくれるのだ。 std::getline() したものを更にスペースで split なりするのは阿呆である。もっとイケメンな書き方がC++なら可能だ。
次のようにグローバル名前空間でシフト入出力演算子をオーバーロードすることで、入力処理が劇的に簡潔に書けるようになる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <iostream> #include <vector> template <typename T, typename C = char, typename Tr = std::char_traits<C>> std::basic_istream<C,Tr>& operator>>(std::basic_istream<C,Tr>& istream, std::vector<T>& rhs) { for (auto&& v : rhs) istream >> v; return istream; } template <typename T, typename C = char, typename Tr = std::char_traits<C>> std::basic_ostream<C,Tr>& operator<<(std::basic_ostream<C,Tr>& ostream, std::vector<T> rhs) { for (auto&& v : rhs) ostream << v << (&v == &*(--rhs.end()) ? "" : " "); return ostream; } |
上記のような演算子オーバーロードを定義しておいた時のベクタへの読み取りと出力は、例えば次のようになる。ベクタの中身がスペース区切り(もちろん上のシフト出力演算子を書き換えれば区切り文字は自在に変更できる)で出力される。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <iostream> #include <string> #include <vector> // ベクタのシフト入力演算子オーバーロード int main(int argc, char** argv) { constexpr std::size_t size {42}; std::vector<std::string> input {size, std::string("")}; std::cin >> input; std::cout << input; return 0; } |
特筆すべきは、ベクタがネストしていても、つまり多次元配列であっても同様の記述が可能であるということだ。これはテンプレートの再帰展開によるものだ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <iostream> #include <string> #include <vector> // ベクタのシフト入出力演算子オーバーロード int main(int argc, char** argv) { constexpr std::size_t row {3}; constexpr std::size_t column {5}; std::vector<std::vector<std::string>> input { row, std::vector<std::string>(column, std::string("")); }; std::cin >> input std::cout << input; return 0; } |
強いて文句をつけるならば、要素数分でベクタを初期化しておかなければならないため、ベクタの宣言が汚くなってしまうことくらいか。
range-besed for 内でのインデックス
range-based for、大好き。
でもインデックス番号が必要になるからしぶしぶカウンタで回すことはちょくちょくある。
それでもrange-based forで回したい時、インデックス番号はイテレータやらをちょいちょいとポインタ演算してやることで計算可能だ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <iostream> #include <string> #include <vector> int main(int argc, char** argv) { std::vector<std::string> strings {"hoge", "fuga", "piyo"}; for (const auto& s : strings) { auto index {&s - &*strings.begin()}; std::cout << "[" << index << "] " << s << std::endl; } return 0; } |
上記の例では読み取りしか行わないため、
const auto& としたが、変更を加えるような処理の場合は
auto&&とする。
要素の最後であるか否かを判定したい場合は次にように書けばいい。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <iostream> #include <string> #include <vector> int main(int argc, char** argv) { std::vector<std::string> strings {"hoge", "fuga", "piyo"}; for (const auto& s : strings) { auto is_end = [&s, &strings]() { return &s == &*(--strings.end()); }; if (is_end()) std::cout << s << std::endl; } return 0; } |
もちろん、こんなクソ簡単な処理にrange-based forを使う意味は皆無である。例はあくまで例である。それと、ラムダで書いたほうが意味がはっきりして見やすいかと思ったが、却って汚くなってしまったかもれない。
暗記できる程度の処理であるため、これこそイディオムとして覚えたほうが楽なのだが、range-based forの簡潔に書けるというメリットを失いかねないため、 index() なんて関数を作ったりしたほうが良いのかもしれない。
追記:もっとイケメンに書けた。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <iostream> #include <string> #include <vector> int main(int argc, char** argv) { std::vector<std::string> strings {"hoge", "fuga", "piyo"}; for (const auto& s : strings) std::cout << s << (&s != &strings.back() ? ' ' : '\n'); return 0; } |
back() の存在をすっかり忘れていた。あと三項演算子が素敵。