Sari la conținut

Semantică de copiere: Deep vs Shallow

Aflați diferența dintre copiile superficiale și cele profunde, cum să implementați operațiunile corecte de copiere pentru clasele care dețin resurse și expresia de copiere și schimb.

Why Copy Semantics Matter

When you copy an object in C++, what exactly gets copied? For simple types, the answer is straightforward — all members are copied bit by bit. But when an object owns a resource through a pointer, the default (compiler-generated) copy constructor performs a shallow copy: it copies the pointer value but not the data it points to. This means two objects now share the same resource, leading to double-free bugs and data corruption.

Understanding copy semantics is essential because copies happen implicitly in many places: passing by value, returning from functions, inserting into containers, and assignment.

Problemă de copiere superficială

Constructorul de copiere generat de compilator copiază fiecare membru. Pentru pointeri, aceasta copiază adresa, nu datele indicate. Aceasta este o copie superficială și este periculoasă pentru clasele care dețin resurse.

shallow_copy_bug.cpp
#include <iostream>
#include <cstring>

class BrokenString {
    char* data_;
    std::size_t len_;

public:
    BrokenString(const char* s)
        : len_{std::strlen(s)}
        , data_{new char[std::strlen(s) + 1]}
    {
        std::strcpy(data_, s);
    }

    ~BrokenString() {
        delete[] data_;  // frees the buffer
    }

    // No copy constructor defined!
    // Compiler generates shallow copy: copies the pointer

    void print() const {
        std::cout << data_ << '\n';
    }
};

int main() {
    BrokenString a{"Hello"};
    {
        BrokenString b = a;  // shallow copy: b.data_ == a.data_
        b.print();           // "Hello" — works for now
    }  // b destroyed: delete[] b.data_ (same pointer as a.data_!)

    // a.data_ is now a dangling pointer — UNDEFINED BEHAVIOR
    a.print();  // may crash, print garbage, or appear to work
    return 0;
}

Implementing Deep Copy

A deep copy allocates new memory and copies the actual data. This ensures each object owns its own independent copy of the resource.

deep_copy.cpp
#include <iostream>
#include <cstring>
#include <algorithm>

class SafeString {
    char* data_;
    std::size_t len_;

public:
    explicit SafeString(const char* s)
        : len_{std::strlen(s)}
        , data_{new char[std::strlen(s) + 1]}
    {
        std::strcpy(data_, s);
    }

    // Deep copy constructor
    SafeString(const SafeString& other)
        : len_{other.len_}
        , data_{new char[other.len_ + 1]}
    {
        std::strcpy(data_, other.data_);
    }

    // Deep copy assignment with self-assignment check
    SafeString& operator=(const SafeString& other) {
        if (this == &other) return *this;  // self-assignment guard

        // Allocate new resource BEFORE releasing old one
        char* new_data = new char[other.len_ + 1];
        std::strcpy(new_data, other.data_);

        // Release old resource
        delete[] data_;

        // Adopt new resource
        data_ = new_data;
        len_ = other.len_;
        return *this;
    }

    ~SafeString() { delete[] data_; }

    void print() const {
        std::cout << data_ << " (len=" << len_ << ")\n";
    }
};

int main() {
    SafeString a{"Hello"};
    SafeString b = a;     // deep copy: b gets its own buffer
    SafeString c{"World"};
    c = a;                // deep copy assignment
    a.print();  // Hello (len=5)
    b.print();  // Hello (len=5)
    c.print();  // Hello (len=5)
    return 0;
}

The Copy-and-Swap Idiom

The copy-and-swap idiom provides an elegant, exception-safe implementation of the copy assignment operator. It reuses the copy constructor and aswapfunction, providing strong exception safety with minimal code duplication.

copy_and_swap.cpp
#include <iostream>
#include <cstring>
#include <utility>

class Buffer {
    char* data_;
    std::size_t size_;

public:
    explicit Buffer(std::size_t size)
        : size_{size}, data_{new char[size]{}} {}

    Buffer(const Buffer& other)
        : size_{other.size_}, data_{new char[other.size_]}
    {
        std::memcpy(data_, other.data_, size_);
    }

    ~Buffer() { delete[] data_; }

    // Copy-and-swap: take parameter by VALUE (invokes copy ctor)
    Buffer& operator=(Buffer other) {   // copy happens here
        swap(*this, other);             // swap resources
        return *this;
    }   // 'other' destroyed here, freeing our old resource

    // Non-throwing swap — essential for strong exception safety
    friend void swap(Buffer& a, Buffer& b) noexcept {
        using std::swap;
        swap(a.data_, b.data_);
        swap(a.size_, b.size_);
    }

    char& operator[](std::size_t i) { return data_[i]; }
    std::size_t size() const { return size_; }
};

int main() {
    Buffer a{5};
    a[0] = 'H'; a[1] = 'i';

    Buffer b{10};
    b = a;  // copy-and-swap: exception-safe, self-assignment-safe

    std::cout << b[0] << b[1] << " (size " << b.size() << ")\n";
    return 0;
}

Se dezactivează copierea

Unele tipuri nu ar trebui să fie copiabile - de exemplu, tipurile care reprezintă resurse unice, cum ar fi mânerele de fișiere, conexiunile de rețea sau firele de execuție. Utilizați=deletepentru a comunica în mod clar și pentru a aplica acest lucru:

class UniqueConnection {
public:
 UniqueConnection(const UniqueConnection&) = delete;
 UniqueConnection& operator=(const UniqueConnection&) = delete;
 // Move operations can still be enabled
};

Before C++11, this was done by declaring copy operations private without a definition — a confusing hack that =delete înlocuiește elegant.

Capcană

O atribuire naivă de copiere care șterge resursa veche înainte de a o aloca pe cea nouă este ruptă în două moduri: (1) autoatribuire (a = a) șterge datele, apoi încearcă să copieze din ele și (2) dacănewaruncă dupădelete, obiectul este lăsat într-o stare nevalidă. Idiomul copy-and-swap rezolvă ambele probleme: copiarea se face înainte de orice modificări, iar schimbarea estenoexcept.

Bună practică

Cea mai bună modalitate de a evita erorile de copiere este să nu gestionați niciodată resursele brute în mod direct. Utilizațistd::stringîn loc dechar*,std::vectorîn loc de matrice brute șistd::unique_ptrîn loc să dețineți pointeri bruti. Când toți membrii dvs. au o semantică de copiere corectă, operațiunile de copiere generate de compilator sunt corecte automat. Aceasta este Regula lui Zero în practică.

Idei cheie
  • Copia generată de compilator este superficială — copiază valorile pointerului, nu datele orientate către
  • Clasele care dețin resurse au nevoie de un constructor copie profundă și un operator de atribuire
  • Modul de copiere și schimb vă oferă gratuit siguranță excepțională și siguranță pentru auto-atribuire
  • Utilizați=deletepentru a interzice în mod explicit copierea tipurilor de resurse unice
  • Cea mai bună strategie este Rule of Zero: utilizați pachetele RAII, astfel încât să nu aveți niciodată nevoie de o logică de copiere personalizată

Test — Verifică-ți cunoștințele

(15 XP)

1. Ce se întâmplă când copiați superficial un obiect care deține memorie heap printr-un pointer brut?

2. De ce expresia de copiere și schimbare ia parametrul după valoare?

3. Which approach is recommended to avoid needing custom copy operations altogether?