読書ノートEffective_C++_条項11:operator=で自己付与を処理する

16182 ワード


直感的なoperator=は、次のように定義されます.
 1 class SampleClass

 2 {

 3 private:

 4          int a;

 5          double b;

 6          float* p;

 7 public:

 8          SampleClass& operator= (const SampleClass& s)

 9          {

10                    a = s.a;

11                    b = s.b;

12                    p = s.p;

13                    return *this;

14          }

15 };

つまり、自分のプライベートメンバーの値をすべて別のオブジェクトのプライベートメンバーに割り当てる値です.operator=が明示的に定義されていない場合、コンパイラが生成するデフォルトのoperator=は、生成された結果もこのようになります.ただし、この場合、プライベートメンバーにはポインタfloat*pが含まれており、深いコピーの目的(ポインタのアドレスをコピーせず、ポインタが指す空間内容をコピーする)を達成するために、このように書くべきである.
 1 class SampleClass

 2 {

 3 private:

 4          int a;

 5          double b;

 6          float* p;

 7 public:

 8          SampleClass& operator= (const SampleClass& s)

 9          {

10                    a = s.a;

11                    b = s.b;

12                    delete p;

13                    p = new float(*s.p);

14                    return *this;

15          }

16 };

大体の考え方はポインタが指す古い内容を削除して、それからこのポインタで新しい空間を指して、空間の内容はs.pが指す内容を埋めます.しかし、このコードがクラッシュする原因は2つあります.1つは、本条項でいう「自己付与」です.読者は考えてみてください.もしそうなら、
1 SampleClass obj;

2 obj = obj;

起こったこと.付与文の実行時にobjが検出する.pはすでに指向する、この時objを解放する.pが指す空間の内容ですが、次の言葉に続きます.
p = new float(*s.p);

注意*s.pは、s.pがobjであるため、プログラムがクラッシュする.pは、その値*objをとる.p(優先度による、これは*(obj.p)に相当する)、obj.pはすでに前の一言で解放されているので、このような操作にバグがあります.
読者は,obj=objというコードを書くほどユーザが馬鹿になるはずがないとは思わないかもしれない.実際には確かにそうですが、明らかな間違いは犯すことはできませんが、万が一次のように書きます.
1 SampleClass obj;

2 3 SampleClass& s = obj;

4 5 s = obj;

または
1 SmapleClass* p = &obj;

2 3 *p = obj;

このエラーはそれほど直感的ではなく、paとpbが同じアドレス空間を指す可能性が高いため、*pa=*pbでも問題が発生する可能性があります.自己付与はうっかりすると発生します.ユーザーが自己付与の文を書かなくてもいいとは決して仮定しないでください.
自己付与を解決するには一言だけです.
 1 class SampleClass

 2 {

 3 private:

 4          int a;

 5          double b;

 6          float* p;

 7 public:

 8          SampleClass& operator= (const SampleClass& s)

 9          {

10                    if(this == &s) return *this; //  

11                    a = s.a;

12                    b = s.b;

13                    delete p;

14                    p = new float(*s.p);

15                    return *this;

16          }

17 };

以前こう書いたことがあります.
 1 class SampleClass

 2 {

 3 private:

 4          int a;

 5          double b;

 6          float* p;

 7 public:

 8          SampleClass& operator= (const SampleClass& s)

 9          {

10                    if(*this == s) return *this; //  , !

11                    a = s.a;

12                    b = s.b;

13                    delete p;

14                    p = new float(*s.p);

15                    return *this;

16          }

17 };

ただし、==は、アドレスが重なるかどうかの判断ではなく、オブジェクト内の各メンバー変数が同じかどうかの判断に使用されることが多いため、これは正しくありません.だからthis=&sでアドレスから本当に自己付与されているかどうかをキャプチャすることができます.
このようにすれば、上記の第一の問題である自己付与を解決することができます.実際にはもう一つの問題が発生してコードがクラッシュする可能性があります.p=new float(*s.p)が正常に空間を割り当てることができなければどうしますか.突然異常を投げ出したらどうしますか.これは元の空間の内容が解放されますが、新しい内容は正常に充填できません.異常が発生した場合、元の内容を維持できる良い方法はありませんか?(プログラムの堅牢性を向上させることができる)
これには2つの考え方があります.本にはまずこのようなものがあります.
 1 SampleClass& operator= (const SampleClass& s)

 2 {

 3          if(this == &s) return *this; // 

 4          a = s.a;

 5          b = s.b;

 6          float* tmp = p; //  

 7          p = new float(*s.p); //  , ,p 

 8          delete tmp; //  , , 

 9          return *this;

10 }

大まかな考え方は古いものを保存して、新しいものを申請してみて、申請に問題があれば、古いものは保存できます.ここでは、「operatorに異常なセキュリティを持たせると、自動的に自己付与セキュリティのリターンが得られることが多い」という最初の言葉を削除することができます.
もう1つの考え方は、一時的なポインタで新しい空間を申請し、内容を記入してから、ローカルポインタが指す空間に解放し、最後にローカルポインタでこの一時的なポインタを指すということです.
 1 SampleClass& operator= (const SampleClass& s)

 2 {

 3          if(this == &s) return *this; // 

 4          a = s.a;

 5          b = s.b;

 6          float* tmp = new float(*s.p); //  

 7          delete p; //  , , 

 8          p = tmp; //  

 9          return *this;

10 }

上記の2つの方法はいずれも実行可能ですが、構造関数の中のコードとこのコードの重複性に注意しなければなりません.考えてみてください.もしこの時、クラスにプライベートなポインタ変数を追加したら、この中のコード、そして構造関数の中の類似のコードをコピーして、更新する必要があります.
本書では、最終的な解決策を示します.
1 SampleClass& operator= (const SampleClass& s)

2 {

3          SampleClass tmp(s);

4          swap(*this, tmp);

5          return *this;

6 }

これにより、コードの一貫したパフォーマンスが保証されるように、コピー構造関数に負担がかかります.コピーコンストラクション関数に問題が発生した場合、たとえばスペースを申請できない場合、次のswap関数は実行されず、ローカル変数を一定に保つ目的を達成します.
さらに最適化されたシナリオは次のとおりです.
1 SampleClass& operator= (const SampleClass s)

2 {

3          swap(*this, s);

4          return *this;

5 }

ここでは,パラメータの参照を外し,一時変数を申請するタスクをパラメータに配置し,コードを最適化する役割を果たすことに注意する.
最後にまとめます.
(1)オブジェクトが自己付与されたときoperator=良好な動作を確保し、その技術には「ソースオブジェクト」と「ターゲットオブジェクト」のアドレスの比較、周到な文順、copy-and-swapが含まれる.
(2)任意の関数が1つ以上のオブジェクトを操作し、複数のオブジェクトが同じオブジェクトである場合でも、その動作が正しいことを決定する.