株式会社エス・スリー・フォー

テンポラリ・バッファとしての std::vector の利用

悩ましきテンポラリ・バッファ

一時的に必要となる領域、すなわちテンポラリ・バッファはプログラムのいたるところで用いられます。

void f() {
  char buffer[256];
  ...
}

上の例では256[char]のbufferをautomatic領域に確保しています。
場合によってはこのような固定サイズではなく、可変長のテンポラリ・バッファを必要とします。可変長のテンポラリ・バッファが必要なとき、通常opeartor newによってヒープ領域から確保するでしょう。

void f() {
  char* buffer = new char[N]; // Nは変数
  ...
  delete[] buffer;
}

当然のことながら、operator newによってヒープから確保した領域は、利用後速やかにoperator deleteによって解放(ヒープに返却)しなければなりません。これを怠るとヒープを食い潰してしまいます。メモリ解放の漏れによるバグはかなり深刻です。長時間稼動しているアプリケーションが突如動作停止したり暴走を始めたり…

メモリの食い潰しに何度となく悩まされたプログラマならその恐さが身に染みているので、うっかりdeleteを忘れることはめったにありません。 とはいえ皆無とはいえません。人は過ちを犯すものです。

void g(char*);

void f() {
  char* buffer = new char[N]; // Nは変数
  g(buffer);
  delete[] buffer;
}

たとえば上のコードは安全でしょうか?

関数gが正しく実装されている限り、ほとんどの場合安全でしょう。
ところが必ずしも安全とは断言できないのです。

関数gあるいはgが直接/間接的に呼び出す関数の中で例外がthrowされたとき、fの末尾にあるdeleteが行なわれないかも知れません。例外の起こりうるコードでは、上のコードは安全とは言えないのです。

だからといって、こんなコードは書きたくありませんよね。deleteが数箇所に現れるコードを書くこと自体、'うっかり'を誘発します。

void g(char*);

void f() {
  char* buffer = new char[N]; // Nは変数
  try {
    g(buffer);
  } catch ( ... ) {
    delete[] buffer;
    throw;
  }
  delete[] buffer;
}

std::vectorによるテンポラリ・バッファ

要はdeleteを忘れなければいいわけです。
テンポラリ・バッファを実現するクラスを用意し、そのデストラクタできちんとdeleteすればいい。そうすればたとえ例外が発生しても、その例外のcatchに先立ってデストラクタが起動します。

template<typename T>
class temporary_buffer {
  T* data_;
public:
  explicit temporary_buffer(size_t n) { data_ = new T[n]; }
  ~temporary_buffer() { delete[] data_ ;}
  T* pointer() { return data_; }
};

void g(char*);

void f() {
  temporary_buffer<char> buffer(N); // Nは変数
  g(buffer.poionter());
}

そのようなテンポラリ・バッファ・クラスを実装するのも手段の一つですが、標準C++ライブラリが提供する std::vector をテンポラリ・バッファ・クラスとして使用することができます。

void g(char*);

void f() {
  std::vector<char> buffer(N); // Nは変数
  g(&buffer[0]); // バッファの先頭をgに渡す
}

このとき、注意しなければならない事があります。

バッファの先頭アドレスを得るのに &buffer[0] としています。
これを buffer.begin() としてはいけません

多くの標準C++ライブラリ実装では std::vector<T>::iteratorT*typedef となっています。が、必ずそうであるとは限らない(ライブラリ仕様で明記されていない)のです。

もうひとつ。buffer の要素数が0 すなわち buffer.size() == 0 のとき、
&buffer[0] の値は不定となります。上記のコードで N ==0 である可能性があるならば &buffer[0] ではなく &buffer.at(0) とすることをお薦めします。メンバ関数 at() は範囲外のアクセスに対し std::out_of_range 例外をthrowしてくれます。

void g(char*);

void f() {
  std::vector<char> buffer(N); // Nは変数
  g(&buffer.at(0));
}

std::vectorによるテンポラリ・バッファは途中でバッファサイズを拡張することができ、ひとつのバッファを何度も使い回すような用途には非常に適しています。

void g(char*);

void f() {
  std::vector<char> buffer(N); // Nは変数
  g(&buffer[0]);
  buffer.resize(NN); // サイズの変更
  g(&buffer[0]);
}

ここでひとつ疑問が生じます。operator new[] によって確保されるメモリ領域は連続していることが保証されています。では、std::vector<T> の内部にあるメモリは連続しているのでしょうか?メモリの連続性が保証されていないなら、得られたバッファの途中に'飛び'が存在することになり、バッファとして使用できません。

困ったことに標準C++の規格書のどこを見ても std::vector<T> の内部にあるメモリは連続している という記述が見当たらないのです。やはり std::vector<T> を'安全なテンポラリ・バッファ'として使うのには無理があるのでしょうか。

大丈夫!

以下に示すのは、C++標準化委員会が発行したDefect Report(欠陥リポート)の抜粋です。

69. Must elements of a vector be contiguous?
Section: 23.2.4 [lib.vector] Status: TC Submicodeer: Andrew Koenig Date: 29 Jul 1998

The issue is this: Must the elements of a vector be in contiguous memory?

(Please note that this is entirely separate from the question of whether a vector iterator is
required to be a pointer; the answer to that question is clearly "no," as it would rule out
debugging implementations)

Proposed resolution:

Add the following text to the end of 23.2.4 [lib.vector], paragraph 1.

The elements of a vector are stored contiguously, meaning that if v is a vector<T, Allocator>
where T is some type other than bool,
then it obeys the identity &v[n] == &v[0] + n for all 0 <= n < v.size().

Rationale:

The LWG feels that as a practical macodeer the answer is clearly &yes&.
There was considerable discussion as to the best way to express the concept of &contiguous&,
which is not directly defined in the standard.

これによると、

vbool以外の型 T を要素とする std::vector<T> であるとき、&v[n] == &v[0] + n ( 0 <= n < v.size() ) を満足する

という但し書きを標準C++規格に追加する。と述べています。

Defect Reportとは、言語仕様上の欠陥を指摘した文書で、ここに挙げられた指摘事項は言語仕様に反映されることになっています。

このことから、std::vector<T> の内部にあるメモリは連続している と結論付けてよさそうです。

もうひとつのテンポラリ・バッファ

std::vector<T> による安全なテンポラリ・バッファを紹介しました。
要はデストラクタが責任持って領域の解放を行なってくれればいいわけです。
このようなちょっと賢いポインタとして std::auto_ptr<T> があるのですが、残念ながら配列に対しては使えないのです。

void g(char*);

void f() {
  // 誤った使い方
  std::auto_ptr<char> buffer(new char[N]); // Nは変数
  g(buffer.get());
}

std::auto_ptr<T> のデストラクタはコンストラクト時に与えられたポインタをdeleteしますが、
delete[]ではないので、new T[...]されたポインタは正しく後始末をしてくれません。

/*
 * Visual C++ v7 <memory> より抜粋
 */
template<class _Ty>
class auto_ptr {
private:
  _Ty* _Myptr;

public:
  explicit auto_ptr(_Ty *_Ptr = 0) thorw()
    : _Myptr(_Ptr) {}

  ~auto_ptr()
  {  // destroy the object
    delete _Myptr; // 配列をdeleteしない!!
  }
  ...
};

boost::scoped_array<T> を使ってみましょう。
boost::scoped_array<T> は配列版 auto_ptr ともいえるオブジェクトで、
デストラクタ内で delete[] するよう実装されています。

void g(char*);

void f() {
  // boost::scoped_array によるテンポラリ・バッファ
  boost::scoped_array<char> buffer(new char[N]); // Nは変数
  g(buffer.get());
}