Когда вызывается оператор присваивания копированием (copy assignment operator) в C++?

Ответ

Оператор присваивания копированием (operator=) вызывается, когда существующему объекту присваивается значение другого существующего объекта того же типа. Это не конструктор, а оператор, который модифицирует состояние уже созданного объекта.

Типичные сценарии вызова:

MyClass a, b, c;

a = b;               // (1) Прямое присваивание одного объекта другому
c = a = b;           // (2) Цепочка присваиваний (возвращает ссылку на левый операнд)

std::vector<MyClass> vec(5);
vec[0] = vec[1];     // (3) Присваивание элементам контейнеров

Ключевые требования к реализации:

  1. Проверка на самоприсваивание: if (this == &other) return *this;
  2. Освобождение старых ресурсов: Перед копированием новых данных нужно корректно освободить ресурсы, которыми владел текущий объект.
  3. Копирование данных: Скопировать (или клонировать) данные из other в текущий объект.
  4. Возврат ссылки на текущий объект: Для поддержки цепочек присваивания.

Пример реализации для класса, управляющего сырой памятью:

class Buffer {
    char* data;
    size_t size;
public:
    // Конструктор по умолчанию
    Buffer(size_t sz = 0) : size(sz), data(size ? new char[size] : nullptr) {}

    // Деструктор
    ~Buffer() { delete[] data; }

    // Конструктор копирования (для Rule of Three/Five)
    Buffer(const Buffer& other) : size(other.size), data(size ? new char[size] : nullptr) {
        std::copy(other.data, other.data + size, data);
    }

    // Оператор присваивания копированием (Copy Assignment Operator)
    Buffer& operator=(const Buffer& other) {
        if (this != &other) { // (1) КРИТИЧНО: проверка на самоприсваивание
            // (2) Освобождаем старые ресурсы
            delete[] data;
            // (3) Копируем новые данные
            size = other.size;
            data = size ? new char[size] : nullptr;
            std::copy(other.data, other.data + size, data);
        }
        // (4) Возвращаем *this
        return *this;
    }
};

Современный подход (Copy-and-Swap idiom): Часто оператор присваивания реализуют через передачу параметра по значению и обмен (swap), что автоматически обеспечивает строгую гарантию исключений и защиту от самоприсваивания.

Buffer& operator=(Buffer other) { // Параметр передаётся по значению (копируется)
    swap(*this, other); // Обмениваем содержимое текущего объекта и временной копии
    return *this;       // Временная копия (со старыми данными) уничтожается при выходе
}

Ответ 18+ 🔞

Давай разберём эту тему, а то она у многих мозги выносит, как будто на дворе 2002-й год и все только про new и delete узнали.

Вот смотри, оператор присваивания копированием — это не какой-то там конструктор, который новый объект из воздуха делает. Это когда у тебя уже есть два готовых чувака, и ты одному говоришь: «Слушай, братан, будь как этот другой». Он уже живой, дышит, а ты ему внутрь новую душу засовываешь. Представь, ты уже построил дом (объект создан), а потом пришёл и сказал: «А теперь, сука, будь точной копией вот того соседского дома». И начинается: старые обои долой, новую мебель заносим. Ёпта, целая операция.

Когда эта хрень вызывается? Да элементарно:

MyClass a, b, c;

a = b;               // (1) Самый классический случай. a уже есть, b уже есть. a теперь должен стать b.
c = a = b;           // (2) Цепочка, как в домино. Сначала a становится b, потом c становится a (который уже стал b). Возвращает ссылку, чтобы можно было так делать.
std::vector<MyClass> vec(5);
vec[0] = vec[1];     // (3) В контейнерах тоже самое. Берём один элемент, переделываем его в другой.

А теперь, блядь, самое главное — как это правильно написать, чтобы не накрыться медным тазом. Тут четыре шага, и пропустишь один — будет тебе хиросима и нигерсраки в коде.

  1. Проверка на самоприсваивание. Это святое, ёпта! if (this == &other) return *this; Зачем? А представь, ты пишешь a = a. Без проверки ты сначала удалишь свои же данные, а потом попробуешь их же скопировать из... себя, которых уже нет. Получишь чистый, неразбавленный пиздец. Удивление пиздец будет, когда программа рухнет. Сам от себя охуел бы.
  2. Освобождение старых ресурсов. Ты же не в коммуналке живёшь, места на всех не хватит. Перед тем как скопировать новые данные из other, нужно аккуратно прибрать за собой: удалить старые массивы, закрыть файлы, отключиться от серверов. Иначе будет утечка памяти — манда с ушами.
  3. Копирование новых данных. Вот теперь, когда место расчищено, можно тащить данные из other. Просто скопировать значения примитивов и сделать глубокое копирование всего, на что есть указатели.
  4. Возврат ссылки на себя. return *this; Это чтобы можно было строить те самые цепочки присваиваний. Без этого нихуя не получится.

Вот тебе пример, как это выглядит в дикой природе, на классе, который памятью рулит:

class Buffer {
    char* data;
    size_t size;
public:
    // Конструктор по умолчанию
    Buffer(size_t sz = 0) : size(sz), data(size ? new char[size] : nullptr) {}

    // Деструктор
    ~Buffer() { delete[] data; }

    // Конструктор копирования (для Rule of Three/Five)
    Buffer(const Buffer& other) : size(other.size), data(size ? new char[size] : nullptr) {
        std::copy(other.data, other.data + size, data);
    }

    // Оператор присваивания копированием (Copy Assignment Operator)
    Buffer& operator=(const Buffer& other) {
        if (this != &other) { // (1) КРИТИЧНО: проверка на самоприсваивание
            // (2) Освобождаем старые ресурсы
            delete[] data;
            // (3) Копируем новые данные
            size = other.size;
            data = size ? new char[size] : nullptr;
            std::copy(other.data, other.data + size, data);
        }
        // (4) Возвращаем *this
        return *this;
    }
};

Но есть, блядь, и более хитрая жопа — современный подход (Copy-and-Swap). Выглядит как магия, но работает на ура.

Buffer& operator=(Buffer other) { // Смотри-ка! Параметр передаётся ПО ЗНАЧЕНИЮ. Он уже копия.
    swap(*this, other); // Просто меняемся с этой временной копией всем содержимым. Быстро и безболезненно.
    return *this;       // А временная копия (со старым барахлом объекта) спокойно умирает при выходе из функции.
}

В чём фишка? Проверка на самоприсваивание не нужна — если это оно и есть, то мы просто сделаем лишнюю копию и поменяемся с ней, что безопасно. Исключительная безопасность на высоте. И код короче. Красота, ебать копать. Выбирай, какой подход тебе ближе, но суть ты теперь, надеюсь, въехал.