この記事は、インテル® デベロッパー・ゾーンに公開されている「The Ultimate Question of Programming, Refactoring, and Everything」の日本語参考訳です。
8. デストラクター内の例外は危険
この問題は、LibreOffice プロジェクトで見つかりました。このエラーは、次の PVS-Studio 診断によって検出されます。
V509 The ‘dynamic_cast
virtual ~LazyFieldmarkDeleter() { dynamic_cast<Fieldmark&> (*m_pFieldmark.get()).ReleaseDoc(m_pDoc); }
説明
プログラムで例外がスローされると、スタックのアンロールが開始され、オブジェクトはデストラクターの呼び出しによって破棄されます。スタックのアンロール中に破棄されるオブジェクトのデストラクターがそのデストラクターを離れる別の例外をスローすると、C++ ライブラリーは terminate() 関数を呼び出して直ちにプログラムを終了します。これは、デストラクターは例外を外に出してはならないという規則に基づきます。デストラクター内でスローされた例外は、そのデストラクター内で処理されなければなりません。
上記のコードは危険です。dynamic_cast (英語) 演算子は、オブジェクト参照を要求された型へキャストできない場合 std::bad_cast 例外を生成します。
同様に、例外をスローできるほかの構造も危険です。例えば、デストラクターで new 演算子を使用してメモリーを割り当てることは安全ではありません。失敗した場合、new 演算子は std::bad_alloc 例外をスローします。
正しいコード
上記のコードは、dynamic_cast を参照ではなく、ポインターとともに使用するように修正できます。そうすることで、オブジェクトの型を変換できない場合、例外を生成せずに nullptr を返します。
virtual ~LazyFieldmarkDeleter() { auto p = dynamic_cast<Fieldmark*>m_pFieldmark.get(); if (p) p->ReleaseDoc(m_pDoc); }
推奨事項
デストラクターはできるだけ単純にします。デストラクターは、メモリー割り当てやファイル読み取りのためのものではありません。
場合によっては、デストラクターを単純にできないこともありますが、努力はすべきです。また、複雑なデストラクターは、一般にクラス設計の貧弱さを示しており、準備不十分なソリューションと言えます。
デストラクターのコードが増えると、すべての可能性のある問題に対応することが難しくなります。例外をスローできる領域とできない領域を区別するのが困難になります。
例外が発生する可能性がある場合、次のように catch(…) を使用して抑止すると良いでしょう。
virtual ~LazyFieldmarkDeleter() { try { dynamic_cast<Fieldmark&> (*m_pFieldmark.get()).ReleaseDoc(m_pDoc); } catch (...) { assert(false); } }
これにより、デストラクター内のいくつかの問題を隠蔽できるだけでなく、多くの場合アプリケーションの動作が安定します。
私は、デストラクターで全く例外をスローしないように主張しているわけではありません。すべては特定の状況に依存します。状況によっては、デストラクターで例外を生成したほうが良いこともあります。実際、特別なクラスでそのようなケースを目にしたことがあります。しかし、それらはまれなケースです。私が目にした特別なクラスでは、オブジェクトを破棄するときに例外を生成するように設計されていましたが、”own string”、”dot”、”brush”、”triangle”、”document” などの一般的なクラスでは、デストラクターから例外をスローすべきではありません。
二重の例外はプログラムの終了を引き起こすため、よく考慮して決定を下す必要があります。
コンパイラーの最適化に関する詳細は、最適化に関する注意事項を参照してください。