CPP04 1.0
読み取り中…
検索中…
一致する文字列を見つけられません
Deep Copy

ディープコピーとは何か?

オブジェクト指向プログラミングでは、オブジェクトに動的に割り当てられたメモリへのポインタ Brain* ( Dog および Cat 内のポインタなど) が含まれている場合、コピーを処理する主な方法が 2 つあります。

1. 浅いコピー:

  • メンバー変数の値のみがコピーされます。
  • メンバー変数がポインターの場合、ポインターのアドレスのみがコピーされ、それが指すデータはコピーされません。
  • 問題:元のオブジェクトとコピーされたオブジェクトの両方が、動的に割り当てられたメモリの同じ部分を指します。

‍* 1 つのオブジェクトのポインタを通じてデータを変更すると、他のオブジェクトにも影響します。

  • 一方のオブジェクトが破棄されると、共有メモリが解放されます。その後、もう一方のオブジェクトが破棄されると、既に解放されているメモリを解放しようとします。その結果、二重解放エラーが発生し、クラッシュや未定義の動作が発生します。これは、メモリリーク(またはメモリ破損)の一般的なシナリオです。

2. ディープコピー:

  • メンバー変数の値がコピーされるだけでなく、メンバー変数が動的に割り当てられたメモリへのポインターである場合は、その指し示すデータの新しい個別のコピーもヒープ上に作成されます。
  • 解決策:元のオブジェクトとコピーされたオブジェクトの両方に、動的に割り当てられたリソースの 独立したコピーがそれぞれ存在します。

‍* 1 つのオブジェクトの動的に割り当てられたメモリ内のデータを変更しても、他のオブジェクトには影響しません。

  • 各オブジェクトは、破棄されるときに独自のメモリを安全に解放できるため、二重解放エラーを防止できます。

EX01でディープコピーが必要な理由

EX01の場合:

  • Dog, Cat それぞれにメンバーprivate Brain* brain;がいます。
  • コンストラクターでは、Brain オブジェクトを割り当てます。

    this->brain = new Brain();
    Represents the brain of an animal, holding a collection of ideas.
    Definition Brain.hpp:37
    * デストラクタでは、このBrainオブジェクトの割り当てを解除します。

    delete this->brain;
    DogおよびCat がデフォルトの (コンパイラ生成の) コピー コンストラクタと代入演算子を使用した場合、またはそれらを浅いコピーで実装した場合:

    // Hypothetical shallow copy constructor
    Dog::Dog(const Dog& other) : Animal(other) {
    this->brain = other.brain; // THIS IS A SHALLOW COPY!
    }
    The Animal class represents a generic animal.
    Definition Animal.hpp:37
    The Dog class represents a canine animal.
    Definition Dog.hpp:33
    Dog()
    Default constructor for Dog. Calls the Animal base class constructor and sets the type to "Dog"....
    Definition Dog.cpp:28
    警告
    これは二重解放問題につながります。originalDogcopiedDog の両方がdelete this->brain;同じメモリを指して解放しようとすると、失敗します。

EX01( Dog および Cat クラス)でのディープコピーの実装方法

Dog および Cat のディープ コピーを実現するには、コピー コンストラクターとコピー代入演算子で Brain* メンバーを明示的に処理する必要があります。

1. デフォルトコンストラクタ( <tt>Dog::Dog()</tt> / <tt>Cat::Cat()</tt> ):

‍新しく作成された Dog, Cat インスタンスに新しい一意のBrainオブジェクトを割り当てます。

this->type = "Dog";
this->brain = new Brain(); // Allocate a BRAND NEW Brain
// ...
}

2. コピーコンストラクタ ( <tt>Dog::Dog(const Dog& other)</tt> / <tt>Cat::Cat(const Cat& other)</tt> ):

‍* 基本クラス ( Animal ) コピー コンストラクターを呼び出します。

  • 重要なのは、オブジェクトに新しい Brain オブジェクト this を割り当てることです。
  • 次に、Brain クラスのコピーコンストラクターを使用して、other.brain の内容をこの新しく割り当てられた Brain にコピーします。
    Dog::Dog(const Dog& other) : Animal(other) {
    // Create a new Brain for this object, and initialize it with the contents of 'other.brain'
    this->brain = new Brain(*(other.brain)); // This calls Brain's copy constructor
    // ...
    }

3. コピー代入演算子 ( <tt>Dog& Dog::operator=(const Dog& other)</tt> / <tt>Cat& Cat::operator=(const Cat& other)</tt> ):

方法1:

Dog& Dog::operator=(const Dog& other) {
if (this != &other) {
Animal::operator=(other); // Assign base part
// Deep copy: assign the CONTENTS of 'other.brain' to 'this->brain'
*(this->brain) = *(other.brain); // This calls Brain's copy assignment operator
}
return *this;
}
Animal & operator=(const Animal &other)
Copy assignment operator for Animal. Assigns the type from the other Animal object....
Definition Animal.cpp:47
Dog & operator=(const Dog &other)
Copy assignment operator for Dog. (Copy And Swap Idiom) Displays a specific copy assignment message.
Definition Dog.cpp:53
  • 自己割り当てチェック(if (this != &other))が含まれます。
  • 基本クラス (Animal) コピー代入演算子を呼び出します。
  • 次に、Brainクラスのコピー代入演算子を使用して、other.brainの内容をBrainがすでに所有しているオブジェクト*(this->brain)にコピーします。
  • この特定のEX01実装では、this->brainは既に存在しているものと想定されています(デフォルトコンストラクタで割り当てられています)。
警告
ダングリングポインタになる危険性がある。
*(this->brain) = *(other.brain); // Deep copy of Brain
もし this->brain == NULL の場合、ここで未定義動作になります。 さらに、もし this->brain が有効なポインタであったとして、この行の前に delete this->brain; を入れた場合:
  1. delete this->brain; (古い Brain を解放)
  2. this->brain = new Brain(*(other.brain)); (新しい Brain を確保) もし、ステップ 2 の new Brain()std::bad_alloc をスローしたら、this->brain は古い Brain のメモリを既に解放した後なので、ダングリングポインタ になります。operator= が完了しないため、オブジェクトは破損した状態になります。 また、delete this->brain; を入れないと、古い Brain のメモリがリークします。

ダングリングポインタ( dangling pointer )とは? -> WikiPedia 参照

‍コンピューター プログラミングにおけるダングリング ポインターとワイルド ポインターは、適切な型の有効なオブジェクトを指さないポインターです。

方法2:Copy and Swap Idiom (コピー・アンド・スワップ・イエィオム) ※推奨

Dog::Dog(const Dog &other) : Animal(other) {
this->brain = new Brain(*(other.brain)); // Deep copy: create a new Brain and copy its contents
}
void Dog::swap(Dog &other) { std::swap(this->brain, other.brain); }
Dog &Dog::operator=(const Dog &other) {
Dog temp(other);
this->swap(temp);
return *this;
}
T swap(T... args)
  1. 安全なコピー ( Dog temp(other); ):

    ‍まず、コピーコンストラクタを使って、other オブジェクトの完全なディープコピーである一時オブジェクト temp を作成します。

    もしこのコピーコンストラクタ(この中での new Brain() など)でメモリ確保が失敗して std::bad_alloc がスローされても、この時点では *this (代入の左辺のオブジェクト) は一切変更されていません。これは元の有効な状態を完全に保っており、強い例外保証を提供します。

    2. 例外安全な交換 ( this->swap(temp); ):

    ‍一時オブジェクト temp が完全に、かつ例外なく構築されたら、*this のリソース(this->brain)と temp のリソース(temp.brain)を交換(スワップ)します。

    ポインタの交換は、std::swap を使えば通常は例外をスローしません。

    この操作は非常に高速です。

    3. 自動的なクリーンアップ:

    swap が終わると、temp は以前 *this が持っていたリソース(古い Brain オブジェクト)の所有権を持つことになります。

    operator= 関数が終了し、temp オブジェクトがスコープを抜ける際に、temp のデストラクタが自動的に呼び出され、以前 *this が持っていた古い Brain のメモリが解放されます。

    覚え書き
    メリット:

    ‍* 強い例外保証: operator= の途中で new が失敗しても、代入の左辺のオブジェクト (*this) は変更されず、元の有効な状態を保ちます。

  • 正しいリソース解放: 古いリソースの解放が、安全に、かつ自動的に行われます。
  • 自己代入の自動処理: dog = dog; のような自己代入も自動的に正しく処理されます。 この「コピー・アンド・スワップ」イディオムは、リソースを所有するクラスのコピー代入演算子を実装する際の、C++98 およびそれ以降でも非常に推奨されるパターンです。

4. デストラクタ( <tt>Dog::~Dog()</tt> / <tt>Cat::~Cat()</tt> ):

‍* この特定のインスタンスBrainが所有するオブジェクトの割り当てを解除する役割を担います。

delete this->brain; // Deallocate *this object's* unique Brain
// ...
}
virtual ~Dog()
Destructor for Dog. Displays a specific destruction message.
Definition Dog.cpp:45

ディープコピーのテスト

‍EX01 では、main関数 (および理想的には Google Test を使用した単体テスト) にディープ コピーの特定のチェックが含まれます。

// In main.cpp or test file
Dog originalDog;
originalDog.getBrain()->setIdea(0, "Original idea"); // Set an idea in original's brain
Dog copiedDog = originalDog; // Copy constructor
// Prove they are deep copies:
// 1. Check if their Brain pointers are different (meaning they point to different Brain objects)
// ASSERT_NE(originalDog.getBrain(), copiedDog.getBrain()); // This is a good unit test assertion
// 2. Modify the copied Brain and observe the original remains unchanged
copiedDog.getBrain()->setIdea(0, "Modified idea");
// Now, originalDog.getBrain()->getIdea(0) should STILL be "Original idea"
// This is the definitive proof of a deep copy.
void setIdea(int index, const std::string &idea)
Sets an idea at a specific index in the brain. Performs bounds checking to ensure the index is valid ...
Definition Brain.cpp:80
Brain * getBrain() const
Gets a pointer to the Dog's Brain object.
Definition Dog.cpp:94

EX01 は、ディープ コピーを保証することで重大なメモリ エラーを防ぎ、 Dog, Cat オブジェクトを安全にコピーして個別に管理できるようにします。これは、堅牢な C++ アプリケーションの基本要件です。

ちなみに、std::string のコピー代入演算について

‍クラス Brain 自体も正統派標準クラス形式に準拠し、std::string ideas[100]; 配列を処理します。 std::string クラス自体は独自の内部ヒープメモリを管理するため(例えば、長い文字列の場合)、 std::string のデフォルトのコピーコンストラクタと代入演算子は、文字列データのディープコピーを実行します。したがって、 Brain のコピーコンストラクタまたは代入演算子がthis->ideas[i] = other.ideas[i]; を反復処理する場合、配列内の各オブジェクト std::string のディープコピーが暗黙的に実行されます。

つまり、「深さ」は下まで広がります。

  • Dog / Cat 新しいオブジェクトを割り当てて Brain * ポインタの Brain ディープコピーを実行します。
  • Brain それ自体が配列要素の std::string ディープコピーを実行します。