#12 クラスのコピーの仕方1 シャローコピー

ということで,Effective C++の12項の内容なんですが,この辺自分もきちんと理解しきれてなかったのでまとめてみました.

コピーの種類

オブジェクトをデザインするときに,コピーの振る舞いとか定義をどうするかを決めないといけないと思うんですが,大まかにいってコピーした時の振る舞いって2つあると思います.

シャローコピー

オブジェクトAとオブジェクトBが2つあったとして,"B = A" とか "Hoge B(A)" とかしたときに,オブジェクトAの領域の一部がオブジェクトBとシェアされます.OpenCV の cv::Mat とかは単純に = でコピーすると画像領域自体はシェアされます.オブジェクト内部に持っているデータ領域がとても大きくて,「別のメモリを確保して,全部コピーして」っていうのが不自然な場合はこちらで実装するのかと思います.シャローコピーの場合だと,オブジェクトAに対する変更がオブジェクトBに影響を与えます.

手書きで恐縮ですが,イメージとしては下記のような感じですかね.

f:id:rkoichi2001:20181215151512j:plain
シャローコピーのイメージ

ディープコピー

オブジェクトAとオブジェクトBが2つあったとして,"B = A" とか "Hoge B(A)" とかしたときに,オブジェクトAの領域とオブジェクトBの領域は完全に別物になります.OpenCVの cv::Mat では,これをやりたければ cv::Mat::clone() という関数を呼べば完全に別物の cv::Mat が作れる様になっています.あと,std::vector なんかも単純に "=" ってやると完全に別のオブジェクト(領域を全く共有してない)ができます.ディープコピーの場合だと,オブジェクトAに対する変更はオブジェクトBに全く影響を与えません.

こちらもイメージとしては下記のような感じですかね.

f:id:rkoichi2001:20181215151639j:plain
ディープコピーのイメージ

どういうときに気を付けるか?

で,実際に自分のオブジェクトを作るとき,どういうときに上記のような話に気を付けてないといけないか...ということですが,おそらく下記の2点に注意する必要があると思います.

1.自分のデザインするオブジェクトにメンバとしてポインタを持たせる場合.
2.自分のデザインするオブジェクトにメンバとしてほかのオブジェクトを持たせる場合.

1の ”自分のデザインするオブジェクトにメンバとしてポインタを持たせる場合.” ですが,こちらに関しては何も考えずに作ると「シャローコピー」&「メモリリーク」します....2の他のオブジェクトを内包するコンテナを作る場合,内包するオブジェクトのコピー戦略がどうなっているかちゃんと調べる必要があります.例えば,OpenCVのcv::Matをメンバとして持つコンテナを作る場合,デフォルトのコピー代入演算子を使ってコンテナのコピーをした場合,cv::Matの指すメモリ領域は共有されます.これが自然なばあいはこれでOKですが,ディープコピーが必要な場合は cv::Mat::clone() を呼ぶ等して適当なコピー代入演算子を作ってあげる必要があります.

まずはシャロコピーのサンプルのコードとして,下記を作って見ました.

#include <iostream>

class ShallowCopyContainer {
public:
  ShallowCopyContainer() 
    : arr(new int[ARR_LENGTH])
  {}

  ~ShallowCopyContainer()
  {
    delete[] arr;
  }

public:
  static const int ARR_LENGTH = 100;

private:
  int* arr;
};

int main(int, char**) {
  
  ShallowCopyContainer contA, contB;
  contB = contA;

  return 0;
}

上記のコードを下記の様に実行します.

valgrind --leak-check=full ./chap_2nd_12_1

すると,ガッツリメモリリークします.

==4928== Memcheck, a memory error detector
==4928== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==4928== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==4928== Command: ./chap_2nd_12_1
==4928==
==4928== Invalid free() / delete / delete[] / realloc()
==4928==    at 0x4C2F74B: operator delete[](void*) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==4928==    by 0x400CD8: ShallowCopyContainer::~ShallowCopyContainer() (chap_2nd_12_1.cpp:12)
==4928==    by 0x400C01: main (chap_2nd_12_1.cpp:25)
==4928==  Address 0x5ab6c80 is 0 bytes inside a block of size 400 free'd
==4928==    at 0x4C2F74B: operator delete[](void*) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==4928==    by 0x400CD8: ShallowCopyContainer::~ShallowCopyContainer() (chap_2nd_12_1.cpp:12)
==4928==    by 0x400BF5: main (chap_2nd_12_1.cpp:25)
==4928==  Block was alloc'd at
==4928==    at 0x4C2E80F: operator new[](unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==4928==    by 0x400CA3: ShallowCopyContainer::ShallowCopyContainer() (chap_2nd_12_1.cpp:7)
==4928==    by 0x400BD0: main (chap_2nd_12_1.cpp:25)
==4928==
==4928==
==4928== HEAP SUMMARY:
==4928==     in use at exit: 73,104 bytes in 2 blocks
==4928==   total heap usage: 3 allocs, 2 frees, 73,504 bytes allocated
==4928==
==4928== 400 bytes in 1 blocks are definitely lost in loss record 1 of 2
==4928==    at 0x4C2E80F: operator new[](unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==4928==    by 0x400CA3: ShallowCopyContainer::ShallowCopyContainer() (chap_2nd_12_1.cpp:7)
==4928==    by 0x400BDC: main (chap_2nd_12_1.cpp:25)
==4928==
==4928== LEAK SUMMARY:
==4928==    definitely lost: 400 bytes in 1 blocks
==4928==    indirectly lost: 0 bytes in 0 blocks
==4928==      possibly lost: 0 bytes in 0 blocks
==4928==    still reachable: 72,704 bytes in 1 blocks
==4928==         suppressed: 0 bytes in 0 blocks
==4928== Reachable blocks (those to which a pointer was found) are not shown.
==4928== To see them, rerun with: --leak-check=full --show-leak-kinds=all
==4928==
==4928== For counts of detected and suppressed errors, rerun with: -v
==4928== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)

で,”何がいけなかったのか?”ですが,C++でデフォルト生成されるコピー代入演算子は組み込み型の変数を単純コピーします.なので,"int* arr" というポインタ変数に関しても単純にアドレスを contB オブジェクトのアドレス変数にコピーします.この振る舞いに関しては,シャローコピーでいいなら問題はないはずです.で,contAとcontBで同じ領域がシェアされて,めでたしめでたし....となるはずなんですが,上の作りだと contB にもともと確保されていた領域 "new int[ARR_LENGTH]" への参照が全くなくなってしまい,メモリリークしてしまいます.つまり,デフォルトの代入演算子だとまずいわけです....では,ちょっと書きなおしてみます.

#include <iostream>

class ShallowCopyContainer {
public:
  ShallowCopyContainer() 
    : arr(new int[ARR_LENGTH])
  {}

  ~ShallowCopyContainer()
  {
    if (arr != nullptr) {
      delete[] arr;
      arr = nullptr;
    }
  }

  ShallowCopyContainer& operator=(const ShallowCopyContainer& rhs) {

    // 自分だった場合は,そのまま自分を返す.
    if (this == &rhs) {
      return *this;
    }

    // もともと自分が持っていた領域を delete[] してリークを防ぐ.
    delete[] this->arr;
    this->arr = rhs.arr;
    return *this;
  }

public:
  static const int ARR_LENGTH = 100;

private:
  int* arr;
};

int main(int, char**) {
  
  ShallowCopyContainer contA, contB;
  contB = contA;

  return 0;
}

上記の変更で,コピー代入演算子が呼ばれた時には自分が持っていた領域を解放するようになるので,メモリリークはなくなりました.ただ,,,まだ二重解放の問題が残っています.というのも,デストラクタで必ず delete が呼ばれるので,contA のデストラクタが呼ばれた時に解放された領域を,contB のデストラクタが再度解放してしまおうとするからです.参照カウントとか導入するとどんどん話が難しくなっていくので,もっと簡単に対処できる方法を模索してみたかったんですが,,,結局 Smart Pointer を使っちゃいます(笑)

#include <iostream>
#include <memory>

class ShallowCopyContainer {
public:
  ShallowCopyContainer() 
    : arr(new int[ARR_LENGTH], std::default_delete<int[]>())
  {}

  ~ShallowCopyContainer()
  {}

  ShallowCopyContainer& operator=(const ShallowCopyContainer& rhs) {

    // 自分だった場合は,そのまま自分を返す.
    if (this == &rhs) {
      return *this;
    }

    this->arr = rhs.arr;
    return *this;
  }

public:
  static const int ARR_LENGTH = 100;

public:
  std::shared_ptr<int> arr;
};

int main(int, char**) {
  
  ShallowCopyContainer contA, contB;
  contB = contA;

  for (int i = 0; i < ShallowCopyContainer::ARR_LENGTH; i++) {
    contA.arr.get()[i] = 99;
  }

  for (int j = 0; j < ShallowCopyContainer::ARR_LENGTH; j++) {
    std::cout << "coutA.arr[" << j << "] : " << contA.arr.get()[j] << std::endl;
    std::cout << "coutB.arr[" << j << "] : " << contB.arr.get()[j] << std::endl;
  }

  return 0;
}

結局 STL のパワーに頼ってしまいましたが,上記の実装で Container A の中身を変えれば Container B の中身も変わる”シャローコピー”ができました.で,次にもうひとつ,コピーコンストラクタの存在も忘れてはいけません.と入っても,結局コピー代入演算子の時に対面したような問題が出てくるだけなので,それを再度踏まないように実装してあげる必要があります.ということで,結局スマートポインタに頼ります.

#include <iostream>
#include <memory>

class ShallowCopyContainer {
public:
  ShallowCopyContainer() 
    : arr(new int[ARR_LENGTH], std::default_delete<int[]>())
  {}

  ShallowCopyContainer(const ShallowCopyContainer& obj)
    : arr(obj.arr)
  {}

  ~ShallowCopyContainer()
  {}

  ShallowCopyContainer& operator=(const ShallowCopyContainer& rhs) {

    // 自分だった場合は,そのまま自分を返す.
    if (this == &rhs) {
      return *this;
    }

    this->arr = rhs.arr;
    return *this;
  }

public:
  static const int ARR_LENGTH = 100;

public:
  std::shared_ptr<int> arr;
};

int main(int, char**) {
  
  ShallowCopyContainer contA;
  ShallowCopyContainer contB(contA);

  for (int i = 0; i < ShallowCopyContainer::ARR_LENGTH; i++) {
    contA.arr.get()[i] = 99;
  }

  for (int j = 0; j < ShallowCopyContainer::ARR_LENGTH; j++) {
    std::cout << "coutA.arr[" << j << "] : " << contA.arr.get()[j] << std::endl;
    std::cout << "coutB.arr[" << j << "] : " << contB.arr.get()[j] << std::endl;
  }

  return 0;
}

で,ここでちょっと気づいたんですが,SmartPointer使っていれば,自分でコピー代入演算子・コピーコンストラクタを定義しなくてもいいのでは?とおもったんで,やってみました.結果,ビンゴでした.シャローコピーを実現するだけなら,SmartPointerを使ってやればコピー代入演算子もコピーコンストラクタも自作する必要はありませんでした.

#include <iostream>
#include <memory>

class ShallowCopyContainer {
public:
  ShallowCopyContainer() 
    : arr(new int[ARR_LENGTH], std::default_delete<int[]>())
  {}

  ~ShallowCopyContainer()
  {}

public:
  static const int ARR_LENGTH = 100;

public:
  std::shared_ptr<int> arr;
};

int main(int, char**) {
  
  {
    ShallowCopyContainer contA;
    ShallowCopyContainer contB(contA);

    for (int i = 0; i < ShallowCopyContainer::ARR_LENGTH; i++) {
      contA.arr.get()[i] = 99;
    }

    for (int j = 0; j < ShallowCopyContainer::ARR_LENGTH; j++) {
      std::cout << "coutA.arr[" << j << "] : " << contA.arr.get()[j] << std::endl;
      std::cout << "coutB.arr[" << j << "] : " << contB.arr.get()[j] << std::endl;
    }
  }

  {
    ShallowCopyContainer contA, contB;
    contB = contA;

    for (int i = 0; i < ShallowCopyContainer::ARR_LENGTH; i++) {
      contA.arr.get()[i] = 101;
    }

    for (int j = 0; j < ShallowCopyContainer::ARR_LENGTH; j++) {
      std::cout << "coutA.arr[" << j << "] : " << contA.arr.get()[j] << std::endl;
      std::cout << "coutB.arr[" << j << "] : " << contB.arr.get()[j] << std::endl;
    }
  }

  return 0;
}


ということで,シャローコピーを実現する自作コンテナの実装方法でした.注意する点としては,,,,

コンテナが生のポインタを持つ場合は,"コピー代入演算子"と"コピーコンストラクタ"の実装が必須!

スマートポインタを使ってやれば,シャローコピーでいいならデフォルトのコピーコンストラクタ・コピー代入演算子が使える.