驚くべきことに,世の C++er の中には,未だに
「自前のクラスを作る場合は,忘れずに代入演算子を定義し,自己代入チェックをしなければいけない」
という考えを持った人がいるようです.
http://d.hatena.ne.jp/nagoya313/20100706/1278428503
確かに,昔はそう言われていました.その理由は,
class String { public: char* _p; String( const char* s ){ _p = (char*)malloc( strlen(s) + 1 ); strcpy( _p, s ); } ~String(){ free( _p ); } String& operator=( String& rhs ){ free( _p ); const char* s = rhs.Get(); _p = (char*)malloc( strlen(s) + 1 ); strcpy( _p, s ); } char* Get(){ return _p; } };
このようなクラスがあった場合に,悲惨なことになるからです.
というか,言うまでもなく,こんなクラスは,
コピーコンストラクタを定義してないやら const 対応してないやら,
ツッコミどころが多すぎて悲惨ってレベルではないですが.
この blog を読みに来るような賢い C++er さんなら,当然,
そもそもこんな俺々クラスを作るようなことはせず, std::string を使うでしょう.
一般に,「コピーコンストラクタを書け」「自己代入チェックをしろ」という雑多なノウハウより,
「何も書かなければ,バグは起きない」というノウハウの方が,遥かに大事です.
これはコピーコンストラクタや代入演算,デストラクタにも通じることであり,
基本的に,コンパイラが自動定義してくれたものの方が,自分で定義したものより,安全で確実.
普通にC++を使う範囲では,実際,これらの関数を自前で定義する必要は,殆どありません.
そうするにあたって心がけることは一点,「生ポインタではなくスマートポインタを使う」こと.
それだけで,細かい気配りを覚える必要もなく,すべてがうまくいくのです.
しかし,スマートポインタを使っていても,ちょっと複雑なことをしようと思うと,
コンパイラによって自動定義された代入演算ではダメな場合が出てきます.
具体的に言うなら,一般に
「コピーコンストラクタかデストラクタを自前で定義しなければいけない場合」
には,コンパイラによって自動生成された代入演算は,不都合が起きます.
そのような場合にバグを防ぐには,やはり自己代入チェックは必要ではないか?
実を言うと,本当に限られた場合を除いては,そんなことはありません.
名著「Exceptional C++」に紹介されていたように,殆どの代入演算は,
コピーコンストラクタ呼び出しと,メンバごとの swap 関数呼び出しによって実現できます.
struct hoge { /* ... */ // メンバごとの swap void swap( hoge& other ) { using std::swap; swap( member1, other.member1 ); swap( member2, other.member2 ); /* ... */ } // 代入演算は完全に定型文 hoge& operator=( hoge const& rhs ) { hoge temp = rhs; this->swap( temp ); // あるいは,まとめて hoge(rhs).swap(*this); return *this; } /* ... */ }
この実装の優れた点は,コピーコンストラクタとswapという,より基本的な操作に,
代入操作を移譲することが出来る点です.
そして代入演算自体は単なる短い定型文,
流石に「全く書かない」には劣りますが,バグ防止としては十分でしょう.
「Exceptional C++」では「例外安全の強い保証を満たせる」ことがメリットとされていましたが,
そんなことよりも,代入操作を単純なコードで書き表せる,
それだけで十二分に強力であり,積極的に使うべきなのです.
勿論,この方法では対処し切れない場合もあります.
それは「 swap 関数を上手く定義できない」場合であり,
例えば boost::optional
しかし,そのようなクラスを自前で用意する必要があるのは,よっぽどの状況です.
通常の C++er は,代入は copy して swap する,とだけ覚えれば,それで十分でしょう.
おまけ
C++0x では,さらに良い( swap すら定義する必要がない!)方法があります.
struct hoge { /* ... */ // Move Assignment は一般にコンパイラで自動定義できる // 勿論,特殊な場合には自前で定義する必要があるが hoge& operator=( hoge && ) = default; // 本体 hoge& operator=( hoge const& rhs ) { // 「copyしてmove」. hoge temp = rhs; return *this = std::move(temp); // あるいは単に return *this = hoge(rhs); // そっちの方がオススメ.今回は分かりやすくするため二行で書いた } /* ... */ }
これだけです.
この方法のいいところは,メンバが増えた場合に, swap 関数を変更しなくても対応できる点.
なので,これからは,こっちを使っていくのが良いでしょう.