C++における深層コピーと浅層コピー:コピーコンストラクタと代入演算子の詳細

コピーコンストラクタと代入演算子の仕組み

C++において、オブジェクトのコピー制御はメモリ管理とリソースの安全性において極めて重要です。特にコピーコンストラクタとコピー代入演算子(operator=)の挙動の違い、および「浅いコピー」と「深いコピー」の違いを理解することは、バグのない堅牢なクラスを設計する上で必須となります。

コピーコンストラクタの呼び出しタイミング

コピーコンストラクタは、既存のオブジェクトを基にして新しいオブジェクトが生成される際に呼び出されます。具体的には以下の状況で発生します。

  1. 関数への引数渡しが値渡し(pass-by-value)である場合。
  2. オブジェクトの宣言と同時に別のオブジェクトで初期化する場合。
  3. 関数の戻り値としてオブジェクトを値渡しで返す場合。

なお、3番目のケースでは、現代のコンパイラは戻り値最適化(RVO)を行うため、コピーコンストラクタが呼び出されないことが一般的です。最適化を無効にする(-fno-elide-constructorsなど)ことで挙動を確認できます。

重要な点として、コピーコンストラクタの引数は必ず参照渡しにする必要があります。値渡しにすると、引数を渡す際に再びコピーコンストラクタを呼び出してしまい、無限再帰(あるいはコンパイルエラー)に陥ります。

#include <iostream>

class Widget {
public:
    Widget(int val) : data_(val) {
        std::cout << "Constructor called, addr: " << this << std::endl;
    }

    // コピーコンストラクタ
    Widget(const Widget& other) : data_(other.data_) {
        std::cout << "Copy Constructor called, src: " << &other << ", dst: " << this << std::endl;
    }

    ~Widget() {
        std::cout << "Destructor called, addr: " << this << std::endl;
    }

private:
    int data_;
};

void processWidget(Widget w) {
    std::cout << "Processing Widget inside function" << std::endl;
}

Widget createWidget() {
    Widget w(200);
    std::cout << "Widget created in function, addr: " << &w << std::endl;
    return w;
}

int main() {
    std::cout << "--- Phase 1: Initialization ---" << std::endl;
    Widget original(100);
    
    std::cout << "\n--- Phase 2: Copy Initialization ---" << std::endl;
    Widget copy = original; // コピーコンストラクタ呼び出し

    std::cout << "\n--- Phase 3: Function Argument (Pass by Value) ---" << std::endl;
    processWidget(original); // コピーコンストラクタ呼び出し

    std::cout << "\n--- Phase 4: Function Return Value ---" << std::endl;
    Widget returned = createWidget(); // RVOにより最適化される可能性あり
    std::cout << "Returned Widget addr: " << &returned << std::endl;

    return 0;
}

コピー代入演算子の挙動

代入演算子(operator=)は、既に生成済みのオブジェクトに対して、別のオブジェクトの値を上書きする際に呼び出されます。新しいオブジェクトは生成されず、左辺のオブジェクトのリソースが再利用されます。通常、メソッドチェーン(a = b = c)を可能にするために、自身の参照(*this)を返します。

#include <iostream>

class Box {
public:
    Box(int v) : value_(v) {
        std::cout << "Box Constructor, addr: " << this << std::endl;
    }

    // コピー代入演算子
    Box& operator=(const Box& other) {
        std::cout << "Assignment Operator, lhs: " << this << ", rhs: " << &other << std::endl;
        if (this != &other) {
            value_ = other.value_;
        }
        return *this;
    }

private:
    int value_;
};

int main() {
    Box b1(10);
    Box b2(20);

    std::cout << "\nAssigning b2 to b1..." << std::endl;
    b1 = b2; // 代入演算子呼び出し (b1は既に存在する)

    std::cout << "\nInitializing b3 with b1..." << std::endl;
    Box b3 = b1; // コピーコンストラクタ呼び出し (b3はこれから生成される)
    
    return 0;
}

コピーコンストラクタと代入演算子の違い

両者の根本的な違いは、「新しいオブジェクトが生まれるかどうか」です。

コード例 呼ばれる機能 説明
Data d1(10);
Data d2;
d2 = d1;
代入演算子 d2は既に存在するため、値の更新のみが行われます。新規オブジェクトは生成されません。
Data d1(10);
Data d2 = d1;
コピーコンストラクタ d2はこの行で初めて生成されます。宣言と初期化が同時に行われるため、コンストラクタが機能します。

浅いコピー(Shallow Copy)と深いコピー(Deep Copy)

クラスがポインタメンバや動的メモリを管理している場合、デフォルトのコピー動作は危険を伴います。

  • 浅いコピー (Shallow Copy): ポインタのアドレス値だけをコピーします。結果として、複数のオブジェクトが同じメモリ領域を指し示すことになり、片方がメモリを解放すると、もう片方がダングリングポインタ(無効なポインタ)を保持することになります。
  • 深いコピー (Deep Copy): ポインタが指している先のデータそのものをコピーし、新しいメモリ領域を確保します。各オブジェクトが独立したリソースを持つため、互いに干渉しません。

安全性を重視するなら深いコピーを実装すべきですが、コピーのコストが無視できない場合や、意図的にリソースを共有させたい場合(スマートポインタの利用など)は設計上の判断が必要です。

浅いコピーの問題点

以下のBufferクラスは、コピーコンストラクタと代入演算子を定義していないため、コンパイラが生成するデフォルトの「浅いコピー」動作を使用します。

#include <iostream>
#include <cstring>

class Buffer {
public:
    Buffer(size_t size) {
        if (size == 0) size = 10;
        data_ = new int[size];
        size_ = size;
        std::cout << "Buffer Alloc: " << data_ << ", size: " << size_ << std::endl;
    }

    // デストラクタ
    ~Buffer() {
        std::cout << "Buffer Free: " << data_ << std::endl;
        delete[] data_;
    }

    // デフォルトのコピーコンストラクタと代入演算子が使用される
    // これらはポインタのアドレスのみをコピーする(浅いコピー)

    void set(int index, int val) { data_[index] = val; }
    int get(int index) const { return data_[index]; }
    int* getPtr() const { return data_; }

private:
    int* data_;
    size_t size_;
};

int main() {
    std::cout << "Creating buf1..." << std::endl;
    Buffer buf1(5);
    buf1.set(0, 111);

    std::cout << "\nCreating buf2 as a copy of buf1..." << std::endl;
    Buffer buf2 = buf1; // 浅いコピー発生
    
    std::cout << "buf1 ptr: " << buf1.getPtr() << std::endl;
    std::cout << "buf2 ptr: " << buf2.getPtr() << " (Same as buf1!)" << std::endl;

    std::cout << "\nAssigning buf3 = buf1..." << std::endl;
    Buffer buf3(3);
    buf3 = buf1; // 浅い代入発生

    // ここで関数が終了すると、buf3, buf2, buf1の順でデストラクタが呼ばれる。
    // しかしbuf2とbuf1は同じアドレスを解放しようとするため、二重解放エラーが発生する。
    return 0;
}

深いコピーの実装

浅いコピーによる問題を解決するには、コピーコンストラクタと代入演算子を明示的に定義し、リソースを複製するロジック(深いコピー)を記述する必要があります。

#include <iostream>
#include <algorithm> // std::copy用

class SafeBuffer {
public:
    SafeBuffer(size_t len = 10) {
        buffer_ = new int[len];
        length_ = len;
        std::cout << "Constructor: Alloc " << buffer_ << std::endl;
    }

    // デストラクタ
    ~SafeBuffer() {
        std::cout << "Destructor: Free " << buffer_ << std::endl;
        delete[] buffer_;
    }

    // コピーコンストラクタ(深いコピー)
    SafeBuffer(const SafeBuffer& src) {
        std::cout << "Copy Constructor: Copying from " << &src << std::endl;
        length_ = src.length_;
        buffer_ = new int[length_];
        // データ内容の複製
        std::copy(src.buffer_, src.buffer_ + length_, buffer_);
        std::cout << "Copy Constructor: Alloc new " << buffer_ << std::endl;
    }

    // コピー代入演算子(深いコピー)
    SafeBuffer& operator=(const SafeBuffer& src) {
        std::cout << "Assignment Operator: Copying from " << &src << std::endl;
        if (this != &src) {
            // 自己代入でなければ、古いリソースを解放して新しいリソースを確保
            delete[] buffer_;
            length_ = src.length_;
            buffer_ = new int[length_];
            std::copy(src.buffer_, src.buffer_ + length_, buffer_);
        }
        return *this;
    }

private:
    int* buffer_;
    size_t length_;
};

int main() {
    std::cout << "--- Step 1: Create Original ---" << std::endl;
    SafeBuffer original(8);

    std::cout << "\n--- Step 2: Copy Construct ---" << std::endl;
    SafeBuffer copied = original; // 深いコピー

    std::cout << "\n--- Step 3: Assign ---" << std::endl;
    SafeBuffer another(2);
    another = original; // 深い代入

    // スコープ終了時に、それぞれのオブジェクトが独立したメモリを解放するためエラーにならない
    return 0;
}

派生クラスにおけるコピー制御

派生クラスのコピーコンストラクタを定義する際は、基底クラス部分をコピーするために基底クラスのコピーコンストラクタを明示的に呼び出す必要があります。これを行わないと、基底クラス部分はデフォルトコンストラクタで初期化されてしまい、コピーが不完全になります。

#include <iostream>
#include <string>

class Entity {
public:
    Entity() : name_("Unknown") {
        std::cout << "Entity Default Constructor" << std::endl;
    }
    
    Entity(const std::string& n) : name_(n) {
        std::cout << "Entity Param Constructor: " << name_ << std::endl;
    }

    Entity(const Entity& other) : name_(other.name_) {
        std::cout << "Entity Copy Constructor: " << name_ << std::endl;
    }

protected:
    std::string name_;
};

class Player : public Entity {
public:
    Player(const std::string& n, int s) : Entity(n), score_(s) {
        std::cout << "Player Param Constructor" << std::endl;
    }

    // 派生クラスのコピーコンストラクタ
    Player(const Player& other) : Entity(other), score_(other.score_) {
        // : Entity(other) によって基底クラスのコピーコンストラクタが呼ばれる
        std::cout << "Player Copy Constructor" << std::endl;
    }

private:
    int score_;
};

int main() {
    Player p1("Hero", 100);
    
    std::cout << "\nCopying p1 to p2..." << std::endl;
    Player p2 = p1;
    
    return 0;
}

タグ: C++ CopyConstructor MemoryManagement deepcopy AssignmentOperator

5月27日 04:43 投稿