C++を思い出す・・・#5
今回の話題はPointer variableがデータメンバとして定義されている場合のデフォルトコピーコンストラクタの挙動について。
ヤバイ。超基礎的な事忘れてる。生成されるデフォルトコピーコンストラクタと代入演算子がメンバのポインタ変数に対してシャローコピーするのを完全に忘れてた。数年触れてなかったにしてもヒドイ。参考書漁っておいて良かった…
— 五味 拓樹 (@530506) 2016年7月13日
C++を書く者としては死ぬ程恥ずかしいド忘れだけど、二度と忘れないようにブログに綴る事にした。
struct X{ X()=default; X(const X&)=default; X& operator=(const X&)=default; X(X&&)=delete; X& operator=(X&&)=delete; void set_value(const int arg){a=arg;} void show()const{std::cout<<*ptr<<std::endl;} private: int a=10; int*ptr=&a; }; int main() { X obj; X obj1(obj); obj1.set_value(200); obj1.show(); // 10 }
obj1の∗ptrは10である。何故ならば、デフォルト生成されるコピーコンストラクタはシャローコピーをするからだ。意図した通りに動作させるにはディープコピーを実装しなければならない。
struct X{ X()=default; X(const X& src_obj){*ptr=*src_obj.ptr;} X& operator=(const X& src_obj) { *ptr=*src_obj.ptr; return *this; } X(X&&)=delete; X& operator=(X&&)=delete; void set_value(const int arg){a=arg;} void show()const{std::cout<<*ptr<<std::endl;} private: int a=10; int*ptr=&a; }; int main() { X obj; X obj1(obj); obj1.set_value(200); obj1.show(); // 200 X x[3]; x[0].set_value(999); x[1]=x[2]=x[0]; for(auto&& temp:x)temp.show(); // 999 999 999 x[0]=x[0]; }
当然の事のだけに、忘れていた自分にとてもショックだ。セマンティック的に、ポインタで間接参照し代入しているが、元データでもこの場合は構わない。
この関連で自己代入時に何もしないというセマンティックを表すために、以下のようなチェックを入れる事が以前あった。
X& operator=(const X& src_obj) { if(this!=&src_obj)*ptr=*src_obj.ptr; return *this; } X x; x=x; // do nothing
しかしこれもよくよく考えればif文自体にもコストが掛かっているので微妙なところがあるな〜と思った。度々自己代入が起きない限りはこういった記述は必要ないのかもしれない。
これが理解できていないと、以下のようなコードを書く羽目になる。
struct X{}; struct Z{ Z(X* p):obj_ptr(p){} Z(const Z&)=default; Z& operator=(const Z&)=default; ~Z(){delete obj_ptr;} Z(Z&&)=delete; Z& operator=(Z&&)=delete; private: X* obj_ptr; }; int main() { Z z(new X()),z1(new X()); z=z1; }
z.obj_ptrに元々あったアドレスは、デフォルト定義されるoperator=によってシャローコピーされるため、上書きが成される。よって、zでnewしたメモリ領域にはどこからも参照できないため、z.obj_ptrはdeleteできない。また、上書きされてしまった事により、z.obj_ptrとz1.obj_ptrは同じアドレスを指すポインタとなる事によって、デストラクタにより同じメモリ領域を二度deleteしてしまう。
struct X{}; template<class _Tp> struct Z{ Z(X* p):obj_ptr(p){} Z(const Z& rhs) { _Tp* p=new _Tp(); *p=*rhs.obj_ptr; delete obj_ptr; obj_ptr=p; } Z& operator=(const Z& rhs) { _Tp* p=new X(); *p=*rhs.obj_ptr; delete obj_ptr; obj_ptr=p; return *this; } ~Z(){delete obj_ptr;} Z(Z&&)=delete; Z& operator=(Z&&)=delete; private: X* obj_ptr; }; int main() { Z<X> z(new X()),z1(new X()); z=z1; }
ディープコピーをし実体をしっかり複製する事によって正しくデストラクタが呼ばれる。コピー先にあるオブジェクトを解放しておくのを忘れてしまいそうだ。z1にてnewで動的にメモリ確保している事自体が無駄だが、これはコードの整合性を保つためにも今一度newを受け付け、コピーコンストラクタやoperator=の方でdelete処理をしなければならない。
余談になるが、そういえば最近、自動生成される特別なメンバ関数たちを明示的に記述する際にautoによって戻り値の型を推定させる流れがあるようだが、operator=のオーバーロードに関しては有害とするテキストが標準化委員会の方で出されていたりしている。
auto operator= considered dangerous
まあそうだよなあ、operator=は通常*this(lvalue reference)を返すものであって、単にautoと書いてしまうとコピーになってしまう。
struct X{ auto operator=(const X&); // X operator=(const X&); auto operator=(X&&); // X operator=(X&&); };
まあ以下のように書き換えれば良いが
struct X{ auto& operator=(const X&); // X& operator=(const X&); decltype(auto) operator=(X&&); // X& operator=(X&&); };
以上、備忘録。肝に銘じよう。