ポインタ指向スタイルとは

以下、論文調でちょっといかめしいですが、ある程度C++の使用経験がある方を対象に、 背景にある問題意識とその解決方針をできるだけ明確にまとめておくために書いたものです。

Cをマスターし、これからC++に挑戦しようとされている方や、 何度か挑戦したけれど、どうも良いコードが書けず悩んでいる方向けには、別途ドキュメントを作成する予定です。

  1. 問題の所在
  2. プログラミング・スタイルの検討
  3. 値指向スタイル
  4. ポインタ指向スタイル

  1. 問題の所在
  2. C++は非常に複雑な仕様をもつ言語です。

    1998年にISO/ANSIによる言語とライブラリの標準化[1] が成り、 その後数年(!)かかって主な処理系の対応が終わった後も、アプリケーション開発の現場にC++が使用される機会は増えたようには思えません。

    むしろJavaによる開発がJ2EEの仕様の複雑化と実行速度の問題で混乱した場合でも、それらをC++によって解決しようという動きにはならず、POJOに代表されるようなJava内部からの解決を図るといったことは、C++の今後に大きな課題を示しているように思えます。

    たとえば、現在ニーズの高いWeb環境やデータベースを活用したアプリケーションの開発を考えた場合、プログラミング言語については、プロジェクトの要件によって、それぞれの長所を活かす形で、


    といった住み分けが考えられます。
    しかし最後のケースは概して大規模になりやすく、開発者の確保がJavaに比べて困難なことから、要件を抑えてJavaで実施されるケースが多いのが現状でしょう。

    本来ならC++の特長が活かせるケースで、開発者が育たず実装も困難であるため、開発プロジェクトが成立しないのは、大変残念な情況です。

    しかし、こうした情況の中でも、C++の標準ライブラリは、過剰な実装を強いるというような批判には有効な対策を打ち出さすことはできませんでした。むしろテンプレートによるメタ・プログラミングなど、新たな要素を取り入れ、さらにスタイルのバリエーションを増やすといった、現場のプログラマーからはますます遠いところを目指してしまっているようです。
    言語やライブラリの設計者の立場からすれば、バリエーションを増やすことは適用の可能性を広げる望ましいことになるのかもしれません。しかしプログラマーの立場からすると、過度の機能や複雑さは開発を困難にするだけのものです。

    さらに問題を難しくしているのは、こうしたライブラリの拡張が標準という形で提供されることです。

    標準化されることで重複する努力が集約され、多くの人々が関わることで高品質のものが出来ていくことは素晴らしいことです。しかし一方で、標準化が、商業的な利益が大きいことから通商上の戦略に使われたり、標準以外の可能性の芽を摘み取ってしまう危険性をもつことも意識しておく必要があります。
    また、標準化された技術そのものも情況に応じて工夫改良をしていくような流動性を失いがちになります。利用者側が、標準だけを視野にいれ、問題が生じてもそれを克服できない状況は避けなければなりません。
    標準を尊重する一方で、適切なオルターナティブを育てる努力をするということも、(特に日本のように過度に標準に依存する傾向がある環境では)重要なことだと思います。

    多少話しを大きくしすぎましたが、問題の目立つようになってきた標準ライブラリを使ったプログラミングに対して、より現場の要求を汲みいれることができるオルターナティブを確立し、C++の持つ長所を引き出して、もっと活用できる機会を増やすことが必要だと考えます。

    以下では、C++の複雑さに戻り、C++がCを出発点に様々な仕様を徐々に加えながら進化してきたことに加え、「値指向」と「ポインタ指向」の2つのプログラミング・スタイルの軸をもったことが問題の根元にあることを確認した上で、有効な解決策を探っていきます。

  3. プログラミング・スタイルの検討
  4. C++の難しさ

    言語仕様が複雑で、標準ライブラリの肥大化がしていくなかで、多くのプログラマーが短い期間で理解できる良いC++コードの作成方法を確立する、これが課題です。

    そこでまず、C++の言語仕様にある本質的な難しさについて、代入を例にとって他の言語と比べて検討し、その中から有効な方法を探ることにします。

    	a = b
    

    Cの場合、たとえばBasicのような型づけの甘い言語に比べて、プログラマーは変数の型を強く意識する必要があります。特に変数がポインタであった場合、

    	const char*	a;				/* A. */
    	const char*	b = "hello, world!";		/* B. */
    
    	a = b;						/* 1. */
    
    	if ( b ) {
    		a = ( char* )malloc( strlen( b ) + 1 );	/* 2-1. */
    		strcpy( a, b );				/* 2-2. */
    	}
    

    Cを学習中のプログラマーが最初に躓(つまず)くのがこの 1. と 2-2. の違いでしょう。

    Basicでは 1. の構文で a も "hello, world!"という値を持つと考えることができたのが、Cでは「ポインタ」と「指し先の領域」をはっきりイメージすることが求められます。
    こうした混乱は、B. の初期化構文が 1. と同じように見える点が原因になっているのかもしれません。
    B. の"hello, world!"が const char* 型の14バイトの領域を指すポインタだと意識でき、2-2. の前に 2-1. の領域確保が必要であることが理解できるようになってはじめて、正しいCのプログラムを書くことができるわけです。

    しかし、アプリケーションがGUIやDBを取り込んで、ある程度複雑なデータを扱うようになると、文字列の処理にすら「型」や「領域の管理」を意識しなければならないのは、普通のアプリケーション・プログラマーには酷な要求になってきます。

    C++では、クラスの導入によってこの問題に応えることができるように思えます。

    	string	a;
    	string	b = "hello, world!";	/* 1. */
    	string	c = b;			/* 2. */
    
    	a = b;				/* 3. */
    

    stringクラスの変数は領域へのポインタでなく、 int などのCの基本型と同じように値を持つと考えてプログラムできます。

    しかし、この「同じように」扱うために、上記 1. ~ 3. には仕組みが必要になります。

    	string( const char* );
    	string( const string& );
    	string& operator=( const string& );
    

    実際に良いC++プログラムを書こうと思うと、プログラマーは、代入といった極めて基本的な処理でさえ、 パフォーマンスの維持や、例外への対応のために、こうした暗黙のうちに動く仕組みを意識する必要にせまられます。(たとえば1.のコードは、const char* からstringを作って、さらにコピー代入演算子による代入を行います)
    さらに、その仕組を実現するために、リファレンスやオペレータ・オーバーロードといった新しい機能が導入され、仕様の肥大化が始まりました。Cに慣れたプログラマーがC++でクラスを自ら作成する場合は、こうした仕組みをC++に導入された機能を使って作りこむ必要があるため、アプリケーションとは関係のない処理を無理やり書かされている感覚がぬぐえません。

    クラスだけでなく、継承関係、仮想関数、テンプレートといった機能はたいへん強力なものですが、仕様の肥大化は指数的にプログラマーの負担を増す結果となっています。

    実際 B.Stroustrup のC++の教科書[4]は英文で1000ページを越え、標準ライブラリも仕様だけで700ページを越えています。[5]

    加えて、言語仕様にもライブラリにも、単純に組み合わせて使うと陥る落とし穴や、素直には推測できない実装上の詳細が数多くあるため、良いC++のコードを作ろうと思うと、少なくともS.Meyersの3部作[6][7][8] 程度には目を通して、それらを理解する必要があります。

    こうした情況下で、日々実務に追われるアプリケーション・プログラマーがC++を使うには、膨大な仕様を絞り込んで使いこなせるようにするためのプログラミング・スタイルが必要になります。
    しかし、現在市販されているC++の解説書のほとんどが、こうしたスタイルの絞込みには触れることなく、膨大な機能の説明に終始しており、しかも落とし穴や詳細まで解説することもできていません。
    貴重な時間を削って解説書を読みきったにもかかわらず、まともなプログラムを書くためのスタートラインにも立てずに、あまりCと変わらないコードをC++コンパイラにかけて、依頼主にC++を使っていますというのが精一杯では、やりきれません。

    これに対してJavaでは、クラスの変数は、はじめからC/C++のポインタに相当するものとして実装されます。これによりポインタと参照先の区別を常に意識する必要がなくなり、ガベージ・コレクションを採用することで領域の管理もシステム側で担うようにしています。

    フレームワークやパターンの議論に目を奪われがちですが、クラス・ライブラリの肥大化やパフォーマンスに問題を抱えながらも、大規模なアプリケーション・システムの構築でJavaが採用される背景には、こうしたプログラマーにとっての負担の軽さが大きく影響しているように思えます。

    では、システム・プログラマーの場合はどうでしょうか?

    Cの場合、型の意識と領域の管理の問題さえしっかり押さえれば、非常にシンプルな仕様で、アセンブラに近いレベルの動作を想定しながら、関数や構造体を使って抽象度の高いプログラムが作成できるといった特長をもちます。
    オブジェクト指向が広く普及してもCが使用されつづけるのは、C++の肥大した仕様がCのシンプルさに劣るからにほかなりません。

    値指向(value-based)とポインタ指向(pointer-based)

    では、利用しやすいプログラミング・スタイルを確立するために、複雑なC++の仕様をどう絞り込めばよいのでしょうか?

    まず、可能な絞り込みを見極めるため、C++の発展に応じた可能なスタイルのレベル分けをしてみます。

    C++で可能なスタイル
    スタイル値指向ポインタ指向備考
    Cプログラミング 構造体やクラスの関数への引渡しは、ポインタを使用する
    クラスの利用 コピー・コンストラクタとコピー代入演算子の導入で、インスタンスを直接関数に引渡し、代入することを可能にした
    仮想関数の利用 仮想関数は派生元のクラスへのポインタかレファレンスを使用して呼び出すが、レファレンスは必ず初期化が必要で参照先も変更できないため、ポインタを使用しないプログラムは現実的ではない
    テンプレートの利用テンプレートは型の一致による機能であるため、値かポインタか、には依存しない
    STLの利用 STLのコンテナは、ポインタを格納した状態でコピー・コンストラクタやコピー代入演算子が呼び出されると、リソース・リークや不正な参照が起こるため、通常は値を格納して使用するか、boostライブラリなどが提供する共用ポインタ・クラスを介して使用する必要がある
    標準ライブラリの拡張 STLは値指向のスタイルを貫いていたが、テンプレートを使ったメタ・プログラミングの登場で、スタイルが多様化してしまった

    Cのスタイルでコーディングするレベルをはじめとして、その機能を取り入れることでスタイルが変更されることがあるものを順に取り出してみても、少なくとも6レベルあることが分かります。

    さらに、それぞれのレベルに、前節の例でとりあげたCの char* を使うケースのように、ポインタを活かす「ポインタ指向(pointer-based)」スタイルか、C++の std::string を使うケースのように、値を活かす「値指向(value-based)」スタイルかを当てはめてみると、それらが混在していて、スタイルを2分していることが分かります。

  5. 値指向スタイル
  6. まず、標準ライブラリをベースに「値指向」でスタイルを絞り込むことを考えてみましょう。

    絞り込みによって、たとえばSTLコンテナにstd::auto_ptrを格納できないことなど、プログラマーを惑わせる落とし穴のいくつかを回避するようなスタイルを作ることは可能でしょう。しかし、落とし穴が散在しているため、軸になるスタイルを作っていろいろな場面で適用するというより、スタイルが例外への対応リストになってしまい、プログラマーの習熟速度を上げることができるかどうか、疑問が残ります。

    さらに以下のような問題があり、実際に長期にわたって検討をしましたが実現には至っていません。

    結局、「値指向」スタイルによる方法では仕様の絞り込みはむずかしく、膨大な仕様を使いこなせるまでの時間と才能をもつプログラマーにしか対応できないように思えます。

  7. ポインタ指向スタイル
  8. ポインタ指向スタイルは、標準ライブラリが追求してきた値指向スタイルへのオルターナティブとして、Cのポインタを使うスタイルにオブジェクト指向の機能を取り込むものです。

    ポインタと指し先の領域を意識して、領域を管理しなければなりませんが、この2点さえ押さえてしまえば落とし穴が少なくシンプルなプログラミングを可能にする、そういったスタイルを目指します。

    具体的な検討に入る前に、その前提として、データクラスと処理クラスの区別にふれておきましょう。

    オブジェクト指向といっても、結果が決められていないシミュレーションを行うようなアプリケーション以外では、純粋にオブジェクトがメッセージを交換しあうモデルは採用できないでしょう。要件で決められた目的を達成するアプリケーション・プログラムでは、その処理手順をなんらかの方法で制御することになります。
    C++の場合、Cの手続き型言語の特徴をそのまま継承しているので、main() 関数から始まるトップダウンの関数呼び出しが、そのまま処理手順の制御になります。

    こうした手続き型言語では、プログラムの変更に対する強度が、変数のスコープをできるだけ局所化することによって決まります。

    Cでは、ファイル(ヘッダ・ファイルとその実装)と関数(宣言と実装)の2つのレベルでインターフェースと実装を分離することが可能で、さらにブロックによってスコープを制限することができました。
    C++のクラスは、処理手順の制御の観点から見ると、Cのファイルと関数の間に位置する抽象データ型を作る仕組みと考えることができます。
    実際、Cではファイル内にスコープを制限したグローバル変数とすることしかなかったデータを、C++ではそれらをクラスのメンバ変数としてより局所化することが可能です。

    このように、クラスを処理で使用される変数の局所化ツールとして使うものを、処理クラスと呼ぶことにします。
    処理クラスは、通常その処理が必要となるブロックで1つ宣言され、コンストラクタで初期化処理を行い、メンバ関数がメンバ変数をクラス内でグローバルな変数として処理し、デストラクタで終了処理をおこないます。

    これに対して、Cの構造体として扱われるデータに、初期化、参照、更新、終了などの処理を加えてクラス化したものをデータクラスと呼ぶことにします。

    データクラスは、通常、処理の過程でヒープ内に置かれるため、0個の場合も含め、その数は一定しないのが普通です。リストなどのコンテナに格納されてまとめて処理されることも少なくありません。

    クラスをその使用方法から処理クラスとデータクラスに分けて考えることで、ポインタ指向のプログラミング・スタイルをより明解に示すことが可能になります。

    以下にその主な特徴を列挙してみましょう。

    こうした特徴に加えて、値指向スタイルで問題になったCによるインターフェースを提供している既存のライブラリとの親和性も高く継承関係と仮想関数を使ったコードにも素直に対応できるため、実際のプログラム開発に有効なポインタ指向プログラミング・スタイルの確立は、十分に可能であると考えられます。

    そのためには、値指向スタイルに軸を置く標準ライブラリ、特にコンテナを含むSTLへのオルターナティブが必要になります。

    今回、「道具箱」として開発していくライブラリは、将来的に、標準ライブラリのオルターナティブとして受け入れられるものの核になることを、目指したいと思います。


    参考文献

    1. ISO/IEC 14882: Programming Language -- C++, First ed. 1998-09-01
    2. ISO/IEC PDTR 19768: Proposal Draft Technical Report on C++ Library Extensions, 2005-01-17
    3. Boost C++ Libraries, http://www.boost.org
    4. Bjarne Stroustrup: The C++ Programming Language, Special ed., Addison-Wesley 2000
    5. Bjarne Stroustrup: The Design and Evolution of C++, Addison-Wesley 1994
    6. Scott Meyers: Effective C++, 3rd ed., Pearson Education 2005
    7. 第3版では、従来のC++標準の普及を目指すことから、 複雑化したC++標準を使いこなすにはどうすればよいかという、姿勢の変更が見られる。 STLが極めて特殊なライブラリであるとする記述などから、ポインタ指向プログラミングのヒントを得ることができた。
      2006/04に第3版の邦訳が出版された。

    8. Scott Meyers: More Effective C++, Addison-Wesley 1996
    9. Scott Meyers: Effective STL, Addison-Wesley 2000
    10. Eric S. Raymond: The art of Unix Programming, Pearson Education 2004
    11. システム・プログラマーの立場からみたプログラミング言語の評価の中で、C++の複雑さに手厳しい批判を浴びせている