ニコニコC++入門

入門サイトや市販の入門書では絶対に教えてくれない、C++の本当の使い方を教えます。

STLとイテレーター

2005-07-24 23:24:54 | C++

 STLというのはC++標準のテンプレートライブラリです。STLを知らないというのは、C言語で printf や malloc を知らないということと同じです。なんとしても覚えてください。

 さて脅しはここまでにしておきましょう。STLは膨大な規模のライブラリで、プログラミング言語の標準としては他に例を見ないほどのものです。また分量だけでなく、その構造も難解で、これからC++を習得しようとする初学者の壁となっていきなり立ちはだかります。STLの学習にくじけている人は少なくないはずです。

 しかし大規模で難解なSTLですが、無節操に多機能化しているわけではなく、むしろひとつの方針の上に体系化されていますので、その方針さえ理解できれば実はそれほど難しくありません。本を読んだりしていまいち納得できないのは、その方針がわかってないからでしょう。そこで今回はSTLが基盤としているデザインパターン、イテレーターについて説明します。

 STLには、コンテナ、アルゴリズム、ストリームに加え、いくつかの有用なテンプレートが収められています。コンテナとは可変長配列やリスト、キューなど、データを収めるための構造です。アルゴリズムとは、検索やソートなど、コンテナに対する操作一般です。ストリームはファイルや画面に対する操作です。この中で特に重要なのはコンテナとアルゴリズムです。

 例えば CData オブジェクトをリストで保存するなら list コンテナを使用します。その中から特定の CData を検索したければ find アルゴリズムで検索するだけです。双方向リストやキューや可変長配列といったコンテナや、ソートやサーチといったアルゴリズムをコーディングする必要は、C++を使っている限り一生ないのです。

 STLで用意されているコンテナやアルゴリズムはすべてのオブジェクトを扱うことができます。こうした汎用性はイテレーター(反復子)によって実現されています。

 イテレーターを簡単に言えば、仮想的なポインタです。ポインタについて基礎的なことがわかっていれば、イテレーターを理解するのは簡単です。まずはポインタによる検索ついて、復習がてらに見て下さい。

void foo()
{
    const int MAX = 256;
    CData d[MAX];
    CData *f = DataFind(&d[0], &d[MAX], CData(80));
    if (f == d[MAX]) throw Exception("Not Found");
}

CData *DataFind(CData *start, CData *end, CData &match)
{
  for (CData *i = start; i != end; i++)
    if (*i == match)
      break;
  return i;
}

 DataFind 関数は CData の配列から match と同じオブジェクトを見つけ出して返す関数です。

 この関数にはいくつか奇妙なところがありますが、気づかれましたか? 従来のCによるプログラミングでこうした関数を作る場合、データが見つからなければ NULL を返すのが一般的だったと思います。しかしC++ではこのように、配列の最後尾から1つはみ出したところを指していたら失敗となります。これはイテレーターパターンと関係があります。

 とにかく i というポインタが配列の先頭から最後まで1つづつ進んでいることに注目してください。またテンプレートを使用すれば、CData だけでなく、どのようなクラスにも適用できることに気づいてください。DataFind をテンプレートにすれば、以下のようになるでしょう。

template <class T> DataFind(T *start, T *end, const T &match)
{
  for (T *i = start; i != end; i++)
    if (*i == match)
      break;
  return i;
}

 しかしこの関数は配列にしか使えないという欠点があります。配列にしか使えない理由は簡単で、ポインタを使っているからです。この欠点を解決するために、ポインタではなくイテレーターを使います。

class iterator : public __it
{
protected:
  __link_type node;
public:
  bool operator== (const iterator& x) const { return node == x.node; }
  reference operator* () const { return (*node).data; }
  pointer operator-> () const { return &(node->data); }
  iterator& operator++ () { node = (__link_type)((*node).next); return *this; }
  iterator& operator-- () { node = (__link_type)((*node).prev); return *this; }
};

 上記は本物の list コンテナのイテレーターからの抜粋です。イテレーターはクラスオブジェクトですが、++ や -> といった演算子が利用されたときに、ポインタであるかのように振舞います。あるイテレーターがなんらかのオブジェクトへのポインタであるフリをしているとき、++ すると、次のポインタへ進んだフリをします。配列のイテレーターなら本当に++するだけですし、リストのイテレーターなら次のノードを参照するようにします。

 要するにイテレーターとは、どんな複雑なメモリ管理をしているコンテナも、単純な配列であるかのようにテンプレートをダマす、仮想的なポインタなのです。

 イテレーターはデザインパターンのひとつです。STLではイテレーターを積極的に活用することでコンテナとアルゴリズムの相互運用性を高めています。アルゴリズムを自作した場合でも、イテレーターパターンに従っていれば、どの標準コンテナに対しても使えるようになります。逆にコンテナにイテレーターを用意しておけば標準のアルゴリズムを使用することができます。そして、まったくお互いの存在を知ることなく作られたコンテナとアルゴリズムが、相互に利用可能になるわけです。

 STL を使用したり拡張したりするには、STL が決めたイテレーターの振る舞いに従う必要があります。例えば find(foo.start(), foo.end()) == foo.end() ならば find が失敗であることを保証しないといけません。STL はコーディング規約という側面も持っているのです。

 コンテナやアルゴリズムによって制限があることも知っておいて下さい。例えば binary_search はソート済みのコンテナに対してしか適用できませんし、sort はランダムアクセスできない list には適用できません(もちろんエラーが出ます)。

 こうした細かい制限は、各コンテナの性質で仕方がなかったということもありますが、それよりもC++の設計思想に深い理由があります。研究段階からいつまでも脱却できない他のプログラミング言語と違って、C++は現場から厳しい要求を受けています。C++はあらゆる面でパフォーマンスに対して丹念に配慮してあり、STL もまた例外ではないのです。

 イテレーターさえ理解できてしまえば、あとはヘルプや書籍を読むだけです。たくさんのコンテナやアルゴリズムがありますが、分量が多いだけで考え方はひとつしかありません。なんとかSTLを覚えて、楽チンプログラミングを堪能してください。もう双方向リストのバグで悩む必要が無いなんて、すばらしいと思いませんか?


テンプレートの正体

2005-07-24 23:14:54 | C++

 オブジェクト指向ではポリモーフィズムを活用することで再利用性を高めているということをすでにお話しましたが、C++はオブジェクト指向だけの言語ではありません。オブジェクト指向しかできないほかの言語とは異なり、便利なものは何でも取り入れようという非常に貪欲な設計方針で作られています。こうしたいわば節操の無いやりかたに対して、たびたび他のコミュニティから非難されてきました。

 しかし90年代初頭にSTLの原型が現れて、テンプレートへの評価は逆転したと言えるでしょう。JavaやC#には当初はテンプレートがありませんでしたが、genericsという形で移植されてしまいました。それほどこのC++のSTLは衝撃的なものだったのです。

 テンプレートのようなプログラミングスタイルはジェネリックプログラミング(総称)と呼ばれ、古くはマクロという形で実現されていたものです。マクロによるプログラミングは複雑に入り組み、読みづらくなり、安全性に疑問が残ります。簡単に言ってしまえば、テンプレートは型安全(タイプセーフ)を考慮したマクロです。そして、全く無関係であると思われていた2つの概念、継承と総称がテンプレートによって1つにまとまりました。

 今回はテンプレートのメリットや活用方法についての説明ではなく、テンプレートの仕組みについてのお話です。

#define max(a, b) (((a) > (b)) ? (a) : (b))

 この伝統的なマクロにはいくつかの問題があります。max(a++, b) は aが2回インクリメントされてしまいますし、max(foo(), bar()) もまた然りです。またこうしたマクロを自作する際には、カッコの付け忘れに注意しなければなりません。これらの問題はテンプレートを使用することで解決できます(下)。

template const T& max (const T& a, const T& b) { return a > b ? a : b; }

 どうでしょうか。何を意味しているのかわかりますか?

 テンプレートは一般に理解しにくいと言われています。一部の人は「またC++は複雑な機能を入れた」と言います。しかしテンプレートは実は驚くほど単純な仕組みによって実装されているのです。そしてそれがわかれば、どんな複雑に入り組んだテンプレートでも(簡単にとは言いませんが)理解できるはずです。

 では実際にテンプレートがどのように実装されているのか見てみましょう。

test1.cpp

CData &foo()
{
  CData left, right;
  return max<CData>(left, right);
}

test2.cpp

CMode &bar()
{
  CMode left, right;
  return max<CMode>(left, right);
}

 上記の2つのソースは、どちらもテンプレート関数 max を使用しています。

 max 関数が使用しているクラスTはまだ型が定まっていません。test1.cpp では CData になりますし、test2.cpp では CMode になってしまいます。それではいつの時点で、型が定まるのでしょうか。

test1.cpp 

CData &foo()
{
  CData left, right;
  return max(left, right);
}

const CData& max(const CData& a, const CData& b)
{
  return a > b ? a : b;
}

test2.cpp 

CMode &bar()
{
  CMode left, right;
  return max(left, right);
}

const CMode& max (const CMode& a, const CMode& b)
{
  return a > b ? a : b;
}

 上記のソースはテンプレートがコンパイラによって展開された直後のものです。赤い行は、コンパイラがテンプレートを元にして作成した関数(のイメージ)です。T の部分が置き換わっていることがわかると思います。

 このようにコンパイラは、テンプレートを利用している部分があると、そのテンプレートを元にして実際の関数を作り出してしまうのです。

 同じ関数があちこちにできてしまうこともあり、コード効率は非常に悪くなります。テンプレートを使用するとコードサイズが激増するので要注意です。このために組込み系ではテンプレートをほとんど使用できません(テンプレートの実体部分を一まとめにする機能を備えたコンパイラも存在します)。

 実際にはインライン化される場合がほとんどでしょう。

template <int N> int sum()
{
  int s = 0;
  for (int i = 0; i < N; i++)
    s += i;
  return s;
}

void foo()
{
  sum<16>() + sum<17>();
}

int sum<16>()
{
  int s = 0;
  for (int i = 0; i < 16; i++)
    s += i;
  return s;
}

int sum<17>()
{
  int s = 0;
  for (int i = 0; i < 17; i++)
    s += i;
  return s;
}

 テンプレートの<>の部分を、テンプレート引数と呼びます。テンプレートの正体は、テンプレート引数を置き換えるだけの仕組みです。従って上記のようなコードも書けます。

 赤い行は例によって、コンパイラが template のコードを元に自動作成したコードのイメージです。ここでは N の部分が置き換わっています。テンプレート引数の部分が置換されただけの同じ関数が2つできています。

 このようにテンプレート引数には class 以外のものも指定できるのですが、コード効率が悪くなるため、よほどの理由が無い限りこうした使い方はされません。

 こうして仕組みがわかれば、奇妙に見えたテンプレートも理解できるのではないかと思います。

 今回の記事ではテンプレートの効能について説明しませんでしたが、テンプレートが使えるのはC++の大きな魅力です。JavaやC#で使えるようになってしまったためにもはや専売特許ではなくなってしまいましたが、それでもテンプレートを利用したプログラミングはC++の大きなアドバンテージだと言えるでしょう。テンプレートを活用しないならC++の魅力は半減ですよ。


キャストとプログラムの品質

2005-07-24 22:29:23 | C++

foo(int *arg)
{
  int work = arg;
}

 もしコンパイラが上記のプログラムに対してエラーを出さなかったとしたらどうでしょうか?

 C言語とC++言語は、Pascal ほどではありませんが、型のチェックが比較的厳しいプログラミング言語です。上記の例のような、うっかり間違って想定外の変数に代入しようとしてしまうような初歩的なバグを、コンパイルの時点でチェックすることができればバグの数も自然と減ります。

 コンパイルエラーを解決していくだけで多くのバグが取れるというのは実は画期的なことです。エラーのチェックが甘い、あるいは勝手に自動変換してしまう言語もありますが、それらの言語ではやはりデバッグに苦労します。ある程度とはいえ、コンパイラがバグを取ってくれるのですから、これを有効に利用しない手はありません。

foo(int *arg)
{
  int work = (int)arg;
}

 この厳しい型チェックはよく裏目に出て不便を強いられますので、C言語ではキャストを使用して強制的に型を変換できるようになっています(上)。しかしながら、C言語のキャストはすべての型チェックを無効にできる、たいへん強力かつ危険なもので、せっかくのエラーチェックを台無しにしてしまいます。そこでC++ではキャストが拡張されました。

foo(int arg)
{
  short work = static_cast<short>(arg);
}

  このC++で拡張されたキャストは、一見無意味で面倒なだけのようなものに見えるかもしれませんが、本当に無駄ならこのような拡張はなされないわけです。拡張されたキャストはバグを未然に防ぐ強力なツールとなります。上記の static_cast は、キャストをつけなくてもワーニングで済む種類のキャストに対してのみ使用できるキャストです。const 属性の変数に対する代入や、ポインタへの変換などは、代入が許されずにエラーとなります。

 C++には static_cast の他にも、const_cast、dynamic_cast、reinterpret_cast という4種類のキャストが用意されています。static_cast についてはすでにお話ししましたので、残りの4つを解説していきましょう。

 const_cast は const を無効化するためのキャストです(逆もできますけど)。C言語でのプログラミングにある程度精通されている人でも、const を使ったことが無いという人は多いかもしれません。const とは静的で変更不可能な定数のことです。この値はプログラムコード領域やROM領域などに置かれます。const を宣言した変数に対して変更を加えるとコンパイルエラーとなります。メモリが豊富なパソコンやワークステーションではエラーチェックされてもあまりうまみがありませんし、define や enum で済ませたほうが都合のよいことも多いでしょう。

 C++では const の重要度が飛躍的に増しています。C言語の const とC++の const は別のものだと言う人もいます。そこで、const_cast の話をする前に const の話をしておきます。

class CFoo
{
private:
  int m_nCount;
  int m_nMax;
protected:
  virtual bool CheckCount() const
    { return true; }
public:
  int SetCount(int count)
    { return m_nCount = count; }
  bool CountDown();
};

bool CFoo::CountDown()
{
  if (!CheckCount())
    return true;
  m_nCount++;
  return m_nCount >= m_nMax;
}

 上記のクラス CFoo は SetCount で指定した値まで、CountDown を使って1つづつカウントするクラスです。

 CBar は CFoo を継承しています。CFoo と同じ動作をしますが、CheckCount を変更しています。CFoo では CheckCount が false ならカウントを強制的に終了させることができるようになっており、CBar ではこれを利用して時間制限を設けています。

class CBar : public CFoo
{
protected:
  virtual bool  CheckCount() const;
private:
  bool IsTimeOver() const;
};

bool CBar::CheckCount() const
{
  SetCount(time); //error
  CountDown(); //error
  return IsTimeOver(); // no error
}

 ところが CBar::CheckCount はコンパイルできません。const が指定されたメソッドではメンバ変数の変更が禁止されるためです。CheckCount はメンバ変数の操作が禁止されており、またメンバ変数を操作するメソッドを呼び出すことも禁止されています。メンバ変数を変更するあらゆる手段が遮蔽されているのです。アクセスできるのは、メンバ変数を変更しないことが約束されているメソッド、すなわち const なメソッドだけです。

 const を利用すると、ヘッダファイルを見るだけでメソッドがメンバ変数を変更するかどうかが一目でわかるようになるため、エラーチェック以上にバグを減らす効果があります。

Sub(char *s);

Foo1(const char *p)
{
  Sub((char *)p); // no error
}

Foo2(const int p)
{
  Sub((char *)p); // no error
}

Bar1(const char *p)
{
  Sub(const_cast<char *>(p)); // no error
}

Bar2(const int p)
{
  Sub(const_cast<char *>(p)); // error

 エラーチェックに有効な const ですが、キャストするだけで const 属性を外すことができてしまいます。関数 Foo1 は引数 *p の const 属性を取りたいだけなのですが、キャストを使用すると Foo2 のようなバグでもエラーがチェックされなくなってしまいますので、お勧めできません。

 const_cast を使用すれば、根本的な型変換をさせずに、const や volatile 属性を付け外しできるようになります。Bar2 はコンパイルエラーとなりますので比較的安全です。またあとからソースを見直したとき、このキャストが const を外すことを目的としたキャストであると一目で判断できます。
 
 先ほどの CheckCount のような、メソッドに対して課した制限を無効にすることもできてしまいますので、メソッドに const を指定しても結局あまり意味がないように思われるでしょうか。

 const メソッドを設定するのは、クラスを作成する人のためではなく、クラスを使う人のためだと考えてください。実のところ、クラスを作成する立場からみると const なメソッドにはあまり恩恵がないのですが、クラスを利用する人にとっては大変役立ちます。クラスを作る場合、それを利用する立場で const を設定していくといいでしょう。

 const なメソッドの中でも、参照カウンタの増減など利用者からは見えない秘密の仕組みを操作したいことが度々あります。そういう場合、とりあえず const メソッドにしておいて見かけ上では値を操作しないように装います。利用者から見れば、メソッドが内部の値を操作していないように見えるわけです。

 このような秘密の仕組みを操作するためには、メソッドの中で const 属性を変更する必要がありますが、そこで古いタイプのキャストを使用すると、あらゆるエラーチェックが無効になってしまいます。const_cast は通常のキャストと違って const 属性を変更する以外の変換が禁止されますので、比較的安全です。const 属性はクラスを利用する人のためのものですが、const_cast はクラスを作る人のためのものだと言えるでしょう。

 次は dynamic_cast です。dynamic_cast は通称ダウンキャストと呼ばれ、C++のキャストの中でも特に複雑な事情があります。

 サブクラス(継承先のクラス)として作成したオブジェクトを、スーパークラス(継承元のクラス)としてアクセスするのは簡単です。スーパークラスのメンバ変数や関数に対しては、特別なことをせずともそのままアクセスすることができます。しかし逆の場合はそう簡単にはいきません。

class CItem
{
public:
  virtual int GetTanka() const;
}; 
class CHammer : public CItem
{
public:
  virtual int GetTanka() const;
  int Attack();
}; 
class CPotion : public CItem
{
public:
  virtual int GetTanka() const;
  int Drink();
};
 
void Sell(CItem c)
{
  c.GetTanka();
  c.Attack();  // error!

 ある基礎データクラスを作成し、そのクラスを継承させて、すべてのデータに対して一定の操作をしたい場合があります。

 上記の例では、アイテムは必ず CItem を継承するようにしています。こうすることで、アイテムの売買を1つの処理で記述できます。

 そのクラスが CHammer だろうと CPotion だろうと、CItem を継承している限り、売買処理を記述するのは1ヶ所だけで済みます。

 ところがこの構造が裏目に出る場合があります。売買処理の中に、あるクラスだけに発生する特殊な例外処理を記述したくなったとしてもできません。上記の例のように、もし売ろうとしているアイテムが武器ならば、残された攻撃力に応じて価格を変動させたいようなことがあるかもしれません。

 こうした特殊処理は、できるならば仮想関数をうまくつかって対処できればそれにこしたことはないのですが、規模が大きくなり処理が複雑になってくるとそういうわけにもいかなくなってきます。あるクラスの事情のためだけに、数百に及ぶほかのクラスを書き換えるのは現実的ではありません。

 そこでC++ではインスタンスをスーパークラスからサブクラスにキャストするための構文 dynamic_cast が用意されています。

void Foo(CItem *item)
{
  CHammer  *pItem = (CHammer *)item;  //コンパイルは通るけど、本当に正しい?
}

 dynamic_cast を使ってキャストすれば、ヌルポインタかどうかを確認することで正しくキャストされたかどうか、つまりスーパークラスをサブクラスに変換できるかどうかを確認することができます(上記)。

void Foo(CItem *item)
{
  CHammer  *pItem = (CHammer *)item;  //コンパイルは通るけど、本当に正しい?
}

 dynamic_cast を使用しなくても、C言語のキャストを使用することで無理やりサブクラスにキャストすることができますが、これは安全ではありませんし、そのサブクラスへのキャストが正当なものかどうか判断するためには、まわりくどい仕組みを用意してやらなければならなくなるでしょう(上記)。

 ダウンキャストはたいへん強烈な印象を与える仕組みですので、変わった挙動に目を奪われがちですが、dynamic_cast が用意された本来の目的は、安全なコードを書くためだということを忘れないで下さい。C言語形式のキャストでもダウンキャストは可能ですが、dynamic_cast を使用することでコードの安全性が向上するのです。

 最後に残るキャストは reinterpret_cast という、なんとも複雑な名前のキャストです。これは主に全く異なる形式のポインタを変換するために使用するためのものです。

 C++のキャストは、キャストの目的を明確にします。const_cast なら const を外すことを目的に、static_cast なら基本的な型変換を目的にしていることが一目でわかり、コードの可読性があがります。

 こうしてC++で拡張されたキャストはいずれも直接的にプログラムの流れを制御するようなものではありませんから、これらを用いずともプログラムは作れるのですが、品質のよいプログラムを効率よく製作するためにこうした仕組みを積極的に利用すべきです。コメントをきちんと書いたり、assert を入れたりするのと同じように、キャストもC++方式を採用すれば、品質はきっと向上するはずです。