ストラウストラップのプログラミング入門(32) 19章「ベクタ、テンプレート、例外」(続々)

ストラウストラップのプログラミング入門』を読む。新年最初の読書日記です。

前回の日記で悩んでいた、「アロケータとvector_base(自前vectorの基底クラス)を導入したコード」に関する疑問が、今夜めでたく解決しました。というわけで、今の自分の理解を書きます。

その前に

以前の読書日記で、次のように書きました。

アロケータは標準ライブラリにありますが、自分で定義することもあるようです。…これが問題で、本文ではどちらの文脈でアロケータを扱っているのかが分かりません。

本文では、アロケータのメンバ関数の実装に関する詳細には踏み込んでいません。なので、ここでは標準ライブラリのアロケータを利用するものと素直に解釈することにします。

19章のコードの補完

19章の後半に掲載されているサンプルコードは、本文の内容を十分に理解していれば、読者がおおよそ補完できると思われる範囲で、省略されているようです。

というわけで、本文のコードに何を追加する必要があったかを挙げていきます。

本文のコードを引用すると、こんなふうになっています。

vector_baseを導入前のコード。# 19.3.1(p.605)に、19.3.6(p.615)のクラス定義を加えました。

template<class T, class A = allocator<T> > class vector {
	A alloc;	// アロケータを使って要素のメモリを処理する
	int sz;	// サイズ
	T* elem;	// 要素へのポインタ
	int space;	// サイズ + 空き領域
public:
	vector() :sz(0), elem(0), space(0) { };
	explicit vector(int s);

	vector(const vector&);  // コピーコンストラクタ
	vector& operator=(const vector&);    // コピー代入

	~vector() { delete[] elem; }
	//(略)
}

vector_baseを導入後の19.5.5のコード(p.627)。これの省略箇所を補完できず、悩んでいました。

template<class T, class A>
struct vector_base {
	A alloc;	// アロケータ
	T* elem;	// 割り当ての開始
	int sz;	// 要素の数
	int space;	// 割り当てた領域の数

	vector_base(const A& a, int n)
		:alloc(a), elem(alloc.allocate(n)), sz(n), space(n) { }
	~vector_base() { alloc.deallocate(elem, space); }
};

template<class T, class A = allocator<T> >
class vector : private vector_base<T,A> {
	public:
	// ...
};


補完(1) 基底クラスにデフォルトコンストラクタを追加する

自前vectorが、標準ライブラリのvectorの振る舞いを真似るのであれば、サイズを指定せずにインスタンスを生成できる必要があります。

template<class T, class A>
struct vector_base {
	// (略)
	vector_base() { }	// ★追加
	// (略)
};

基底クラスに、引数なしのコンストラクタを追加します。

補完(2) 基底クラスのコンストラクタに初期化を定義する

先ほど引用した「基底クラスを導入した後のコード」から分かることは、次の2つです。

  • メンバ変数を基底クラスに移動した
  • 派生クラスが基底クラスをprivateで継承している

基底クラスはstructなので、そのメンバーのスコープはデフォルトでpublicです。しかし、privateで継承したことによって、派生クラスから見ると、基底クラスのメンバのスコープはprivateになります。

そのため、派生クラスのコンストラクタから初期化することはできません。派生クラスから、適切な基底クラスのコンストラクタを呼出し、基底クラスのコンストラクタがメンバ変数を初期化する必要があります。

template<class T, class A>
struct vector_base {
	// (略)
	vector_base() :elem(0), sz(0), space(0)  { }	// ★初期化を追加
	// (略)
};

(この部分は、finalfusionさんのコメントのおかげで理解できました。ありがとうございます)

補完(3) 派生クラスから基底クラスのコンストラクタを呼び出す

最終的に、この方法を知ったので解決しました。

派生クラス側のコンストラクタの宣言を、単にこうすればよかったんですね。

template<class T, class A = allocator<T> >
class vector1 : private vector_base<T,A> {
public:
	vector1(): vector_base() { }
	vector1(int s): vector_base(allocator<T>(), s) { }
	// (略)

特に、いまの場合、派生クラスのテンプレート引数内に、あるテンプレート引数(T)によって初期化されるテンプレート引数(A)があります。Aの変数オブジェクトを引数に要求する基底クラスのコンストラクタに、これを渡す方法が分からず、困っていました。

補完(4) 基底クラスのデストラクタを正しく呼び出す

実行時エラーの原因はこれでした。基底クラスに移したメンバ変数をdeleteしようとするコードが、派生クラスのデストラクタに残っていました。

template<class T, class A = allocator<T> >
class vector1 : private vector_base<T,A> {
public:
	// (略)
	~vector1() { } // ★基底クラスのメンバ変数を触ろうとしていたコードを削除
	// (略)
};

まとめ

コードはこうなりました。# 動作確認用に、コンストラクタ、デストラクタ内で標準出力のコードを足しています。また、標準ライブラリのvectorとぶつからないように、vectorのクラス名をvector1に変更しています。

template<class T, class A>
struct vector_base {
	A alloc;
	T* elem;
	int sz;
	int space;

	vector_base() :elem(0), sz(0), space(0) {
		cout << "vector_base()\n";
	}
	vector_base(const A& a, int n)
		:alloc(a), elem(alloc.allocate(n)), sz(n), space(n) {
		cout << "vector_base(const A&, int)\n";
	}
	~vector_base() {
		cout << "~vector_base\n";
		alloc.deallocate(elem, space);
	}
};

template<class T, class A = allocator<T> >
class vector1 : private vector_base<T,A> {
public:
	vector1(): vector_base()
	{
		cout << "vector()\n";
	}
	vector1(int s): vector_base(allocator<T>(), s) 
	{
		cout << "vector(int s)\n";
	}

	vector1(const vector1&);  // コピーコンストラクタ
	vector1& operator=(const vector1&);    // コピー代入

	~vector1() {
		cout << "~vector()\n";
	}
	//(略)
};

// (略)

// 動作確認
int main(){
	vector1<double> v(10);
	v.push_back(2.0);
	cout << "v: size == " << v.size() << endl;
	cout << "v: capacity == " << v.capacity() << endl;

	cout << endl;

	vector1<string> vs;
	vs.push_back("hoge");
	cout << "vs: size == " << vs.size() << endl;
	cout << "vs: capacity == " << vs.capacity() << endl;
}

本当にこれでよいかは分かりませんが、コンパイルも通り、実行時エラーも出ず、vectorもどきとして動くようになりました。

実行結果です。

vector_base(const A&, int)
vector1(int s)
v: size == 11
v: capacity == 20

vector_base()
vector1()
vs: size == 1
vs: capacity == 8
~vector1()
~vector_base
~vector1()
~vector_base

19.3.6「vectorの一般化」に次の記述があります。(p.613)

  • Xがデフォルト値を持たない場合、vectorをどのように扱えばよいか。
  • 使い終えた要素が確実に削除されるようにするには、どうすればよいか。

19.3.6では、vector要素のためのメモリ領域を新たに確保する操作を伴う、vector::resize()、vector::reserve()、vector::push_back()に関する話題で、上の2点が解説されました。

しかし、後者については、vector自体のメモリ領域に注意を払う局面でも、同じように考えるべきことでした。それができていなかったから、派生クラスのデストラクタの修正を見落としたわけですね。

時間はかかりましたが、色々勉強になりました。