はじめに
Effective C++ 第3版の11項 50ページから勉強していきます。
今回は、「operator= の実装では、自己代入に備えよう」についです。
Effective C++ 第3版 - 11項 operator= の実装では、自己代入に備えよう -
前置き
今回は、「operator= の実装時の自己代入」について学んでいきます。
今回の勉強
自己代入
自己代入とは以下のようなコードをいいます。
class Widget{};
Widget w;
w = w; // 自己代入
おかしなコードに見えますが、エラーにはなりません。
また、こんなコード書くわけがないと思っても、下のように配列を用いている場合に、自己代入になることがあります。
i と j が同じ値なら、これは自己代入になります。
a[i] = a[j]; // 自己代入になるかもしれない
ポインタ px と py が同じオブジェクトを指しているなら、自己代入になります。
*px = *py; // 自己代入になるかもしれない
上のように分かりにくい自己代入は、参照やポインタといった、同じオブジェクトを別の変数で表す仕組みに(エイリアス・別名と呼ばれる)よって生じます。
複数個の同じオブジェクトを参照やポインタで扱う場合、同じオブジェクトが異なる変数で表される可能性があることを考慮に入れる必要があるそうです。
また、継承の仕組みによって、下のように同じオブジェクトが異なる型の変数で表されることもあります。
class Base {};
class Derived : public Base {};
void func(const Base& rb, Derived* pd) {
std::cout << &rb << std::endl;
std::cout << pd << std::endl;
}
Derived b;
func(b, &b);
0x7ffc06f3ceff
0x7ffc06f3ceff
operator= の実装時における自己代入
ここからは、operator= の実装時における自己代入について考えていきます。
説明のために、Widget と Bitmap というクラスを作成します。
Widgetは、Bitmapクラスのオブジェクトを動的に生成し、そのオブジェクトのポインタをメンバ変数に持つとします。
以下に2つのクラスを記述します。
class Bitmap {};
class Widget {
public:
Widget();
~Widget();
Widget& operator=(const Widget& rhs);
private:
Bitmap* bm_; // Bitmapのポインタ
};
Widget::Widget() { bm_ = new Bitmap(); } // 動的なオブジェクトを生成
Widget::~Widget() { delete bm_; } // オブジェクトの破棄
それでは、operator= を実装していきます。
以下のコードは、operator= の実装として一見良さそうですが、自己代入に関しては安全ではありません。
Widget& Widget::operator=(const Widget& rhs) {
delete bm_; // 現在のBitmapを破棄
bm_ = new Bitmap(*rhs.bm_); // 右辺のBitmapをコピー
return *this;
}
このような処理では、*thisとrhsが同じオブジェクトの場合、自己代入に問題が起きます。
これは、ポインタ bm_ が指し示す Bitmapオブジェクトを破棄した時には、rhsの Bitmap(rhs.bm_)も破棄されたことになります。
operator= の処理が終わると、本来、自己代入で変化するはずがない bm_ が「破棄されたBitmapオブジェクト」を指し示すポインタを持つことになります。
この問題を解決する方法は、operator= の処理のはじめで、自己代入かどうかをチェックするというものです。
Widget& Widget::operator=(const Widget& rhs){
if(this == &rhs){ // 同一性テスト
return *this;
}
delete bm_;
bm_ = new Bitmap(*rhs.bm_);
return *this;
}
上のコードは、うまく動作します。
しかし、まだ、例外に関する危険がまだあります。
特に、new Bitmap(*rhs.bm_) でで例外が発生した場合(メモリ不足やBitmapのコピーコンストラクタで失敗)、Widgetのメンバbm_は、「破棄されたBitmapオブジェクト」を保持することになります。
この問題を解決する方法は、今の例の場合では、bm_にdeleteを適用する前に、bm_が指し示すべきオブジェクトをコピーで生成しとけば良いそうです。
Widget& Widget::operator=(const Widget& rhs) {
Bitmap* tmp = bm_; // 元のbm_の値を記録
bm_ = new Bitmap(*rhs.bm_); // bm_が*rhs.bm_のコピーを指し示すようにする
delete tmp; // 元のbm_が指し示すオブジェクトを破棄
return *this;
}
このようにすると、「new Bitmap(*rhs.bm_)」で例外が投げられても、bm_は(つまり、bm_を内部に持つWidgetも)変更されないことになります。
また、同一性テストがなくても、このコードは自己代入に問題がありません。
それは、rhsのBitmapオブジェクトのコピーを作成し、bm_がそれを指し示すようにしてから、元のbm_のオブジェクトを破棄しているからです。(自己代入に関しては、あまり効率が良くない。)
また、例外にも自己代入にも安全なコードにするため、「コピーと交換」というテクニックがあるみたいです。
このテクニックは、29項を勉強してから追記したいと思います。
サンプルコード
以下に、勉強で使用したコードを示します。
# include <iostream>
class Base {};
class Derived : public Base {};
class Bitmap {
public:
Bitmap() : x_(3) {}
void printMemb() { std::cout << "x_ : " << x_ << std::endl; }
private:
int x_;
};
class Widget {
public:
Widget();
~Widget();
Widget& operator=(const Widget& rhs);
private:
Bitmap* bm_;
};
Widget::Widget() { bm_ = new Bitmap(); }
Widget::~Widget() { delete bm_; }
/* // 1st
Widget& Widget::operator=(const Widget& rhs) {
std::cout << "\n*************** 1st ****************" << std::endl;
bm_->printMemb();
delete bm_;
*bm_;
bm_ = new Bitmap(*rhs.bm_);
bm_->printMemb(); // 未定義の動作
return *this;
}
*/
/*
// 2nd
Widget& Widget::operator=(const Widget& rhs){
std::cout << "\n*************** 2nd ****************" << std::endl;
if(this == &rhs){
std::cout << "同じポインタ" << std::endl;
return *this;
}
delete bm_;
bm_ = new Bitmap(*rhs.bm_);
return *this;
}
*/
// 3rd
Widget& Widget::operator=(const Widget& rhs) {
std::cout << "\n*************** 3rd ****************" << std::endl;
bm_->printMemb();
Bitmap* tmp = bm_;
bm_ = new Bitmap(*rhs.bm_);
delete tmp;
bm_->printMemb();
return *this;
}
void func(const Base& rb, Derived* pd) {
std::cout << &rb << std::endl;
std::cout << pd << std::endl;
}
int main() {
std::cout << "11_qiita.cpp" << std::endl;
Derived b;
func(b, &b);
Widget w;
w = w;
}
実行結果
11_qiita.cpp
0x7fffe927e25f
0x7fffe927e25f
*************** 1st ****************
x_ : 3
x_ : 0
------------------------------------------------
*************** 2nd ****************
同じポインタ
------------------------------------------------
*************** 3rd ****************
x_ : 3
x_ : 3
まとめ
今回は、operator= の実装時に気をつけるべき自己代入について学びました。
覚えておくこと
- operator= の定義は、自己代入に対して安全に振る舞うように書こう。
- そのテクニックには、コピー元とコピー先のオブジェクトのアドレスを比較する方法、
- コード(ステートメント)の順序を注意深く決める方法、
- 「コピーと交換」(29項)の方法がある。
- 複数のオブジェクトを操作する場合、異なるオブジェクトと考えられていたものが、同じオブジェクトであることがある。
- そのような場合でも、正しく動作するように関数を書く。
参考文献
[1] https://www.amazon.co.jp/gp/product/4621066099/ref=dbs_a_def_rwt_hsch_vapi_taft_p1_i0