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

ストラウストラップのプログラミング入門』を読む。今日は、第19章「ベクタ、テンプレート、例外」の19.4まで。ようやくHello, templateです。

19章では、前章までに作成したvectorクラスをさらに拡張します。前半(18.2節まで)では、あるサイズのvectorを、より大きなサイズのvectorに変換できるようにします。後半(18.3節から)は、自作vectorで、double以外の要素を扱えるようにします。そのためにテンプレートを使います。

19.2「サイズの変更」でのメモリ操作に対する雑感

標準ライブラリのvectorと同じく、自作するvectorも、要素数(sz)と、要素数+空き領域(space)の2つのメンバ変数を持ちます。

引数なしコンストラクタを使って自作vectorのオブジェクトを作るときは、spaceの値を手動で0にします。また、vectorオブジェクトをコピーしたら、コピー後のサイズであるところのint型の値を、これまた手動でszやspaceに代入します。

素数や空き領域は、自分にとってあくまでも「コレクションに問い合わせて取得する」モノであるという感覚なので、手動で設定することに何ともいえない抵抗がありました。そんな値、信頼できるんかいな?と思ってしまう・・・いや、信頼できるように自分で書けという話なのですが・・・。

19章を最後まで写経してやっと若干慣れましたが、前半は「本当にspace = size = 0なんて自分で書くの??」と首を捻っていました。こんな調子じゃメモリを操作するようなプログラムは書けないなぁ。。。

19.2.6「現時点でのvector

19.2.6に、これまで実装してきたvectorクラスの宣言が掲載されています。本文の解説どおり作ってきたクラスとの相違点をざっくり確認します。

explicit

引数ありコンストラクタの宣言に、explicitというキーワードがつきました。本文では18.3で解説されています。

explicitは、暗黙の型変換を不許可にする機能を持ちます。ちょっと防御的にしたようです。

vector要素へアクセスする関数を、値渡しから参照渡しに変更

変更前。

double operator[](int n) const { return elem[n]; }

変更後。

const double& operator[](int n) const { return elem[n]; }

18.4で、「doubleは小さなオブジェクトなので、戻り値は値渡しにした」という解説がありました。(p.576)

次節からはdouble以外の値も扱うので、より大きなサイズのオブジェクトも入りうるでしょう。そのことを見越して、参照渡しに変更されたのかなと思いました。

19.3「テンプレート」

ジェネリックプログラミングとオブジェクト指向プログラミングについて解説がありました。これは面白かった。

以前、「JavaのイレイジャとC++のテンプレートがコンパイルを通る仕組みは同じなのか?」という素朴な疑問を残していました(http://d.hatena.ne.jp/torazuka/20110902/cxx)。

しかし、型情報を消去することで汎用的な振る舞いを実現するジェネリクスと、特定の型に合わせたコードを生成するテンプレートでは、まるで違うんですね。やっと分かりました。(どちらもコンパイラが頑張って解決するという点は同じですが)

分からなかったこと

こういうコードがあるとします。

template<class T> class vector1 {
    int sz; // 要素の数
    T* elem;   // 最初のdouble型の要素へのポインタ
    int space;  // 要素の数 + 新しい要素のための空領域
public:
    // ...(略)....
    void resize(int newsize, T def = T());    // ☆
};

template<class T> void vector1<T>::resize(int newsize, T def = T())   // ☆'
{
    reserve(newsize);
    for(int i = sz; i < newsize; ++i) elem[i] = def;  // 新しい要素を初期化
    sz = newsize;
}

struct No_default{
    // デフォルトコンストラクタなし
    No_default(int) { };
};

int main(){
    vector1<double> v1;
    v1.resize(100);
    v1.resize(200, 0.0);
    v1.resize(300, 1.0);

    vector1<No_default> v3;
    v3.resize(100, No_default(2));    // ★
}

No_defaultクラスには、引数なしのコンストラクタがないことがポイントです。

コンパイルすると、No_defaultクラスのオブジェクトを要素に取るvectorまわりで、エラーになります。

error C2512: 'No_default::No_default' : クラス、構造体、共用体に既定のコンストラクターがありません。
クラス テンプレート のメンバー関数 'void vector1::resize(int,T)' のコンパイル

★の箇所では、resize関数の引数No_default(2)によって、コンストラクタ No_default(int) が呼び出されるハズです。メンバー関数resizeの宣言(☆)に初期値T()を記述していますが、これが実行されることはありません。

しかし、エラーになります。ためしに、引数なしコンストラクタを追加すると、コンパイルは通りました。

ということは、たとえ実際に呼び出しを行わない場合でも、デフォルトコンストラクタを持たないクラス(No_default)のオブジェクトを、このテンプレートに渡してコンパイルすることはできないということでしょうか。静的な解決が完結(?)してないとイカンということですよね、きっと。(そうなのか?)

19.3.6「vectorの一般化」

自作vectorの中に、自作allocatorクラスのオブジェクトをメンバとして保持するようにコードを変更します。

allocatorクラスは、次の4つのメンバ関数を持ち、メモリ管理を行います。

  • 生のメモリの並びを、ある型のデータのために確保する
  • 確保済みのメモリを、ある型のデータとして初期化する
  • ある型のデータを、生のメモリに戻す(という言い方は適切でないかも…)
  • 確保していたメモリを解放する

自作vectorのメモリを直接操作するメンバ関数から、allocatorの関数を呼び出して、危ない仕事をディスパッチする使い方をします。

複雑で難しい・・・。そして、長々と解説しておきながら、「エキスパートになる準備が整うまで手をだすな」という、おそらくは真っ当な忠告が書かれていました(19.3の最後)。スッポスッポ先生、まじスッポスッポ先生。

トラブル備忘メモ: IDEの補完

テンプレートとして宣言したクラスのオブジェクトに、Visual Studioの補完(インテリセンス)が効きません。

template<class T, class A = allocator1<T>> class vector1 {
    A alloc;  
    // ...(略)...
};

上記のとき、allocator1クラスのメンバ関数が補完されません。これは、そういう仕様? 不便…。

環境は、Microsoft Visual C++ 2010 Express Version 10.0.30319.1 です。

ヘッダファイルを読み込んで辞書にするような作業が、本来必要なのかもしれないと思い、うっすら調べ中です。

Visual Assist Xという有料アドインは見つけたけれども・・・むー。