安全なコンストラクター

クラスメンバの初期化の順序に関する最近の記事では 、非常に興味深い議論が行われました。特に、クラスメンバを適切にフォーマットする方法、値で格納してコンストラクタを次のように整理する方法についての質問が議論されました:

  A :: A(int x):b(x){} 

またはここに保存します:

  A :: A(int x){b = new B(x);  } 

それぞれのアプローチには長所と短所がありますが、この記事では例外処理の問題に焦点を当てたいと思います。

順番に始めましょう。 特定のクラスがあり、そのコンストラクターが例外をスローする場合があるとします(ファイルなし、接続なし、パスワードなし、操作を実行するための十分な権限がないなど)。 私たちのクラスは非常にシンプルで予測可能です。

 クラスX {
プライベート:
   int xx;
公開:
   X(int x){
     cout << "X :: X x =" << x << endl;
     if(x == 0)throw(exception());
     xx = x;
   }
   〜X(){
     cout << "X ::〜X x =" << xx << endl;
   }
 }; 

つまり、コンストラクター引数がゼロの場合、例外がスローされます。

特定のクラスが必要で、そのオブジェクトにはクラスXの2つのオブジェクトが含まれている必要があるとします。

オプション1-ポインター付き(注意、危険なコード!)


科学のすべてを行います:

 クラスCnt {
プライベート:
   X * xa;
   X * xb;
公開:
   Cnt(int a、int b){
     cout << "Cnt :: Cnt" << endl;
     xa =新しいX(a);
     xb =新しいX(b);
   }
   〜Cnt(){
     cout << "Cnt ::〜Cnt" << endl;
     xaを削除します。
     xbを削除します。
   }
 }; 

彼らは何も忘れていないようです。 (もちろん、厳密に言えば、少なくとも、ポインターで正しく機能するコピーコンストラクターと割り当て操作を忘れていました。まあ、大丈夫です。)

このクラスを使用します。

  {
   Cnt c(1、0);
 } catch(...){
   cout << "error" << endl;
 } 

そして、いつ、いつ、それが構築され、破壊されるかを把握します。


それだけです コンストラクターは作業を停止し、Cntオブジェクトのデストラクタは呼び出されません(当然、オブジェクトは作成されていません)。 合計、何がありますか? 1つのオブジェクトX、(xa)が永久に失われるポインター。 この場所では、すぐにメモリリークが発生し、おそらくより貴重なリソース、ソケット、カーソルのリークが発生します...

これは最も不快な状況の1つであり、常に特定の引数(最初の引数がゼロではなく、2番目の引数がゼロ)でのみリークが発生するわけではないことに注意してください。 そのような漏れを見つけることは非常に困難です。

明らかに、そのようなソリューションは、非常に単純なプログラムにのみ適しています。例外が発生した場合、単純に無力になり、それだけです。

解決策は何ですか?

最も単純で、最も信頼性が高く、最も自然なソリューションは、値によってオブジェクトを保存することです。


例:

 クラスCnt {
プライベート:
   X xa;
   X xb;
公開:
   Cnt(int a、int b):xa(a)、xb(b){
     cout << "Cnt :: Cnt" << endl;
   }
   〜Cnt(){
     cout << "Cnt ::〜Cnt" << endl;
   }
 }; 

それはコンパクトで、エレガントで、自然です...しかし、主なことは安全です! この場合、コンパイラは発生するすべてを監視し、(可能であれば)不要になったすべてをクリーンアップします。

コードの結果:

  {
   Cnt c(1、0);
 } catch(...){
   cout << "error" << endl;
 } 

このようになります:

  X :: X x = 1
 X :: X x = 0
 X ::〜X x = 1
エラー 

つまり、Cnt :: xaオブジェクトは自動的に正しく破棄されました。

ポインターを使用したクレイジーな決定


次の悪夢は本当の悪夢になります。

  Cnt(int a、int b){
   cout << "Cnt :: Cnt" << endl;
   xa =新しいX(a);
   {
     xb =新しいX(b);
   } catch(...){
     xaを削除します。
    投げる
   }
 } 

Cnt :: xcが表示されたらどうなるか想像できますか? そして、初期化の順序を変更する必要がある場合は?..そのようなコードに付随して、何も忘れないように多くの努力をする必要があります。 そして、最も厄介なのは、熊手をどこにでも広げたのはあなた自身です。

例外についての叙情的な余談。


なぜ例外が発明されたのですか? プログラムの通常のコースの説明を、いくつかの障害に対する反応の説明から分離するため。

この例では、この美しい教義をひどく踏みにじっています。 例外を処理するコードを、例外の原因となるコードの近くに配置する必要があります。

これは、例外メカニズムの魅力を無効にします。 実際、Cの概念に戻ります。ここでは、各操作の後に、グローバル変数の値またはエラーの他の兆候を確認する必要があります。

これにより、コードが混乱し、理解および保守が困難になります。

リアルインディアンソリューション-スマートポインター


それでもポインターを保存する必要がある場合は、ポインターをラップすればコードを保護できます。 自分で作成することも、既存の多くのものを使用することもできます。 Auto_ptrの例:

 クラスCnt {
プライベート:
   auto_ptr <X> ia;
   auto_ptr <X> ib;
公開:
   Cnt(int a、int b):ia(新しいX(a))、ib(新しいX(b)){
     cout << "Cnt :: Cnt" << endl;
   }
   〜Cnt(){
     cout << "Cnt ::〜Cnt" << endl;
   }
 }; 

クラスメンバーを値で格納するという決定にほとんど戻りました。 ここでクラスauto_ptr <X>のオブジェクトを値で保存します。コンパイラはこれらのオブジェクトのタイムリーな削除を再び処理します(デストラクタでdeleteを呼び出す必要はありません)。 そして、それらは順番にXオブジェクトへのポインタを保存し、メモリが時間通りに解放されることを確認します。

はい! 接続することを忘れないでください

  #include <メモリ> 

auto_ptrテンプレートについて説明しています。

新しいことについての叙情的な余談


C ++のCに対する利点の1つは、C ++を使用すると、通常の変数と同様に、複雑なデータ構造(オブジェクト)を操作できることです。 つまり、C ++はこれらの構造を作成して削除します。 プログラマは、自分(プログラマ)が自分でオブジェクトを作成し始めるまで、リソースを解放することを考えないかもしれません。 「新規」を書いたらすぐに、必要な場所に「削除」を書く義務があります。 そして、これらはデストラクタだけではありません。 さらに、ほとんどの場合、コピー操作と割り当て操作を個別に実装する必要があります。つまり、C ++サービスを拒否し、非常に不安定な状況に陥っています。

もちろん、実際の生活では、しばしば「新しい」を使用する必要があります。 これは、アルゴリズムの仕様によるもの、パフォーマンス要件によって決定されるもの、または単に他の人のインターフェースによって課されるものです。 ただし、選択肢がある場合は、「新しい」という言葉を書く前に、おそらく3回考える必要があります。

すべて成功! そして、あなたの記憶が決して流れないように!

Source: https://habr.com/ru/post/J59349/


All Articles