Hatena::ブログ(Diary)

IKB: 雑記帖 このページをアンテナに追加 RSSフィード Twitter

2012-02-15

map のダンプ

std::map を使っていたとして、この中身をコンソールに出力したくなる、そんな場面はたびたびあるとおもう。 で、 C++ 使いとしては、すらすらとこんな具合に書けるようになりたいよね。

typedef std::map<std::string, int> my_map_t;
struct my_pair_t : my_map_t::value_type {
  my_pair_t(const my_map_t::value_type &v)
    : my_map_t::value_type(v) {}
};
std::ostream& operator<< (std::ostream& os, const my_pair_t &p) {
  return os << "(" << p.fisrt << ", " << p.second << ")";
}
...
#include <algorithm>
#include <iostream>
#include <iterator>
my_map_t map;
...
std::copy(map.begin(), map.end()
  , std::ostream_iterator<my_pair_t>(std::cout, "\n"));

map の中身 (要素の型は my_map_t::value_type) を順に std::out にコピーする、という書き方。

アルゴリズム copy に渡す三番目の引数、 ostream_iterator の型特化で、 map の要素型でなく my_pair_t という別の型を導入しているところに注目(C++ のダークサイドへの対処)。

たいていの場合、要素型に対して operator<< を定義しておけば、 ostream_iterator の代入演算子 (operator =) がこの << 定義を見つけて使ってくれるんだけれど (name lookup)、 map の場合、 value_type が std::pair になっているために、 << 演算子の検索範囲が std 名前空間に限定されてしまい、しかして my_map_t::value_type の operator<< は大域名前空間にあるため見つけられなくなるという面倒が起きる。

(くわしくは c++ - Why does ostream_iterator not work as expected? - Stack Overflow を参照)

std 名前空間に規格に存在しない定義を追加するのは規格違反になるため、自分の map 特化型の value_type に対する std::operator<< を追加してはいけないという縛りもうまれる。

そこで、変換用ラッパー my_pair_t を大域名前空間に定義して ostream_iterator の型特化にこれを指定するという方法で回避するってわけ。

別のやり方

ほかにも蓄積変数(コレクティング・パラメーター)を使う方法が考えられる。 C++ アルゴリズムでいえば、 numeric ライブラリーにある std::accumulate を使う方法。

std::ostream* pair_out(std::ostream *os, const my_map_t::value_type& v) {
  (*os) << "(" << p.fisrt << ", " << p.second << ")";
  return os;
}
...
#include <numeric>
std::accumulate(map.begin(), map.end(), &std::cout, pair_out);

std::accumulate(begin, end, init, func) は Haskell でいうところの foldl で、範囲 [begin, end) にある要素を、二項演算関数 func を使って init に足しこんでいくという関数

ここでは初期値に std::cout を(へのポインター)を指定し、二項演算関数 pair_out で要素の内容を順にストリームに蓄積(書き出)している。

初期値に std::cout を直接渡さないのは、これまた C++ のダークサイドで…。 いや、これはダークサイドとはちょっと違うか?

accumulate に渡す初期値は「コピー可能」なオブジェクトでなければならず、 std::ostream 系はコピー演算子が private になっていて使えないため。

accumulate で蓄積値を上書きするのでなく、計算のたびにあたらしいコピーをつくるのは、計算の途中での副作用を避ける関数型言語の特性をマネしたかったからなんだろうなあ。

C++11 std::ref

コメントで std::ref を使えば参照のコピーを渡せると教えていただいたので試してみた。

id:faith_and_brave さんのコメントを受けて確認し、変更。) すごい! たしかに便利だ。

たしかに渡せるけれど、この文脈では使いにくい。 理由はふたつ。

  1. 関数引数の型には auto を指定した型推論を使えないため std::ref が背後で生成する std::reference_wrapper<std::ostream> を引数戻り値の型として明示しなければならない。
    • テンプレートで定義をサボる方法はあるけれど、 accumulate に渡す際に型を明示して特化しないといけない。
  2. 生成された reference_wrapper に挿入演算子 (<<) のオーバーロードがないため、引数として渡された参照オブジェクトを std::ostream& にキャストしなおさなければならない。

以下がサンプル。 g++ 4.6.1 で -std=c++0x オプションをつけてコンパイルできることを確認している。

#include <functional> // for std::ref
#include <iostream>   // for std::cout
#include <numeric>    // for accumulate

#include <map>

typedef std::map<int, int> map_t;

std::ostream& pair_out(std::ostream &os, const map_t::value_type &v) {
  return os << v.first << " = " << v.second;
}

int main() {
  map_t map;
  std::accumulate(map.begin(), map.end(), std::ref(std::cout), pair_out);
  return 0;
}

faith_and_bravefaith_and_brave 2012/02/16 10:22 > accumulate に渡す初期値は「コピー可能」なオブジェクトでなければならず、 std::ostream 系はコピー演算子が private になっていて使えないため。

<functional>にあるstd::refを使えば参照で渡せますね。

i_k_bi_k_b 2012/02/16 11:21 噂の c++11 新機能…! 気になったので試してみましたが、この文脈ではかなり使いにくいという印象です。

faith_and_bravefaith_and_brave 2012/02/16 11:47 reference_wrapperは参照への変換演算子がありますので、print_outでreference_wrapperを受ける必要はないです。
http://ideone.com/YiqJy

i_k_bi_k_b 2012/02/16 12:12 g++ 4.6.1 の推論がヘボいんでしょうかねえ…? キャストしないと no match for ‘operator<<’ in ‘os << v.std::pair<const int, int>::first’ というエラーでコンパイルが通らなくなるんですよ。
"operator<<" に合致する演算が reference_wrapper にないので、ラップしている型の側で名前探索してくれればいいのですが、そうはなっていない。 (ためにキャストして戻してあげないと使えない)

faith_and_bravefaith_and_brave 2012/02/16 12:40 いえ、print_outのパラメータ自体がostream&でいいです。
http://ideone.com/5HoGg

i_k_bi_k_b 2012/02/16 18:44 そうだったのですね。 これは便利だ!
確認したので、記事、変更しました。 ありがとうございます。

i_k_bi_k_b 2012/02/16 18:46 …短縮 URL の先がサンプルコードだったのですね… 見てませんでした。

egtraegtra 2012/02/17 02:34 my_pair_tを定義する方法はウマいと思いましたが、accumulator + refのほうは今イチだと感じました。
なぜなら、allumulatorを無理矢理使うくらいなら、for_each関数なりfor文なりを使うほうがよっぽど素直だと思うのです。
#include <iostream> // for std::cout
#include <algorithm> // for std::for_each

#include <map>

typedef std::map<int, int> map_t;

std::ostream& pair_out(std::ostream &os, const map_t::value_type &v) {
return os << v.first << " = " << v.second;
}

int main() {
map_t map = {
{1, 10},
{2, 20},
{3, 30},
};
// for_each
std::for_each(map.begin(), map.end(), [](const map_t::value_type &p) {
std::cout<< "(" << p.first << ", " << p.second << ")\n";
});
// range‐based for
for (const auto &p : map ) {
std::cout<< "(" << p.first << ", " << p.second << ")\n";
}
return 0;
}
(g++ 4.6.2で確認しました。おそらく4.6.1でも動くと思います)

faith_and_bravefaith_and_brave 2012/02/21 10:36 > egtraさん
accumulate(fold)は2項演算一般に対する抽象化のアプローチなので、この用途は本来の目的から逸脱したものではないと思います。

i_k_bi_k_b 2012/03/02 11:05 > egtra さん
for 文や for_each アルゴリズムの例、ありがとうございます。

ところで for や for_each は「ループする」という一番プリミティブな(抽象度の高い)機能のため、何でもできます。
一方で「copy アルゴリズムは列を別の列にコピーする」という意図を、「accumulate アルゴリズムは、列を、与えた二項演算を使って与えた初期値に足しこんでいく」という意図を明示しています。

個人的な好みの話ですが、コードを書くときはできるだけ抽象度の低い(この例でいえば copy や accumulate、できればもっと特殊化された)階層で仕事をするのがよいとおもっています。
関数型言語の話で恐縮ですが、関数を定義するときに、再帰で直接書き下すよりは fold といった高階関数を、 fold よりはもっと特殊化された map を使ったほうが、意図を伝えやすくなるし、抽象化の階梯を降りるごとにできる仕事が少なくなるため部品への結合が少なくなり、部品の取り換えがより簡単になる(よって、いろいろな実験がしやすくなる) …といった話です。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証