#12 クラスのコピーの仕方2 ディープコピー

ということで,Effective C++ 12項の内容の続きですが,ここでは自作コンテナを Deep Copy する例を取り上げます.

ディープコピーのイメージ

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

ディープコピーした場合は,コピー元のオブジェクトとコピー先のオブジェクトになんの関係もないので,コピー後に片方を変更しても他方には影響がありません.おそらく,普通にオブジェクトをコピーした時とか,コピー代入演算子を使った場合はこちらの挙動をイメージする場合が多いのではないかなと思います.

intとかdoubleとかの組み込み型の挙動を考えてみても,,,,

int a, b;
a = 10
b = 20;

b = a;
a = 1000;

当然ながら,aの代入後もbは20のままです.この感覚から行くと,AとBというオブジェクトがあったとして,

Object A(hoge1), B(hoge2);
B = A
A = xxx

とやっても,Bは"A = xxx"の変更の影響を受けない.ただ,cv::Matみたいにケースバイケースではあると思うので,使う側も要注意ですね.

下記,ディープコピーを実現するサンプルを作ってみました.

#include <iostream>
#include <memory>

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

  ~DeepCopyContainer()
  {
    delete[] this->arr;
  }

public:
  static const int ARR_LENGTH = 100;

public:
  int* arr;
};

int main(int, char**) {

  std::cout << "chap_2nd_12_5" << std::endl;

  // 生のポインタを持つ Container.単体としての挙動は成立.
  DeepCopyContainer d1, d2;

  return 0;
}

で,ディープコピーを実現するサンプルです.下記のようになにも考えずにポインタを持つコンテナを作ってしまうと,コピー代入演算子を呼んだ時に,メモリリークしてしまいます.

#include <iostream>
#include <memory>

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

  ~DeepCopyContainer()
  {
    delete[] this->arr;
  }

public:
  static const int ARR_LENGTH = 100;

public:
  int* arr;
};

int main(int, char**) {

  std::cout << "chap_2nd_12_5" << std::endl;

  // 生のポインタを持つ Container.
  DeepCopyContainer d1, d2;

  // デフォルトのコピー代入演算子を使ってしまうと,メモリリーク...
  d1 = d2;

  return 0;
}

また,デフォルトのコピーコンストラクタを呼び出すと,メモリの二重解放が起こります....


#include <iostream>
#include <memory>

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

  ~DeepCopyContainer()
  {
    delete[] this->arr;
  }

public:
  static const int ARR_LENGTH = 100;

public:
  int* arr;
};

int main(int, char**) {

  std::cout << "chap_2nd_12_5" << std::endl;

  // 生のポインタを持つ Container.
  DeepCopyContainer d1;

  // デフォルトのコピーコンストラクタを使ってしまうと,最後に二重解放.
  DeepCopyContainer d2(d1);

  return 0;
}

<||

ということで,自作コピーコンストラクタとコピー代入演算子を作成します.

>|cpp|

#include <iostream>
#include <memory>
#include <cstring>

class DeepCopyContainer {
public:
  DeepCopyContainer() 
    : arr(new int[ARR_LENGTH])
  {
    std::cout << "DeepCopyContainer()" << std::endl;
  }

  ~DeepCopyContainer()
  {
    std::cout << "~DeepCopyContainer()" << std::endl;
    delete[] this->arr;
  }

  DeepCopyContainer(const DeepCopyContainer& obj)
    : arr(new int[ARR_LENGTH])
  {
    std::cout << "DeepCopyContainer(const DeepCopyContainer& obj)" << std::endl;
    std::memcpy(this->arr, obj.arr, ARR_LENGTH * sizeof(int));
  }

  DeepCopyContainer& operator=(const DeepCopyContainer& obj) {
    std::cout << "DeepCopyContainer& operator=(const DeepCopyContainer& obj)" << std::endl;    
    // 自己代入チェック.
    if (this != &obj) {
      std::memcpy(this->arr, obj.arr, ARR_LENGTH * sizeof(int));
    }

    // 自分を返す.
    return *this;
  }

public:
  static const int ARR_LENGTH = 100;

public:
  int* arr;
};

int main(int, char**) {

  std::cout << __FILE__ << std::endl;

  // 生のポインタを持つ Container.
  DeepCopyContainer d1;

  for (int i = 0; i < DeepCopyContainer::ARR_LENGTH; i++) {
    d1.arr[i] = 99;
  }

  for (int i = 0; i < DeepCopyContainer::ARR_LENGTH; i++) {
    std::cout << d1.arr[i] << ", ";
  }
  std::cout << std::endl;

  // コピーコンストラクタの挙動チェック.
  DeepCopyContainer d3(d1);
  for (int i = 0; i < DeepCopyContainer::ARR_LENGTH; i++) {
    std::cout << d3.arr[i] << ", ";
  }

  // コピー代入演算子の挙動チェック
  DeepCopyContainer d4;
  d4 = d1;
  for (int i = 0; i < DeepCopyContainer::ARR_LENGTH; i++) {
    std::cout << d4.arr[i] << ", ";
  }

  return 0;
}

もしくは,SmartPointerを使って下記のようにも実装できます.

#include <iostream>
#include <memory>
#include <cstring>

class DeepCopyContainer {
public:
  DeepCopyContainer() 
    : arr(new int[ARR_LENGTH], std::default_delete<int[]>())
  {
    std::cout << "DeepCopyContainer()" << std::endl;
  }

  ~DeepCopyContainer()
  {
    std::cout << "~DeepCopyContainer()" << std::endl;
  }

  DeepCopyContainer(const DeepCopyContainer& obj)
    : arr(new int[ARR_LENGTH], std::default_delete<int[]>())
  {
    std::cout << "DeepCopyContainer(const DeepCopyContainer& obj)" << std::endl;
    std::memcpy(this->arr.get(), obj.arr.get(), ARR_LENGTH * sizeof(int));
  }

  DeepCopyContainer& operator=(const DeepCopyContainer& obj) {
    std::cout << "DeepCopyContainer& operator=(const DeepCopyContainer& obj)" << std::endl;    
    // 自己代入チェック.
    if (this != &obj) {
      std::memcpy(this->arr.get(), obj.arr.get(), ARR_LENGTH * sizeof(int));
    }

    // 自分を返す.
    return *this;
  }

public:
  static const int ARR_LENGTH = 100;

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

int main(int, char**) {

  std::cout << __FILE__ << std::endl;

  // 生のポインタを持つ Container.
  DeepCopyContainer d1;

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

  for (int i = 0; i < DeepCopyContainer::ARR_LENGTH; i++) {
    std::cout << d1.arr.get()[i] << ", ";
  }
  std::cout << std::endl;

  // コピーコンストラクタの挙動チェック.
  DeepCopyContainer d3(d1);
  for (int i = 0; i < DeepCopyContainer::ARR_LENGTH; i++) {
    std::cout << d3.arr.get()[i] << ", ";
  }

  // コピー代入演算子の挙動チェック
  DeepCopyContainer d4;
  d4 = d1;
  for (int i = 0; i < DeepCopyContainer::ARR_LENGTH; i++) {
    std::cout << d4.arr.get()[i] << ", ";
  }

  return 0;
}

ということで,まとめとして...

1.生のポインタを持つクラスを作るときは,コピーの戦略を考える.

2.生のポインタでなく,出来る限りスマートポインタを使う.

3.オブジェクトを内包するクラスを作るときは,内包するオブジェクトのコピー戦略を調べて,自分の意図とあっているかチェックする.