ストラウストラップのプログラミング入門(31) 19章「ベクタ、テンプレート、例外」(続)
『ストラウストラップのプログラミング入門』を読む。やっと今月初の読書日記です。やりましたね(?)
これまで、本文に出てきたコードは全部写経し、動作することを確認してきたのですが、ここにきてハマってしまいました。19章のあちこちに散ったコード片を集めて1つのコードにした結果、コンパイルが通りません。寄せ集め方が悪いのだと思いますが、どう悪いのかが分からないので、整理を兼ねて、今日はその話を書きます。
やっていることは、こんな感じです。
- 本文掲載コードを写経して繋ぎ合わせ
- 本文で省略された実装を推測して補完
- 訳書に未反映の原著のエラッタを反映
- (19.3.6)アロケータの導入
- 自分で定義すべきものなのかどうかが、よく分からないまま実施
- (19.5)新たな基底クラスの導入
- それによって変更が発生する箇所が、よく分からないまま実施
4以降は明らかにアレとして、3までの作業にもミスがあるかもしれません。
2. 本文で省略された実装を推測して補完
19章では、double型用に作ってきたvector(以下、自前vector)を他の型でも使えるように、テンプレートを使って書き換えます。
自前vectorでは、個々の要素をdouble型配列のメンバとして保持していました。19.2.6に掲載されているコードは、次のような感じです。
class vector1 { int sz; // 要素の数 double* elem; // 最初のdouble型の要素へのポインタ int space; // 要素の数 + 新しい要素のための空領域 public: vector1() :sz(0), elem(0), space(0) {}; explicit vector1(int s) :sz(s), elem(new double[s]), space(s) { for(int i = 0; i < sz; ++i) elem[i] = 0; } // (略) };
# 本文では自前vectorもvectorというクラス名で掲載されていますが、標準ライブラリのvectorとぶつかるので、便宜上、自分はvector1というクラス名に変更して写経しています。
そして、19.3.1「テンプレートパラメータとしての型」(p.605)に、templateを使って書き換えたクラス宣言が掲載されています。こんな感じです。
template<class T, class A = allocator<T> > class vector1 { int sz; T* elem; int space; public: vector1() :sz(0), elem(0), space(0) {}; explicit vector1(int s); // (略) };
テンプレート版では、コンストラクタ vector1(int s) の実装が省略されています。
この状態(コンストラクタの実装が元のコードのまま)だと、double型の要素を持つ自前vector型の変数は使えますが、たとえば、int型の要素を持つそれは使えません。
error C2440: '初期化中' : 'double *' から 'int *' に変換できません。
こんなふうに怒られます。(当たり前ですね)
テンプレートを使う前のコードでは、elemをdouble配列で初期化していました。初期化する要素の型をtemplate引数で指定するので、コンストラクタの初期化構文の中でも、doubleではなくTを使うのが妥当でしょう。
というわけで、この箇所を補完しました。
vector1(int s) :sz(s), elem(new T[s]), space(s) { for(int i = 0; i < sz; ++i) elem[i] = 0; }
(これでいいのかな?)
3. 訳書に未反映の原著のエラッタを反映
公式サイトを確認したところ、19章のエラッタが翻訳版には未反映でした。
(+12/28/2010) pg 679: s/elem(a.allocate(n))/elem(alloc.allocate(n))/
http://www.stroustrup.com/Programming/errata.html
この部分は、19.5.5「vectorのためのRAII」で、アロケータ追加後のコードとして示されているコード片の中にあります。
手元のコードには、修正を反映しました。
4. アロケータの導入
自前vecotrでは、メンバ関数の中でメモリを直接触るのではなく、アロケータに任せます。アロケータに関する自分の理解は浅く、メモリを直接触るような操作をまかせる関数をいろいろ持つクラス、くらいの認識です。(勉強中です)
アロケータは標準ライブラリにありますが、自分で定義することもあるようです。…これが問題で、本文ではどちらの文脈でアロケータを扱っているのかが分かりません。
19.3.6「vectorの一般化」では、アロケータのコードが示される直前に、次のように書かれています。
まず、初期化されていない記憶域を取得し、操作する方法を見つけ出す必要がある。幸い、標準ライブラリには初期化されていないメモリを提供するallocatorクラスがある。少し単純化したバージョンは、次のようになる。
これは、説明のために示しただけなのか、こんなふうに実装しようといっているのか、どっちなんでしょう。
この後、テンプレートを使うように変更した自前vector1の定義に、アロケータを加えます。
template<class T, class A = allocator<T> > class vector1 {
// (略)
}
2番目以後のテンプレート引数は、それより前に定義されたテンプレート引数を使って初期化できます。なので、この場合、Aであるテンプレート引数を明示的に与えなければ、Tをテンプレート引数に取るallocatorで初期化されます。
ここでの疑問は、そのallocatorというのは、自分で定義するのか否かが分からない、というものです。さらにいうと、自分で定義するにしては、アロケータの関数の実装が本文で省略されているけれども、補完できるほどまだアロケータを理解していない、という問題に直面しています。…アロケータを勉強すれば解決するんかな、これ。
5. 新たな基底クラスの導入
19.5.5「vectorのためのRAII」では、vector用に確保するメモリをリソースとみなしてRAIIしようということで、さらに自前vetorを進化させます。そのために、自前vectorの基底クラスとして、新たにvector_baseクラスを定義します。
vector_baseクラスには、自前vectorクラスで定義していたメンバ変数が定義されています。
template<class T, class A = allocator<T> > struct vector_base { A alloc; T* elem; int sz; int space; vector_base() {} vector_base(const A& a, int n) :alloc(a), elem(alloc.allocate(n)), sz(n), space(n) {} ~vector_base() { alloc.deallocate(elem, space); } };
このvector_baseを、自前vector1では、private継承して使います。しかし、そのときにvector1側で行う必要がある変更内容が分かりません。
下記のコードは、原文ママです。
template<class T, class A = allocator<T> > class vector1 : private vector_base<T,A> { public: // ... };
思いきり省略されていて、またしてもこれを補完できず、悩んでいます。
まず、vector_baseに持っていったメンバ変数は、vector1側で定義しなくてもよいですよね。
では、コンストラクタはどうでしょうか。基底クラスにコンストラクタの実装がない場合は派生クラスで書く、という認識ですが、vector1のコンストラクタを(先ほどの2.でテキトーに補完した状態で)コンパイルしようとすると、
vector1() :sz(0), elem(0), space(0) {}; vector1(int s) :sz(s), elem(new T[s]), space(s) { for(int i = 0; i < sz; ++i) elem[i] = T(); }
と、怒られます。
error C2614: 'vector1
' : イニシャライズ リスト内のクラス 'space' が基本クラスでもメンバーでもありません。
以前、継承とオーバーライドに関する理解を保留しましたが、そろそろツケを払う時なのかも。
ひとまず今はこんな感じです。