C++ Advent Calendar 2012 「18日 : Cer に知って欲しい C++」

この記事は, C++ Advent Calendar 2012 (C++ Advent Calendar 2012 - PARTAKE) の18日目です.

温いネタをやりたいので C 言語を使っている人 (Cer) に C++ の知って欲しい/今すぐ使える機能を Tips 的に書いていこうと思います.

対象は特に設けなくていい気がしますが, 例えば数値計算クラスタとか. 普段 malloc とか for とかぶんぶんしてるような気がするので言い例かも.

よく分からないところがあったらこの記事か Twitter の @krustf にでも質問してください.

後, 詳しい説明はしないほうが良いと思います. "C++ ってこんな風にかけるのか!" ぐらいの感想を持って頂いて, 使ってみようとする人が増えてくれればと思います. その点では途中よく分からない語が出てくるかもしれませんが「へー」ぐらいで見逃してください.

注意

書かれている全てのコードは GCC 4.7.1 でビルドおよび動作を確認しています.
あとここで言っている C++C++11 のことです. 天変地異が起こったとしても間違っても C++03 のことではありません.

以下のようにコンパイルオプションを付けてビルドします.

% g++ -std=c++11 -pedantic-errors main.cpp

Case 1 : malloc/free しないで動的な連続領域を確保する.

よくあるもので「特に」発狂するのが malloc/free で, 大体メモリの free し忘れがある. そもそも free を手動でやって, 「あれ? free したっけ?」は必ずあるし, SAFE_FREE(ptr) とか SAFE_DELETE(ptr) とかいう悪しき関数マクロを作っていた人も多いはず.

C++ ならそもそも malloc/free に頼らず, メモリ上で連続な配列を動的確保できる. それが std::vector.

#include <vector> // std::vector

int main() {
  std::vector<int> v(100, 0);
}

std::vector の T には配列の要素の型を書く. 上記の例の場合, C 言語で書くと以下のようなものと同じ.

void main() {
  int* v = malloc(sizeof(int) * 100);
  for(int i = 0 ; i < 100 ; v[i++] = 0); // あえて memset 使ってない.
  free(v);
}

std::vector では, 作成時に配列の初期要素数と, 初期化時の値が渡せる. 何も渡さないで, 空の動的配列を作ることもできる.

後で要素を追加するには push_back メンバ関数がある. また size メンバ関数で配列の現在の要素数が分かる. C の配列と同じように [] でアクセスすることもできる.

std::vector<int> v;

assert(v.size() == 0);
for(int i = 1 ; i <= 5 ; ++i) {
  v.push_back(i);
}
assert(v.size() == 5);

int sum = 0;
for(std::size_t i = 0 ; i < v.size() ; ++i) { // v.size() の戻り値は通常 size_t
  sum += v[i];
}
assert(sum == 15);

実行結果 : Ideone.com - lYLsbp - Online C++0x Compiler & Debugging Tool

push_back を用いず, std::vector の作成時に配列サイズを渡しておけばこれまでの C の配列のように扱うことができるので非常に便利.

C++ の規格的にも std::vector の配列の要素は連続していることが保障される. 先頭要素へのポインタが欲しい場合には data メンバ関数を使えば取れる.

Case 2 : 単純な計算で for をまわさない

例えば, とりあえず配列に入っている値の総和が欲しかったり, 全部を n 倍したいとかあったとき, C 言語ではいちいち for 文を書く必要がある *1.

int sum(int* arr, int size) {
  int s = 0;
  int i;
  for(i = 0 ; i < size ; ++i) {
    s += arr[i];
  }
  return s;
}

void main() {
  int arr[5] = { 1, 2, 3, 4, 5 }
  assert( sum(arr, 5) == 15 );
}

C++ なら std::accumulate がある.

#include <array>   // std::array
#include <numeric> // std::accumulate
#include <cassert> // assert

int main() {
  std::array<int, 5> arr = { 1, 2, 3, 4, 5 }; // C の配列と同じ
  assert( std::accumulate(std::begin(arr), std::end(arr), 0) == 15 );
}

std::begin, std::end はそれぞれ配列の先頭要素と末尾の次の要素 (上の例で言うと"5"の次のアドレス) , と今は考えておくといいだろう.

for 文が要らなくなってコードが短くなりバグの発生確率も減った*2. 素晴らしい.

他にもヘッダは異なるが, には安定/非安定ソートや, 条件にマッチした要素を探し出す find, 配列のコピーを行う copy などの関数が用意されている. 詳しくは algorithm - cpprefjp - C++ Library Reference を見るといいだろう.

Case 3 : リソース管理

我々は Cer ないし C++er である. である以上リソース ( memory, file I/O, graphics... ) の確保と解放の面倒を見なければいけない.

しかし, リソースの管理はめんどくさい. 恐らく大体の場合, 大量の動的確保メモリやグラフィックスハンドルやその他コンテキストが山のようにあり, それら全てを安全に削除する必要がある. GC がある言語がなんとうらやましいことか.

C++ にはそれらリソースの管理(特に解放処理)についてスマートポインタを用意している. スマートポインタはクラスであり, ポインタを模倣するように作られている. 複数のスマートポインタがあり, またそれぞれ違う用途で用意されているが, 基本的には標準ライブラリにある shared_ptr と unique_ptr が最も使われるだろう.

#include <memory> // std::shared_ptr

std::shared_ptr<int> global;

int main() {
  void * lp = 0;
  {
    std::shared_ptr<int> local = std::make_shared<int>(42);
    assert(*local == 42);
    global = local;
    lp = local.get(); // 実際のポインタは get で取得できる.
  }
  void * gp = global.get();
  assert(lp == gp); // ポインタが等値 == 同一アドレス
}

// プログラム終了時に global のみが所有するため global がリソースを削除.

※簡単なサンプルが思い浮かばなかった... ごめんなさい.

std::shared_ptr はリソースを共有するためのスマートポインタで, 複数箇所で同一リソースを参照したときに, 「全ての参照が外れたらその段階でリソースを削除する」という動作をする.
例えば, 音楽ファイルの PCM データをメモリに展開したときに shared_ptr で保持すれば, 複数のサウンドデバイスで共有することができる. *3

しかし, std::shared_ptr ではもったいないと思う時があるだろう, 例えば関数のローカルスコープ内でのみ使用するリソースなら, 単に自動的にリソースを解放してくれれば良い. そういう時は std::unique_ptr が妥当だろう.

#include <memory> // std::unique_ptr

int main() {
  std::unique_ptr<int> ptr(new int(42));
  assert(*ptr == 42);
  // スコープの終わりで自動的に削除
}

かなり単純なサンプルだが, 大体分かると思う. * でポインタと同じようにポインタの中身を見ることができるし, shared_ptr と同じく get で実際のポインタを取り出せる.

例えばこれは, FILE などで使いたくなるが, 若干手順を踏む必要がある. *4

#include <memory> // std::unique_ptr
#include <cstdio> // std::FILE
#include <cassert>

int main() {
  std::unique_ptr<FILE, decltype(&std::fclose)> file_ptr(std::fopen("hoge.dat", "w"), std::fclose);

  if(file_ptr) // 有効なポインタが入っている場合 true と評価される. shared_ptr でも同じ.
    std::fprintf(file_ptr.get(), "hello, world");

  // スコープの終わりで自動的に std::fclose が呼ばれる.
}

カスタムデリータという機能を用いて std::fclose を file_ptr で確保したリソースの削除を行う関数として登録している. (unique_ptr のデリータ指定 - krustf の雑記)

shared_ptr にもあるので, これを使えば通常の free や delete といった関数以外で削除しなければならないリソースも shared_ptr, unique_ptr を用いて管理できる.

もちろん, 生のポインタを使うべき箇所 (ライブラリ内部など) もあると思うが, 基本的にはこれらを用いて自動で解放することが, 人道的に最も良い解決策である.

Other : Boost C++ Libraries

C++ の(恐らく)最も代表するべきライブラリに Boost がある.

Boost C++ Libraries

この C++ Advent Calendar でも恐らく幾度となく紹介されたと思う.

実は今回紹介した shared_ptr も, 元々は Boost で実装されていたライブラリである. 他にもスレッドライブラリや, ハッシュコンテナ, 乱数生成など実に多くのライブラリが Boost から C++ 標準ライブラリに移植されている.

標準ライブラリにはないが, Boost にはもっと有用なライブラリが沢山用意されているので, 一度上の公式ページからドキュメントを読んでみるといいだろう.

ちなみに私が最近よく使っているのは,

  1. Chapter 6. Boost.Container - 1.52.0
  2. Chapter 15. Boost.Lexical_Cast 1.0 - 1.52.0
  3. Chapter 17. Boost.MPI - 1.52.0
  4. Chapter 1. Phoenix 3.0 - 1.52.0
  5. Chapter 1. Range 2.0
  6. Boost Test Library

辺りか.

数時間前に言っていた多次元配列も完全にメモリが連続なものは Boost.MultiArray で作れる. (MultiDimensional Array Libary - 1.52.0)

終わりに

今回は緩めでした. (というか時間ないやばい)

僕が C を使っている人々に特に薦めたいのは最初の Case 1 で紹介した vector です. 配列を動的に確保しなければいけないのはどうしようもないのですが, malloc/free は最大限控えるように考えてほしいと思います. 僕もかなり malloc/free には苦しめられた記憶がありますので...
メモリの解放タイミングを幾分か考える必要がないのでかなり気持ちが楽になると思います. 精神の安定は良きことです.

Case 2 では for 文を可能な限り書くなというのをメッセージとして書いたのですが, 単純な for-each や for ループに置き換えづらいものに対しては使いづらいかも知れません. それでも, 単純なループならほとんど に用意されている関数で置き換えられますので, 積極的に使うことをお勧めします. その点では Boost.Range がかなりお勧めです. (Boost.Range アルゴリズム関数のすすめ - boostjp)

Case 3 ではリソースの管理を考えました. 単純に free/delete するものではないリソース (fclose や glDeleteBuffers など) でも, カスタムデリータ機能を使って安全に解放できます. unique_ptr や shared_ptr を用いてリソースをもっと手軽に扱っていきましょう.

最後に Boost を若干紹介しました. 僕は今 MPI を用いてなんとも言えない (言っていいのかよくわからない) 計算をしているのですが, Boost.MPI は非プリミティブな型であっても Boost.Serialization を用いてシリアライズし, 転送できるようになっています. そのため std::vector などを転送することが簡単で *5 また自分で作った struct なども所定の手順を踏めば転送できるようになっています.

Cer の皆さんにとって良きメッセージになったかは分からないですが, .c を .cpp に変え, gcc を g++ に変えてみることから始めてみませんか?

*1:恐らく一般的には

*2:バグを減らすには, コードを書かないことが望ましい

*3:ただし, std::shared_ptr で配列を渡す場合は若干手順が増える. [http://d.hatena.ne.jp/faith_and_brave/20110920/1316507398:title]

*4:C++ なら本当は を使うべきかもしれないが出力フォーマット指定が...

*5:Boost.MPI に用意されている