「C++再考」の訳注集4
    第6、7章不要なコピーとは

 こんにちは。第5章は理解できたでしょうか。よく飛ばし読みをお勧めする私ですが、第5、6、7章は、ある意味でこの本の内容全体を予感させるものであり、また、当然基本でもあるので、是非理解することをお勧めします。もし、あなたが初心者なら、この辺を理解しただけでも、相当の進歩のはずです。がんばってください。


 ちょっと、復習から入ります。そもそも、C++にはポリモーフィズムとか多態性とか呼ばれる機能があり、それがこの言語の特徴(のひとつ)なのでした。(これは、「クラス」や「継承」とともにいわゆる「オブジェクト指向言語」の特徴でした。)
 ポリモーフィズムとは、「プログラマがあるクラスとその派生クラスを複数作っておくが、プログラムの実行時にしか使われる派生クラスがわからない。そのようなプログラムでもきちんと動作するしくみ」のことです。(なんだかわからない人は、C++入門の仮想関数のあたりを参照してください。)
 たとえば、

基底クラス:
 class Vehicle (乗り物)

派生クラス:
 class Truck: public Vehicle (トラック)
 class Helicopter: public Vehicle (ヘリコプター)
 その他、いろいろ

などとあるときに、プログラマはVehicleのポインタを用意しておけば、そのポインタで、トラックやヘリコプター(のポインタ)を表すことができる。それぞれに共通する「動作」は仮想関数にすれば、間違いなく動作する。、、、というような話でした。

 まず、このような考え方は、C++では基本であり、とても重要であることは、繰り返しておきます。その上で敢えて言うと、Vehicleのポインタを、プログラム中で直に取り扱うと、必要なときに毎回newしたりdeleteしたり大変で、いつか間違える、、、ということから、第5章の代理クラスが考えられたのです。

 第5章の最後に

 //parking_lotはSurrogateVehicleの配列
 parking_lot[num_vehicles++]=x; //xはVehicleの派生クラスのインスタンス
(または parking_lot[num_vehicles++]=VehicleSurrogate(x); )

などとあります。(ちょっと本題からずれますが、はじめの式とかっこの中の式が同じなのは、parking_lot[num_vehicles]がSurrogateVehicleであることが前に定義されているので、上のように何も書かなくても、自動的に、Vehicleの派生クラスのインスタンスであるxがSurrogateVehicleに変換されるのです。このように自動的に変換されるためには、SurrogateVehicleにVehicleを引数にとるコンストラクタがあればよいのです。知らなかった人は今覚えてくださいね。引数がひとつのコンストラクタは、データを変換するための関数と考えることもでき、しかも、必要時には自動で呼び出されるのです。今の場合は、VehicleをSurrogateVehicleに変換するのです。これは私にはとても感動的な機能ですが、話し出すとそれだけで一回分になるので、今回はこれで納得してください。)

 初心者の方は、まず、上の式が簡単きわまりないことを、十分味わってください。あまり苦労した経験の無い初心者には、感慨は無いかもしれませんが、ポインタを使わないでよい、ということは、すばらしいことなのです。(一応、注意しておくと、本当にポインタを使わないのでは、もちろんありません。ポインタはVehicleSurrogateのプライベートメンバになって、隠れているのです。)

 その上で、第5章のやり方の欠点をひとつ言いましょう。それは、(場合によっては)不要なコピーが多くなりすぎる、ということです。今回はこの点を説明したいのです。
 では、まず、コピーについて考えてみましょう。そのために、たとえば、もう一度、

 parking_lot[num_vehicles++]=x;

を見てください。これは簡単です。簡単ですが、実は、とても複雑なのです。

 簡単に言うと、背後では、まず、xがSurrogateVehicleのコンストラクタに渡され、SurrogateVehicleに変換される。次に、このSurrogateVehicleがすでに存在している別のSurrogateVehicleであるparking_lot[num_vehicles]に代入される、、、のです。(num_vehiclesがひとつ増えたり、不要になったオブジェクトが破棄されたり、、、ということもありますが、これは本筋でないので省略しました。)たいして複雑ではないですか?私はかなり複雑(難しいというのではなくまわりくどいという意味で)だと思います。
 第5章の実装(つまりコード)を見ると、変換や代入でcopyという関数が呼ばれることがわかります。p56、トラックの例を見てください。この関数は、実質、newをしてコピーコンストラクタを呼んでいるのです。(さらに言うと、xは派生クラスですが、Vehicleの何度目かの派生クラスである場合、一度のnewで基底クラスのコンストラクタが次々に呼び出されるのです。それも複雑ですね。)

 つまり、「人間にとってわかりやすい」は「コンピュータには大仕事」なのです。ここで、もし、このクラスのインスタンスがとても大きなデータを持っていたとするとどうなるでしょう。工夫の無い実装では、その大きなデータがコピー(変換や代入)のたびに、メモリ上をあっちからこっちと動き回るのです。もちろん、最後の結果は、parking_lot[num_vehicles]にVehicleの派生クラスのインスタンスが格納されただけなのに、、、です。このようなコピーが繰り返されれば、実行速度は当然遅くなっていきます。データが画像データのように巨大なものならかなり大変です。
(初心者は「実行速度なんかどうでもいいだろ」と思うでしょう。私はそれが初心者の正しい態度だと思います。でも、だんだん腕に自信がつくと、実行速度も気になるようになります。なんにしても「実行速度が速い」というのがC++の売りなんですから。)

 もちろん、これは一例にすぎません。私が言いたいことは、おそらく初心者が想像する以上に、いろいろなところでコピーが行われているということです。

 ここで、単純に、「じゃあ、コンピュータの仕事を減らすようにしよう」とは、なかなかいきません。「コンピュータに都合が良い」は大抵「人間には(とても)わかりずらい」だからです。人間にわかりやすい代理クラスの基本的な発想は活かしたいものです。そこで登場するが、第6、7章のハンドルというクラスなのです。これらのクラスでは、使用カウントというテクニックを使うことで不要なコピーを極力減らしているのです。

 第6章のはじめに「なるべく不要なコピーをなくしたい」ということが書いてあります。つまり、そういうことだったのです。

 ところで、第5章の代理クラスは役に立たないのか、それじゃ、なんのためにあったんだ、、、と思わないでください。もし、あなたが「不特定多数のお客さんが使うライブラリ」の実装をしているのなら、この章の代理クラスの実装を商売に使うことはお勧めできないでしょう。しかし、その場合でも、第6章以下の基礎訓練にはなったはずです。
 もし、あなたが利用目的のはっきりした特定のコードを書いていて、扱っているデータが大きくないのなら、この章の代理クラスで十分役に立つのではないでしょうか。そして、十分役に立つコードを書く、、、というのがC++の基本理念なのです。

 

前のページ
後のページ
訳注の目次
総目次