C++ 再入門 その13 ヒープからのメモリ割り当てと返却(3) - スマートポインタとガベージコレクション
C言語のポインタ型というのは、とても便利なもので、どんなサイズのデータであっても、そのデータが格納されているアドレスを覚えておくだけでデータを読み書きすることが出来て、データを他の関数などに渡す時もデータをコピーするのではなくポインタをコピーするだけで済むという使い方が出来ます。
問題はポインタ型はアドレスが入っているだけで、その指し示す先の面倒を一切見ていないことです。ポインタが何を指し示しているかの管理はすべてプログラムを書く人に委ねられおり、言語としてチェックするのは「型」だけです。ですから、コードが正しく書かれていなければポインタは、アクセスしてはならないアドレスであっても何の躊躇も無く読み書きしてしまいます。ある程度、経験を積んだプログラマであれば、普通に使うポインタ型の変数を正しく処理できるようになるものですが、他の関数から受け取ったアドレスが正しいかどうかはわからないものですし、何らかのエラーや異常が起こった場合に正しくポインタが処理されているのかと言うと怪しいことも多いです。よく問題になるのが、ポインタ変数と指す先の変数の有効範囲(寿命)が異なることが良くあり、指す先の変数が動的に確保されている場合にポインタがローカル変数だったりすると、メモリを解放する前に関数を抜けてポインタ変数がなくなって、もう解放できなくなったり、指す先を解放した時に同時にポインタ変数を無効化しておかないと、もう解放して無効になった領域を使ってしまうこともあります。
C++では、この自由すぎるポインタ型を扱いやすくするための参照型というのが導入され、その実体はポインタに過ぎないのですが、変数宣言の部分で初期化する時に参照先を指定して行い、その参照先を変更することが出来ないという文法上の制限を行うことにより(変更すればコンパイルエラーとなる)、プログラマがうっかり初期化をしないで、その変数を使うことを防いでいます。また変数から、その指す先を取り出すときにも普通の変数と同じような書き方が可能になっており参照(ポイント)していることを意識せずに使えるようになっています。その意味で参照は元の変数の別名であるという説明がなされています。もっとも参照型と参照する変数の間の有効範囲の違いは自動的に処理してくれるわけではなく、今までのポインタ型にコピーすることも出来るので、これでポインタ型にまつわる問題がすべて解決するとはいかないのが残念なところです。このポインタ型と参照型の違いはC++を学ぶ人が良く混乱するところでもありますし、非常に似た動作を行うので正確に扱わなくても結構、コンパイルが通ってしまうこともあり、あまり褒められた使い方をしていないコードにも良く出会います。
C++ 再入門 その12 ヒープからのメモリ割り当てと返却(2) - new と delete
またC++ではオブジェクト(クラス)のコンストラクタとデストラクタの機構があるので、動的に確保した領域を変数の寿命と同期して確保したり解放したりする書き方が容易になったのですが、今度は例外処理という機構のお陰でコードが最後まで実行されずに他の関数が呼び出され、そこで抜けなく処理を書かずに実行を継続すると、本来であれば解放されるはずのメモリが残ってしまったり、解放してしまった筈の領域を指す参照やポインタに値が入ったままになってしまうという事故も発生します。まあ問題なのはデストラクタがいつ実行されるのかはコードをいくら眺めてもわからないのが言語仕様なので、無効になっている筈のポインタが予想以上に長く生き残っていて、もう死んでいる筈のメモリをアクセスしても平然としていたのに、ほんの数行コードをイジっただけでデストラクタを実行するタイミングが変わってバグが発覚するなんて言うことも経験しました。そこでメモリを確保、解放するだけのコンストラクタやデストラクタから呼び出すメソッドを明示的に書いて、こちらの想定したタイミングで実行されるように四苦八苦することもあります。
こんな風にC++になってもメモリ管理には悩まされ続けるのですが、オブジェクトやオブジェクトが使うメモリがいつ有効になって無効になるのかは本当に難しいのです。特にポインタの指す先はひとつしか無いのに、ポインタはいくらでも宣言してコピーできるので、その指す先を解放する時には必ずすべてのポインタを無効にしなければなりません。これを論理的に手作業で管理するのは特にコードをメンテナンスした時には忘れることもあり、非常に手間がかかります。これを何とかするためにいくつかのメモリ管理手法が導入されました。
最初に考え出されたのが auto_ptr というもので、これはテンプレートで実装されており、必要な型をテンプレートの引き数で渡して使うクラスとして実装されています。使う時には autp_ptr に続き<>で囲った型を書くことで特殊なポインタを宣言し初期化することが出来ます。このポインタが有効範囲を抜けて無効になった時には明示的に delete を呼び出さなくても auto_ptr のデストラクタが面倒を見てくれるので、解放漏れが無くなるというスグレモノです。面白いのが所有権という概念があり、この型のポインタをコピーすると所有権がコピー先に移り、元のポインタは無効になるという特徴があります。ですからさっきまで使えていたポインタがコピーすることにより使えなくなるということが発生します。具体的な使い方は以下を見てください。
auto_ptr クラス
C++11スマートポインタ入門
この auto_ptr は実際に使ってみると少々力不足でいくつかの問題があり、新しい処理系では非推奨になりました。互換性のために今でも使うことは出来ますが、それは auto_ptr の特性を良く理解している場合のみで出来れば新しく導入されたポインタ型に移行すべきものです。新しく導入されたのは unique_ptr、shared_ptr、weak_ptr の3種類で、auto_ptr は基本的には unique_ptr で置き換えることになります。
C++20スマートポインタ入門
shared_ptr で新しく導入されたのは参照カウンタという仕組みで、これはWindowsユーザであればお馴染みかもしれないCOMインターフェースの AddRef、Release と似た仕組みです。ポインタをコピーした際には必ず参照カウンタを増やし、そのポインタが無効になる時には参照カウンタを減らす作業を行い、カウンタが0になったら安心してポインタの指す先を解放できるという仕組みです。これが出来るまでは自分で参照カウンタを実装していたのですが、何かの拍子に参照カウンタの数が狂うと永久にメモリが解放されなかったり、まだ使っているのに解放されたりするので、例外のトラップなどでかなり慎重に気を使う必要がありました(そのクラスのデストラクタで残っている参照が無いかのチェックを行ってバグを探していました)。
参照カウント
リファレンスカウント
これらのポインタ型の説明では「所有権」という概念が登場し、これをどう扱うかをキチンと設計する必要があります。この概念は Rust などではより重要になり、今はポインタ型に必須の考え方になっています。しかしながら、こんな面倒な管理が必要なのはもう少し何とかならないかと思うのが普通で、一部の言語では古くから使われているガベージコレクションの仕組みがC++にも導入されるようになりました(BASICの文字列変数は古くからガベージコレクションが使われている例です)。
ガベージコレクション
ガベージコレクションの実装にはいくつかの種類があるのですが、複数のポインタが相互に参照している(循環参照)といつまでも参照しているポインタが無くならないので永久に解放されないという問題もあって、これを避ける工夫が必要になります。またガベージコレクションの対象となるメモリとそうではない今までの使い方のメモリを明確に区別する必要があり、複数のメモリ管理手法を混ぜるとロクなことにはなりません。一般的には処理系によって単一のメモリ管理手法が使われるのですが、他の言語で作られたライブラリ関数を呼び出したりすれば、必ずしも同じメモリ管理が使われているとは限らないので、どちらが使われているポインタ型であるかは意識し続ける必要はあります。
C++でもWindowsでは一般的に使われている.NET環境を導入すればC++であってもJavaやC#で使われているガベージコレクション機能を借用することも出来るようになっています。これを使う時に追跡参照(^%)という構文が追加されていて、ポインタ自身の参照を取り出すことが出来たりします。急にこの構文を見るとびっくりすることがあるかもしれません。.NETはMicrosoftの共通言語基盤で他のプラットフォームでも使えるようになりましたが、限られたコンパイラでしかサポートされていないので、移植性に難があるという問題があります。もっとも、その実装はBASIC時代から蓄積されたノウハウが詰まっているので、なかなかのスグレモノではあります。
C++/CLI
もちろん移植性の問題もあるので、.NET環境を嫌うまたは却って面倒になるという向きもあり、ライブラリとして提供されているパッケージを使うことが多いです。
Boehm GC を使う
いずれのせよC++でGCを使う時の問題は「混ぜるな危険」で、GC配下にあるメモリとそうでないメモリを細心の注意を払って区別し続けなければなりません。とはいえ使ってみるとやっぱり便利なんですよね。
さて、次回はメモリ割り当ての最後として、自力でメモリ管理を行い、それをオブジェクトに割り当てるための placement new(配置new)を取り上げるつもりです。
ヘッダ画像は、以下のものを使わせていただきました。
https://commons.wikimedia.org/wiki/File:ISO_C%2B%2B_Logo.svg
Jeremy Kratz - https://github.com/isocpp/logos , パブリック・ドメイン,
https://commons.wikimedia.org/w/index.php?curid=62851110による
#プログラミング #プログラミング言語 #プログラミング講座 #CPP #C言語 #ヒープ #動的メモリ割り当て #ポインタ #参照 #スマートポインタ #リファレンスカウント #ガベージコレクション