2016年6月22日
C++11スマートポインタで避けるべき過ち Top10
本記事は、原著者の許諾のもとに翻訳・掲載しております。
(注:2017/10/25、いただいたフィードバックを元に翻訳を修正いたしました。修正内容については、 こちら を参照ください。)
私は新しいC++11のスマートポインタをとても気に入っています。自分でメモリを管理するのが嫌だと感じる多くの仲間たちにとって、これはいろいろな面で天の助けでした。私の場合、このおかげで新人にC++を教えるのがずっと楽になりました。
しかし、C++11のスマートポインタを幅広く使っていた2年ちょっとの間で、使い方を誤ると、プログラムの効率が落ちたりクラッシュして壊れたりするという事態に何度も遭遇しました。参照用に、以下に例を載せました。
まずはこれらの”過ち”を、簡単なAircraftクラスを例に取って見てみましょう。
class Aircraft
{
private:
string m_model;
public:
int m_flyCount;
weak_ptr<aircraft> myWingMan;
void Fly()
{
cout << "Aircraft type" << m_model << "is flying !" << endl;
}
Aircraft(string model)
{
m_model = model;
cout << "Aircraft type " << model << " is created" << endl;
}
Aircraft()
{
m_model = "Generic Model";
cout << "Generic Model Aircraft created." << endl;
}
~Aircraft()
{
cout << "Aircraft type " << m_model << " is destroyed" << endl;
}
};
過ちその1: unique_ptrで十分なところにshared_ptrを使う
私が最近引き継いで作業をしたコードベースは、全てのオブジェクトの作成/運用のにshared_ptrを使っていました。コードを分析すると、このケースの9割で、shared_ptrでラップされたリソースが共有されていないことが分かりました。
これは、以下の2つの点で問題があると言えます。
- リソースの所有権を本当に1つだけにしておきたい場合、unique_ptrの代わりにshared_ptrを使ってしまうと、コードがリークやバグの影響を受けやすくなってしまいます。
-
微妙なバグ: 「リソースを別の共有ポインタに割り当ててしまったことでそのリソースが他のプログラマと共有されることになり、さらにそのポインタが意図せずリソースを変更してしまうものだった」という事態を想定せずに今までやってきていたら、恐ろしいですね。
-
不要なリソースの利用: 共有したリソースを他のポインタが変更してしまうことがないとしても、そのポインタが必要以上にリソースを保持し続け、それによって元々のshared_ptrがスコープを外れた後もあなたのRAMを無駄に独占するかもしれません。
- shared_ptrの作成は、unique_ptrの作成よりもリソースを食います。
- shared_ptrはその内部で、指示するオブジェクトのスレッドセーフな参照カウントと、制御ブロックを保持する必要があります。これにより、shared_ptrはunique_ptrよりも重くなってしまいます。
お勧め – 基本としてはunique_ptrを使うべきです。もし後からリソースの所有権を共有する必要性が出てきたら、いつでもshared_ptrに変更できます。
過ちその2: shared_ptrで共有されたリソース/オブジェクトをスレッドセーフにしない
shared_ptrを使うと、あるリソースを複数のポインタを通して共有することができ、そのリソースは基本的に複数のスレッドで使用可能になります。「オブジェクトをshared_ptrでラップすれば、それでもうスレッドセーフになる」と考えてしまうというのは、よくある間違いです。「shared_ptrで管理される共有リソースの周辺では、同期基本命令を使う」というのは、依然として自分の責任でやらなければならないことです。
お勧め – 複数のスレッド間でリソースを共有する予定がないのであれば、unique_ptrを使いましょう。
過ちその3: 自動ポインタ(auto_ptr)を使う
auto_ptrの機能は明らかに危険なので、今や非推奨になっています。ポインタを使って値を渡す時にコピーコンストラクタが実行する所有権の移動は、ポインタから参照先の値をもう一度取得する時に、システムの致命的なクラッシュを引き起こしかねません。以下の例で考えてみてください。
int main()
{
auto_ptr<aircraft> myAutoPtr(new Aircraft("F-15"));
SetFlightCountWithAutoPtr(myAutoPtr); // Invokes the copy constructor for the auto_ptr
myAutoPtr->m_flyCount = 10; // CRASH !!!
}
お勧め – unique_ptrはauto_ptrと同じ働きをします。コードベースを検索してauto_ptrを見つけ、その全てをunique_ptrに置き換えましょう。こうすればだいぶ安全ですが、コードの再テストはお忘れなく。
過ちその4: shared_ptrの初期化にmake_sharedを使わない
make_sharedには、ポインタを直接使うよりも明らかにいい点が2つあります。
1. パフォーマンス: 新たにオブジェクトを作成し、それからshared_ptrを作成する場合、2つの動的メモリの割り当てが生じます。1つめは新しいオブジェクト自体で、2つめはshared_ptrコンストラクタで作成された管理オブジェクトです。
shared_ptr<aircraft> pAircraft(new Aircraft("F-16")); // Two Dynamic Memory allocations - SLOW !!!
一方、make_sharedでは、C++コンパイラは管理者オブジェクトと新しいオブジェクトの2つを抱えられるだけの十分な大きさのメモリを1つ割り当てるだけです。
shared_ptr<aircraft> pAircraft = make_shared<aircraft>("F-16"); // Single allocation - FAST !
2. 安全性: Aircraftオブジェクトが作成されたが、何らかの理由で共有ポインタの作成が失敗したという状況を考えてみてください。この場合、Aircraftオブジェクトは削除されず、メモリリークが生じてしまいます。 MSコンパイラのメモリヘッダでの実装を見た後に気づいたのですが、割り当てが失敗すると、リソース/オブジェクトが削除されます。ですからこの種の使用法であれば、もう安全性に関する心配はありません。
お勧め – 共有ポインタをインスタンス化するにはポインタをそのまま使う代わりにmake_sharedを使いましょう。
過ちその5: オブジェクト(生ポインタ)を作成してすぐにshared_ptrに割り当てない
オブジェクトを作成したら、すぐにshared_ptrに割り当てるべきです。生ポインタは、再利用してはいけません。
以下の例を考えてみてください。
int main()
{
Aircraft* myAircraft = new Aircraft("F-16");
shared_ptr<aircraft> pAircraft(myAircraft);
cout << pAircraft.use_count() << endl; // ref-count is 1
shared_ptr<aircraft> pAircraft2(myAircraft);
cout << pAircraft2.use_count() << endl; // ref-count is 1
return 0;
}
“アクセス違反”を引き起こしプログラムがクラッシュしてしまいます。
問題は、最初のshared_ptrがスコープを外れたら、myAircraftオブジェクトが破棄されてしまうということです。2つめのshared_ptrがスコープを外れると、前に破棄したオブジェクトをまた破棄しようとします。
お勧め: shared_ptrの作成にmake_sharedを使用していないのであれば、せめてスマートポインタで管理されたオブジェクトを同じコード行に作成しましょう。以下のような感じです。
shared_ptr<aircraft> pAircraft(new Aircraft("F-16"));
過ちその6: shared_ptrで使用されている生ポインタを削除してしまう
shared_ptr.get() のAPIを使って、shared_ptrから生ポインタへのハンドルを取得できます。しかし、これはリスクが高いので避けるべきです。以下のコードについて考えてみてください。
void StartJob()
{
shared_ptr<aircraft> pAircraft(new Aircraft("F-16"));
Aircraft* myAircraft = pAircraft.get(); // returns the raw pointer
delete myAircraft; // myAircraft is gone
}
shared_ptrから生ポインタ(myAircraft)を取得した後、それを削除します。関数が終了するとshared_ptr pAircraftはスコープから外れ、すでに削除したmyAircraftオブジェクトを削除しようとします。この結果、お馴染みの”アクセス違反”が生じるのです!
お勧め – 共有ポインタ(shared_ptr)から生のポインタを取り出す前によく考えてみてください。また取り出した後も削除せず保持してください。「誰かが生のポインタからリソースを削除してしまい、それによって共有ポインタ(shared_ptr)にアクセス違反が生じてしまう」ということがいつ起こるかわかりません。
過ちその7: ポインタの配列にshared_ptrを使用する際にカスタムデリータを使用しない
次のコードについて考えてみてください。
void StartJob()
{
shared_ptr<aircraft> ppAircraft(new Aircraft[3]);
}
ここでは、共有ポインタ(shared_ptr)はAircraft[0]のみを指します。Aircraft[1]とAircraft[2]ではスマートポインタがスコープ外になってもメモリが解放されず、メモリリークが生じています。Visual Studio 2015使っている場合は、ヒープコラプトエラーを検出します。
お勧め – 常にshared_ptrで管理されているオブジェクトの配列にカスタムデリートを渡すように設定してください。次のコードで問題は解消されます。
void StartJob()
{
shared_ptr<aircraft> ppAircraft(new Aircraft[3], [](Aircraft* p) {delete[] p; });
}
過ちその8: 共有ポインタを使用する時に循環参照を回避しない
多くの場合、クラスがshared_ptrを参照先としていると、循環参照が生じます。次の2つのシナリオを考えてみてください。IcemanとMaverickによってそれぞれ操縦される2つのAircraftオブジェクトを生成します(映画『トップガン』の登場人物を使用しないわけにはいきませんでした!!! )。MaverickとIcemanはそれぞれのウィングマン(僚機)を参照しないといけません。
そのため、初期の設計ではAircraftクラス内で自己を参照するshared_ptrを導入しています。
class Aircraft
{
private:
string m_model;
public:
int m_flyCount;
shared_ptr<Aircraft> myWingMan;
…
main()では、MaverickとGooseというAircraftオブジェクトを生成し、互いをウィングマンとして参照するように設定します。 a
int main()
{
shared_ptr<aircraft> pMaverick = make_shared<aircraft>("Maverick: F-14");
shared_ptr<aircraft> pIceman = make_shared<aircraft>("Iceman: F-14");
pMaverick->myWingMan = pIceman; // So far so good - no cycles yet
pIceman->myWingMan = pMaverick; // now we got a cycle - neither maverick nor goose will ever be destroyed
return 0;
}
main()のreturnの段階で、2つの共有ポインタが破棄されて欲しいところです。しかし、互いを循環参照先とするため、破棄されません。スタックからスマートポインタはクリアされますが、2つのオブジェクトは互いを参照先としているため、オブジェクトとして存続できるのです。
プログラムを実行すると、出力結果は次のとおりです。
Aircraft type Maverick: F-14 is created
Aircraft type Iceman: F-14 is created
では、どのように修正すればよいのでしょうか? Aircraftクラス内のshared_ptrをweak_ptrに変えればよいのです! 変更後にmain()を実行すると、出力結果は次のようになります。
Aircraft type Maverick: F-14 is created
Aircraft type Iceman: F-14 is created
Aircraft type Iceman: F-14 is destroyed
Aircraft type Maverick: F-14 is destroyed
両方のAircraftオブジェクトが破棄されたことに気付いたと思います。
お勧め – リソースの所有権を必要とせず、オブジェクトの寿命を決定したくない場合は、weak_ptrを念頭にクラスの設計をするといいでしょう。
過ちその9: unique_ptr.release()で返された生ポインタを削除しない
Release()メソッドがunique_ptrの管理するオブジェクトを破棄することはありませんが、unique_ptrオブジェクトはオブジェクトを削除する責任から解放されます。このオブジェクトは手動で誰か(あなた)が削除する必要があります。
Main()が存在すると依然としてAircraftオブジェクトがまだ生きているため、次のコードではメモリリークが生じています。
int main()
{
unique_ptr<aircraft> myAircraft = make_unique<aircraft>("F-22");
Aircraft* rawPtr = myAircraft.release();
return 0;
}
お勧め – unique_ptrに対してRelease()メソッドを呼び出すときに、生ポインタを削除するのをお忘れなく。もし、unique_ptrの管理するオブジェクトを削除したい場合はunique_ptr.reset()の使用を検討してください。
過ちその10: weak_ptr.lock()を呼び出す際に、有効か否かを確認しない
weak_ptrを使用する前に、weak_ptrをlock()メソッドを呼び出して取得する必要があります。lock()メソッドは実際にweak_ptrをshared_ptrにアップグレードして使用できるようにします。しかしながら、もしweak_ptrの指すshared_ptrオブジェクトが有効でない場合、weak_ptrは空になります。破棄済みのweak_ptrに対してメソッドを呼び出すとアクセス違反が生じます。
例えば、次のコードスニペットでは、”mywingMan” weak_ptrが指すshared_ptrはpIceman.reset()を介して破棄されています。myWingman weak_ptrを介してどのアクションを実行してもアクセス違反が生じてしまいます。
int main()
{
shared_ptr<aircraft> pMaverick = make_shared<aircraft>("F-22");
shared_ptr<aircraft> pIceman = make_shared<aircraft>("F-14");
pMaverick->myWingMan = pIceman;
pIceman->m_flyCount = 17;
pIceman.reset(); // destroy the object managed by pIceman
cout << pMaverick->myWingMan.lock()->m_flyCount << endl; // ACCESS VIOLATION
return 0;
}
次のif checkを書き込むことで、myWingman weak_ptrを使用する前に破棄済みか否かを確認することができます。
if (!pMaverick->myWingMan.expired())
{
cout << pMaverick->myWingMan.lock()->m_flyCount << endl;
}
編集: 上のコードは、今や99%のソフトウェアが置かれているマルチスレッド環境では使うべきではないと、記事を読んだ多くの人に指摘されました。破棄されているかを確認してからロックがかけられるまでの間にweak_ptrが破棄されるかもしれないのです。皆さんありがとうございます! ここでは、 Manuel Freiholz の解決策を適用します。Lock()を呼び出した後、そして使用する前にshared_ptrが空ではないか確認します。
shared_ptr<aircraft> wingMan = pMaverick->myWingMan.lock();
if (wingMan)
{
cout << wingMan->m_flyCount << endl;
}
お勧め – 使用前に必ずweak.ptrが有効か確認してください。つまり、空ではない共有ポインタがlock()関数を介して返されているかコード内で使用する前に確認するのです。
次のステップは?
C++ 11のスマートポンタやC++ 11のことをもっと詳しく知りたい場合は、次の本をお勧めします。
-
『Effective Modern C++: 42 Specific Ways to Improve Your Use of C++11 and C++14』Scott Meyers著 (訳注:日本語版があります。 Effective Modern C++ ―C++11/14プログラムを進化させる42項目 )
C++ 11の冒険の旅を楽しんでください。この記事がお役に立てたかぜひ教えてください。
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa