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 を引数と戻り値の型として明示しなければならない。
    • テンプレートで定義をサボる方法はあるけれど、 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;
}