Rubyist のための他言語探訪 【第 11 回】 C++

著者:まつもとゆきひろ

C++

今回はオブジェクト指向言語としては古典の一つと言っても良い C++ について紹介しようと思います。 とはいえ、この連載で今まで紹介してきた他の言語と違って C++ に関する紹介は世にあふれていますから、ここではあまり知られていない (かもしれない) C++ の歴史や思想などもからめて紹介します。

C プラス……

C++ は AT&T (当時) の Bjarne Stroustrup によって開発された C with Classes の後継言語です。 Bjarne は Cambridge 大学の学位論文を当初 Simula で快適に書いていたのですが、Simula のプログラムはデータ量が増加するにつれ処理時間が長くなりすぎてしまったので、実行性能の理由からプログラムを BCPL で書き直さざるを得ませんでした。 BCPL は C の先祖にあたる言語で、データ型も整数しかないような低レベル言語です。 そこで、Bjarne は Simula のオブジェクト指向プログラミング機能と、C の高速性を兼ね備えた言語として C++ を開発しました。

C++ は C との互換性を重要視し、C ほとんどそのままにいくつかの機能を追加しました。

クラスと継承
構造体 (struct) にクラス (class) という別名を与えました。また、class は継承ができるようにしました。出自からして Simula の直接の影響を受けている C++ では継承関係にあるクラスをベースクラス (base class) と導出クラス (derived class) と呼びます。ただし、本連載では Ruby との比較のため、Ruby の流儀にならいスーパークラスとサブクラスと呼ぶことにします。
new 演算子
クラスのインスタンスは new 演算子によって生成することができます。C++ にはガーベージコレクタが提供されませんから、インスタンスは明示的に開放する必要があります。それには delete 演算子を使います。
可視性
クラス (と構造体) のメンバにどこから見えるかどうかを指定できるようにしました。可視性にはどこからでも見える public、自分自身とサブクラスから見える protected、自クラスからしか見えない private の 3 種類があります。
メンバ関数
C++ ではクラスメンバとして関数を定義することができ、それはいわゆるメソッドとして動作します。効率を重視する C++ ではメンバ関数はデフォルトでは動的結合しません。virtual 宣言を行ったメンバ関数だけが、レシーバ (これも Ruby 用語です。C++ ではメッセージセンドというモデルを採用していませんので、厳密にはレシーバは存在しません) の動的な型に応じてメンバ関数の選択が行われます。
関数オーバーロード
C++ では引数の数や型に応じて同名の関数を複数定義することができます。これを関数オーバーロードと呼びます。
演算子オーバーロード
C++ では演算子も再定義できます。複素数型の乗算を「*」演算子で行うなど、データ型の自然な拡張が可能です。

C のいいところも悪いところもそのまま取り込んで、オブジェクト指向という新しいスタイルを提供しようと言う言語が C++ です。

静的型オブジェクト指向言語の必須機能

個人的なことで、あまり知られていないのですが、私は Ruby の開発を始めるずっと前、学生時代にはむしろ静的型のオブジェクト指向言語の擁護者でした。 私の好みだったのは C++ ではなく、Eiffel だったのですが。 当時はオブジェクト指向言語と言えば Smalltalk (や CLOS) に代表される動的型の言語が主流で (もっともオブジェクト指向と言う考え方そのものがまだマイナーだったのですが)、それに対抗して新しい考え方を応援するつもりだったのです。 Eiffel の設計者である Bertand Meyer の著書『‘Object-Oriented Software Construction’』を読んで強い影響を受けただけだという説もありますが。 いずれにしても動的言語の権化のような Ruby の設計者にもそんな過去があったということで。

そんな私が静的型のオブジェクト指向言語について考察を行った結果、「まともな」静的型オブジェクト指向言語には以下の二つの機能が必要であると言う結論に達しました。

  • 多重継承 (multiple inheritance)
  • 総称型 (generics)

静的型言語は型が適合しなければポリモルフィズムを発揮できませんから、ある機能を呼び出すためにはかならず共通のスーパークラスを持つ必要があります。 しかし、そのような共通のスーパークラスがひとつに決まるとは限りませんから、静的型言語において継承の重要な役割である機能共有を制限無く実践するためにはなんらかの多重継承が必要です。 これは DuckTyping が可能な動的言語にはない制約です。

また、コンテナクラスのような「他のクラスを格納するクラス」によって、せっかくの静的型チェックが疎外されないためには、総称型、つまり「型をパラメタに取る型」が必要です。 これはつい最近まで総称型を持たなかった Java がコンテナクラスからオブジェクトを取り出すたびに Object の参照になってしまうため、キャストによって動的にチェックしなければならなかったことを考えるとわかりやすいでしょう。

当初の C++ にはこの二つが両方とも欠けていましたから、1980 年代後半当時、オブジェクト指向好きの間では、C++ の評価は「性能は出るし、実用的かもしれないけど、オブジェクト指向言語としては二級品」というものだったように思います。

しかし、そのような評価をそのままにしておかないのが C++ の良いところ (恐ろしいところ) です。 バージョンを重ねるにつれ、C++ は上記の多重継承も、総称型も両方とも提供するようになりました。 しかも、大方の予想を越えるレベルで。

C++ の多重継承

C++ の多重継承はデフォルトではスーパークラスを重複して持ちます。 A というクラスがあり、B クラスと C クラスがそれぞれ A クラスを継承していた時、B と C を両方継承した D クラスの構造体としてのメンバレイアウトは

A に属するメンバ (B から見える)
B に属するメンバ
A に属するメンバ (C から見える)
C に属するメンバ
D に属するメンバ

になります。

CLOS 流の多重継承に慣れた身からは、A に属するメンバが重複するなんて「なんととんでもない」と感じますが、実用性と実行性能という観点からは納得できます。 D クラスのオブジェクトを B クラスのオブジェクトとして扱う時には単純継承同様そのままポインタを渡せば良いわけですし、C クラスのオブジェクトとして扱う時には単純にオフセットを加えるだけですみます。

CLOS 流の同一のスーパークラスは共有される継承が行いたければ、継承時に明示的に virtual を指定します。 実行速度が低下する選択肢に対して明示的に virtual 指定が必要なところはメンバ関数の virtual 指定と似ています。 C++ が一貫した設計原理に則っていることをうかがわせます。

総称的プログラミング

もうひとつの総称的プログラミングはテンプレートによって実現されました。 テンプレートは Eiffel などに見られる総称型と C がもともと持っていたマクロの中間のような不思議な存在です。 当初多くの人が「とりあえず総称型は実現できるが、なんだか不格好な機能」と感じたはずです。

しかし、C++ のテンプレートは、ここからほとんどの人が想像もしなかった方向に発展していったのです。

まず、テンプレートの基本から見てみましょう。大変ベタな例ですが、テンプレートを使ってスタッククラスを定義するとしましょう。 そのクラス宣言は以下のようなものになるに違いありません。

template <class T>
class Stack {
  T* s;
  int size;
public:
  void stack(void);
  void push(T);
  T pop(void);
};

これにより T として任意の型 (クラスに限らない) を格納できるスタックが出来上がります。 使い方は

Stack<int>* stack = new Stack<int>;

stack->push(5);
int v = stack->pop();

のようになります。 総称型としてはごく普通の使い方だと思います。

しかし、テンプレートをうまく使うと動的言語における DuckTyping と同様のことを行うことができます。 次の Ruby プログラムは DuckTyping の例としてよく用いられるものです。

def log_puts(out, message)
  out.write(message)
  out.write("\n")
end

log_puts は出力先として out オブジェクトが文字列一つを引数に取る write メソッドを持つことしか要求しません。 ですから、出力先が実際のファイルであるか、あるいは StringIO であるか、はたまた ユーザ定義の「なにか」であるかを問いません。 静的言語では静的な型チェックが行われるため、out がある特定の型に属している必 要があり、そのためあらかじめ共通のクラス (または Java であればインタフェース) を継承するように設計する必要があります。

と、言われていたのですが、C++ では log_puts 関数はテンプレートを使って以下のように書くことができます。

template<class OUT, class MESG>
log_puts(OUT out, MESG message) {
   //...実装は省略...
}

こうすれば、out に任意の型を取る log_puts 関数が実現できます。 さらに、out に指定する型が必要とする操作を定義していなければ、コンパイル時にエラーになります。

総称型プログラミングは大変さまざまな可能性が開けています。最近の C++ 標準規格には、オブジェクト指向よりもむしろ総称型指向のライブラリ STL (Standard Template Library) が含まれていますし、さらに挑戦的な Boost C++ Library では、lambda などの関数型プログラミングを含めた今まで見たこともない世界が展開されています。

C++ の設計思想

Bjarne Stroustrup 自身による『‘The Design and Evolution of C++’』では、C++ の設計目標と設計原則として以下のようなものがあげられています。

設計目標

  • C++ は本格的なプログラマにとってプログラミングをもっと楽しいものにする
  • C++ は次の資質を持った汎用プログラミング言語である:
    • ベター C
    • データ抽象型をサポートする
    • オブジェクト指向プログラミングをサポートする

設計原則 (抜粋)

  • C++ の進化は現実の問題をその動因とする
  • 完全主義にはこだわらない
  • C++ は今現在役に立つ言語でなければならない
  • 人に何かを強制しない
  • 静的タイプシステムに対する暗黙の違反が無い
  • 同じ機能なら人に教えやすい方を選ぶ
  • C のプリプロセッサは使わないように
  • C との正当でない非互換性が無い
  • C++ 以外に他の低レベル言語を必要としない (アセンブラは除く)
  • 使わない機能はコストを発生させない

すばらしい設計原則です。 この本全体を通じて、Bjarne の正直で誠実で一貫性のある態度は尊敬に値します。 また、彼は C++ の長年の開発過程にあったことをきちんと記憶 (記録) して、紹介しています。 このような態度は見習いたいものです。 私が将来『Ruby の設計と進化』というタイトルの本を書くことがあったとしても、これだけの情報を集めて整理することなどできそうにありません。 私はとてつもなく忘れっぽいのです (えへん)。

C++ はなぜ嫌われるか

このように、一貫性のある設計原則と長年の経験に裏打ちされた C++ はすばらしい言語のように思えます。 実際、私でさえ『‘C++ の設計と進化’』を読み返した直後には、(過去の悲惨な経験を忘れて)「もう一度 C++ を使ってみようかな」という気になりましたから。一瞬ですけど。

しかし、一方で C++ はかなり嫌われている言語でもあります。 これだけ熱心に設計されているのに嫌われてしまうのはいったいどうしたことでしょう。 まあ、広く使われている言語は、どうしても悪く言われるものではありますが、C++ の不人気さは相当です。

バベル案内より引用。

C++

C++はバカであり、バカな言語で賢いシステムを書くことはできない。言語は世界を形作る。バカな言語はバカな世界を作る。

こんなことを言うのが異端であるのはわかっている。 固有名詞の異端だ。 私だって大学生のときは C++ が好きだったのだが、それは私がそれしか知らなかったからだ。

えらい言われようです。

これはおそらくは C++ が低レベルを切り捨てることができなかったからではないでしょうか。 C++ は C の代替物として誕生した故に組み込みプログラミングのような低レベル層でも使えるよう、実行性能を犠牲にするような機能 (たとえばデフォルトでポリモルフィズム を適用するとか、ガーベージコレクションの採用とか) を導入することができませんでした。 しかし、その抽象化機能により、もともと C++ が対象するよりももっと高レベルな領域でも使われています。 「C++ は速い」という事実は、他の言語を押しのけて C++ を採用する大きな動機となります。 その事実はプログラマの負担によって実現されているという別の事実の方はめったに目を向けられることがありません。

かくして、マネージャは (性能のゆえに) C++ を選択し、プログラマは性能を実現するために (生産性を犠牲にして) 日々 C++ と格闘するのです。 そのギャップこそが C++ の不人気の理由ではないでしょうか。 しかし、高レベル層をより (遅いかもしれない) 高級な言語 (たとえば Ruby とか ;-) でカバーし、低レベル領域だけを C++ をカバーするとなると、今度は C との差別化がほとんど行えなくなってしまうという皮肉な事態が待っています。

適材適所と言う言葉がありますが、C++ は適材適所の選び方が難しい言語でもあります。

しかし、希望もあります。 現時点では C++ はもっとも進歩した総称型プログラミング言語です。 たとえば、Boost C++ ライブラリは古典的な C++ 観からは想像もできないような総称型プログラミングの世界を見せてくれています。

オブジェクト指向機能もある総称型プログラミング言語。 これがこれからの C++ の立ち位置になるのかもしれません。

C++ の情報

C++ の情報は世の中にあふれていますが、その中でも未来を感じさせてくれる以下のものを紹介します。

‘C++ の設計と進化’』 Bjarne Stroustrup (原題『‘The Design and Evolution of C++’』)
C++ の各機能について「なぜそのようにしたか」、「どう変化してきたか」という他では手に入らない情報が満載です。言語設計者による文章の模範といってもよいと思います。
Boost C++ ライブラリ
C++ の新しい世界を開く (かもしれない) ライブラリです。本文中で紹介した関数型プログラミングだけではなく、日付操作、スレッド機能など、ごく普通 (で、すごく便利) な機能も満載です。

著者について

matz.jpgまつもとゆきひろは自他ともに認める日本を代表する言語オタクです。 言語好きが昂じて自分の言語を設計してしまった大馬鹿者です。 が、オタクとかハッカーとか呼ばれる人種はみんな多かれ少なかれそんなものじゃないでしょうか。

バックナンバー