Как реализуется оператор присваивания копированием по умолчанию в C++?

Ответ

Компилятор C++ автоматически генерирует оператор присваивания копированием (operator=), если он не объявлен явно и в классе нет полей, запрещающих копирование (например, ссылок). Этот сгенерированный оператор выполняет почленное копирование (member-wise copy) для всех нестатических полей и базовых подобъектов.

Пример сгенерированного поведения:

class Example {
public:
    int value;
    std::string name;
    int* dynamic_data;
};

Example obj1;
obj1.value = 42;
obj1.name = "Test";
obj1.dynamic_data = new int(100);

Example obj2;
obj2 = obj1; // Вызов сгенерированного operator=
             // obj2.value = obj1.value (копирование int)
             // obj2.name = obj1.name (копирование std::string работает корректно)
             // obj2.dynamic_data = obj1.dynamic_data (ПРОБЛЕМА: копируется указатель, а не данные!)

Ключевые проблемы сгенерированного оператора:

  1. Поверхностное копирование указателей (Shallow Copy): Оба объекта начинают владеть одним и тем же ресурсом в динамической памяти.
  2. Утечки памяти: Старые данные obj2.dynamic_data не освобождаются.
  3. Двойное освобождение: Деструкторы обоих объектов попытаются освободить одну и ту же память, что ведет к неопределенному поведению.

Решение — реализация оператора присваивания копированием (часть "Правила трех/пяти"):

class Example {
    int* dynamic_data;
    size_t size;
public:
    // Пользовательский оператор присваивания копированием
    Example& operator=(const Example& other) {
        if (this != &other) { // Защита от самоприсваивания (a = a)
            // 1. Освобождаем старые ресурсы
            delete[] dynamic_data;
            // 2. Выделяем новую память и копируем данные
            size = other.size;
            dynamic_data = new int[size];
            std::copy(other.dynamic_data, other.dynamic_data + size, dynamic_data);
        }
        return *this; // Возвращаем ссылку на текущий объект
    }
    // ... также нужны конструктор копирования и деструктор
};

Идиома "Copy-and-Swap" — более надежная и безопасная альтернатива, которая также обеспечивает строгую гарантию исключений.

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

Ответ 18+ 🔞

Давай разберём эту тему, а то там подводных камней — овердохуища. Представь, что компилятор C++ — это такой заботливый, но слегка туповатый друг. Он видит, что ты не написал свой оператор присваивания (operator=), и думает: «Ёпта, чувак поленился, надо помочь!» И он автоматически генерирует его за тебя.

Этот сгенерированный оператор работает по принципу «не выёбывайся» — он просто тупо копирует каждое поле из одного объекта в другой. Для простых штук вроде int или double — норм. Для умных объектов вроде std::string — тоже ок, у них свои мозги внутри. Но вот когда дело доходит до указателей — тут начинается пиздец, Карл.

Смотри, как это выглядит на практике:

class Example {
public:
    int value;
    std::string name;
    int* dynamic_data; // Вот эта хуйня — корень всех бед
};

Example obj1;
obj1.value = 42;
obj1.name = "Test";
obj1.dynamic_data = new int(100); // Выделили память в куче

Example obj2;
obj2 = obj1; // Вызов того самого, сгенерированного компилятором operator=

И что же произошло, спросишь ты? А произошло вот что:

  • obj2.value = obj1.value — скопировалось число, всё чётко.
  • obj2.name = obj1.namestd::string сам скопировал свои внутренние данные, тоже ок.
  • obj2.dynamic_data = obj1.dynamic_dataА вот тут, блядь, полный пиздец. Скопировался сам указатель, а не данные, на которые он указывает! Теперь оба объекта (obj1 и obj2) тычут своими dynamic_data в один и тот же кусок памяти в динамической памяти.

К чему это приводит? Да к полной жопе:

  1. Утечка памяти: У obj2 мог быть свой выделенный кусок памяти. После присваивания указатель на него потерян нахуй, и освободить его уже не получится.
  2. Двойное освобождение: Когда obj1 и obj2 выйдут из области видимости, их деструкторы (даже сгенерированные) вызовут delete для одного и того же адреса. Это гарантированный крах программы, неопределённое поведение, одним словом — караул, ебуть!

Что делать? Правильно, написать свой оператор присваивания. Это часть знаменитого «Правила трёх» (а теперь и «Правила пяти»). Выглядеть он будет как-то так:

class Example {
    int* dynamic_data;
    size_t size;
public:
    // Пользовательский оператор присваивания копированием
    Example& operator=(const Example& other) {
        if (this != &other) { // Святое правило: проверка на самоприсваивание (a = a). Без неё — самоуничтожение.
            // 1. Выкидываем нахуй старые данные
            delete[] dynamic_data;
            // 2. Копируем размер и выделяем новую память
            size = other.size;
            dynamic_data = new int[size];
            // 3. Копируем сами данные, а не указатели
            std::copy(other.dynamic_data, other.dynamic_data + size, dynamic_data);
        }
        return *this; // Возвращаем себя, чтобы можно было делать цепочки (a = b = c)
    }
    // ... и не забудь про конструктор копирования и деструктор, а то будет половинчатая хуйня!
};

Но есть способ ещё круче и надёжнее — идиома Copy-and-Swap (Копируй-и-Меняй). Она как хитрая жопа — убивает сразу всех зайцев: и копирование, и защиту от исключений, и самоприсваивание.

Example& operator=(Example other) { // Обрати внимание! Параметр передаётся ПО ЗНАЧЕНИЮ. То есть копия создаётся уже здесь.
    swap(*this, other); // Меняемся содержимым с этой копией. Всё, что было у нас — теперь в other.
    return *this;
    // А тут выходит из scope параметр `other` и вызывает деструктор, который по-хорошему прибирает за собой (освобождает наши старые данные).
}

Красота, правда? Всё гениальное просто. Компилятор — он, конечно, друг, но иногда его помощь — это как медведь в посудной лавке: вроде хотел как лучше, а получилось нихуя. Так что за ресурсы отвечай сам, чувак.