Flat Leon Works

アプリやゲームを作ってます。

【C++】弱参照のすすめ

弱参照というものをご存知でしょうか。弱参照はとても便利で個人的にも気に入っている仕組みなのですが、この便利さがあまり知られていないような気がします。そこで今回は弱参照の良さを紹介したいと思います。

(注意)

この記事はC++を前提としています。他のプログラミング言語だとまた事情が変わってきます。

弱参照とは

弱参照を知るにはガベージコレクションについて知る必要があります。ガベージコレクションとは不要になったオブジェクトを自動で削除する仕組みのことです。このガベージコレクションでオブジェクトの削除の判断に使われるのが参照です。ガベージコレクションはオブジェクトが参照されている間は削除せず、オブジェクトへの参照がなくなったら削除します。そして、弱参照はガベージコレクションが参照とみなさない参照です。オブジェクトがいくら弱参照されていても、通常の参照がないのであればガベージコレクションはそのオブジェクトを削除します。このように、ガベージコレクションにおいてオブジェクトの生存期間を延命させない参照を弱参照と呼びます。

C++での弱参照

C++には言語自体にはガベージコレクションの機能がありませんが、C++11から標準ライブラリにスマートポインタという形でガベージコレクションが導入されました*1

先ほど説明したガベージコレクションでの「参照」と「弱参照」は、C++のスマートポインタでは以下のような対応になります。

スマートポインタでの参照方法
種類スマートポインタ
参照(強参照)std::shared_ptr
弱参照std::weak_ptr

ところで、C++にはすでにT&という記法による参照が存在します。ガベージコレクションの「参照」と混同しないように、この記事の以降の文章ではガベージコレクションでの「参照」を「強参照」と記述します。またT&の参照は「参照(T&)」と記述します。

記事中の「参照」の使い分け
用語説明
強参照ガベージコレクションでオブジェクトの生存期間を延命させる参照
弱参照ガベージコレクションでオブジェクトの生存期間を延命させない参照
参照(T&)C++言語機能の参照
参照一般的な意味での参照

弱参照の使い所

弱参照は強参照あるいはガベージコレクションの文脈で名前が出ることが多いので、強参照の代わりに使うという印象があります。例えば、強参照ではオブジェクトが延命されてしまうのでそれを避けるために弱参照を使う。あるいは、強参照の天敵である循環参照を回避するため部分的に強参照の代わりに弱参照を使うなどです。ですが、弱参照の一番の使い所はそこではないのです。

オブジェクトの削除を検知できる参照としての弱参照

弱参照の一番の使い所は、オブジェクトの削除を検知したい場合です。弱参照は参照先のオブジェクトが削除された場合に参照が無効化されるので、弱参照を使うことでオブジェクトがまだ生きているかどうかを知ることができます。C++での通常の参照(T&)やポインタではこれができません。通常の参照(T&)やポインタではオブジェクトが削除された場合、それを知ることができないので削除されたオブジェクトに不正にアクセスすることになってしまいます。これはダングリングポインタと呼ばれ、C/C++での危険な罠の一つになっています。

ダングリングポインタを回避する方法は弱参照だけではありませんが、弱参照を使った方法が手軽で確実かつコストパフォーマンスも良いと思います。

弱参照で「オブジェクトの削除を検知する」vs 強参照で「オブジェクトを延命させる」

弱参照を「オブジェクトの削除を検知する」ために利用できると述べましたが、安全性のため、つまりダングリングポインタを回避するためということであれば、強参照によって「オブジェクトを延命させる」という回避策もあります。では、「オブジェクトの削除を検知する」のと「オブジェクトを延命させる」のはどちらが良いのでしょうか。

弱参照で「オブジェクトの削除を検知する」メリット

私は基本的には、弱参照で「オブジェクトの削除を検知する」方を選ぶのが良いと思います。弱参照を選ぶ理由、あるいは強参照を選ばない理由は以下のとおりです。

  • オブジェクトの所有権は明確にしたほうがよい
  • 延命されたオブジェクトへの処理は意味がないことが多い
  • 強参照はメモリによくない

オブジェクトの所有権は明確にしたほうがよい

強参照の仕組みは本質的には「オブジェクトの共有」です。複数の者がオブジェクトの共有を行うことになります。これはオブジェクトの所有権が曖昧になることでもあります。オブジェクトの所有権が曖昧だとオブジェクトの寿命も曖昧になり、オブジェクトがいつまで生存しているのか予想がつかなくなります。これはプログラムの不具合の原因となる可能性があります。

もちろん、オブジェクトの所有権を明確にすることよりも、所有権を曖昧にしてオブジェクトを確実に生存させることの方が重要な場合もあります。そのような場合は強参照を使ったほうがいいのですが、そういう状況は少ないのではないかと思います。

延命されたオブジェクトへの処理は意味がないことが多い

本来削除されているべきオブジェクトが強参照により延命させられていた場合、そのオブジェクトへの処理は基本的には意味がないものです*2。オブジェクトが延命されたことによりプログラムは落ちないかもしれませんが、意味のない処理を行うのは無駄ですしそこからバグが発生する可能性もあります。

オブジェクトが「延命させられている」のではなくの「意図して生存させている」のであれば問題ありませんが、先ほども言ったようにそのような状況は少ないと思います。

強参照はメモリによくない

強参照はメモリにとって良くないことが多いです。

  • 意図せずメモリに残り続ける可能性がある
  • 循環参照によりメモリリークが発生する可能性がある
  • メモリの断片化を引き起こしやすい

意図せずメモリに残り続ける可能性がある

強参照で参照されているオブジェクトは、どこか1箇所からでも参照されている限り削除されません。そのため、意図せずオブジェクトが生存を続けてしまう可能性があります。当然、それは無駄にメモリを食うことになります。

弱参照の場合、オブジェクトの延命を行わないので、意図せずオブジェクトを生存させてしまうことによる無駄なメモリ消費はありません。

循環参照によりメモリリークが発生する可能性がある

C++の強参照であるstd::shared_ptrには、循環参照があるとオブジェクトが永遠に削除されないという問題があります。オブジェクトが削除されないということは永遠にメモリに残り続けることになるのでメモリリークとなり最悪の場合、メモリ不足でプログラムが落ちます。

弱参照の場合は、そもそもオブジェクトの延命が行われないので循環参照をしていても問題ありません。

メモリの断片化を引き起こしやすい

メモリは確保と解放をスタックのようにLIFO(後入れ先出し)にしないと断片化していきます。メモリの断片化はメモリが潤沢な環境ではたいした問題になりませんが、ゲーム機などメモリが少ない環境だとメモリの確保ができなくなり致命傷となってしまいます。

強参照によるオブジェクトの延命はメモリの解放の順番を変えてしまうため、容易にメモリの断片化を引き起こしてしまいます。

弱参照を使う場合はオブジェクトが延命されないので、強参照のようにオブジェクトが延命されることによるメモリの断片化は発生しません。ただし、弱参照でも参照カウントなどを管理するデータ領域は弱参照同士で共有するため、その管理データ領域が延命され、メモリの断片化が発生する可能性はあります。これを防ぐためには管理データ領域用のメモリ確保を工夫する必要があります。*3

強参照の使い所

弱参照のメリットが多いとはいえ、もちろん強参照にも使い所はあります。

  • 参照先のオブジェクトの生存を保証する
  • オブジェクトの所有権を扱う

参照先のオブジェクトの生存を保証する

強参照で参照されている間、オブジェクトは絶対に破棄されません*4。つまり、強参照で参照している間は確実にオブジェクトへアクセスできるわけです。これは強参照の大きなメリットです。弱参照では、参照している間にオブジェクトが削除される可能性を考慮してプログラミングを行う必要があります。

オブジェクトの所有権を扱う

強参照はオブジェクトの所有権を保持/複製(共有)/放棄/移動*5することができます。これらは弱参照ではできないことです。

まとめ

  • 弱参照を使うことでオブジェクトの削除を検知できる
  • 弱参照を使うことで不正な参照(ダングリングポインタ)を回避できる
  • 強参照でも不正な参照は回避できるが、弱参照にはメリットが多い
  • もちろん強参照を使うべき場面もある

(補足)std::weak_ptrの使い勝手の悪さについて

C++での弱参照といえばstd::weak_ptrですが、残念ながら「オブジェクトの削除を検知するための参照」としては使い勝手が悪いです。

  • std::shared_ptrからしstd::weak_ptrを作ることができない*6
    • つまり、std::shared_ptr管理下にないオブジェクトや自動変数からstd::weak_ptrを作り出すことができない
  • std::weak_ptrから直接参照先にアクセスできない(std::shared_ptrを経由する必要がある)

このようにstd::weak_ptrstd::shared_ptrに強く結びついており、手軽には扱えないようになっています。おそらく、std::weak_ptrはオブジェクトの削除を検知するためではなく、std::shared_ptrの弱点を補うために用意されたクラスなのでしょう。まぁ本来、弱参照はそういうものですし。

弱参照の仕組み自体はシンプルなので、「オブジェクトの削除を検知するための参照」が欲しい場合は自作するという手段もあります。(追記: 自作方法についての記事を書きました → 【C++】弱参照クラスを自作する)

*1:C++のスマートポインタはガベージコレクションとみなされない場合もありますが、ここではガベージコレクションとして扱います。

*2:オブジェクトが処理対象なのではなく、処理を補佐するためのものならそうとも限りませんが

*3:std::weak_ptr の場合は、その元となる std::shared_ptr の作成時に std::make_shared ではなくアロケータを指定可能な std::allocate_shared を使い独自のアロケータを渡すことでメモリの断片化を防ぐことができるかもしれません。

*4:強参照を正しく使っている限りは

*5:所有権の複製と放棄を組み合わせれば所有権の移動になる

*6:もちろんstd::weak_ptrの複製は可能