Чем отличается конструктор копирования от оператора присваивания
Перейти к содержимому

Чем отличается конструктор копирования от оператора присваивания

  • автор:

Чем отличается конструктор копирования от оператора присваивания

Классы: копирование и присваивание

В этой части мы продолжим начатое в статье Элементы класса, о которых всегда необходимо помнить обсуждение конструктора копий (copy constructor) и операции присваивания (assignment operator). Или, вернее, начнем подробное рассмотрение весьма нетривиальной проблемы, каковой на самом деле является копирование и присваивание в классах.
Эти два элемента вполне заслужили отдельного рассмотрения. Создание программ на C++ без понимания внутренней сущности этих функций-членов сродни бегу на марафонскую дистанцию без тренировки (возможно, это не самое удачное сравнение, проще говоря, эти функции очень важны).
Конструктор копий служит для создания новых объектов из существующих. Операция присваивания нужна для того, чтобы сделать один существующий объект эквивалентным другому существующему.
Что означает создать копию? Как один из вариантов, это означает присваивание значений элементов одного объекта элементам другого. Этот ответ, однако, далеко не полон. C++ — это язык, который практически не ограничивает выбор пути реализации программы. И способ создания копий объектов — не исключение из этого правила.
Иногда для копирования классов достаточно просто привести один объект в то же состояние, что и другой. Это весьма просто, и мы увидим, как это делается. Однако если вашему приложению требуются другие методы копирования, C++ не станет создавать их за вас, хотя, если вы не напишете эти функции, компилятор сделает это сам. Правда, результат при этом может существенно отличаться от того, что вам бы хотелось.

  • Понятие копирования
  • Копирование буквальное и развернутое
  • Когда выполняется копирование
  • Разница между копированием и присваиванием
  • Положение в классах
  • Блокирование копирования и присваивания
  • Реализация копирования через присваивание
  • Копирование и присваивание в производных классах

Здесь мы поговорим об одном из аспектов внутреннего функционирования программ, написанных на C++ — о копировании. Копирование в программах на C++ происходит, прямо или косвенно, буквально на каждом шагу. Причем, не всегда с первого взгляда очевидно, где происходит копирование, а где нет.
Мы начнем с рассмотрения синтаксиса интересующего нас предмета, а затем попробуем углубиться в его осмысление.

Определение конструктора копий

Конструктор копий используется для создания новых объектов из уже существующих. Это означает, что, так же как для других конструкторов, новый объект еще не существует к моменту его вызова. Однако только конструктору копий объект передается как аргумент по ссылке. Итак, синтаксис конструктора копий прост. Конструктор копий произвольного класса X выглядит так:
Х(const X&) ; // конструктор копий класса Х
Так как конструктор копий — это все таки конструктор, то он должен иметь имя, совпадающее с именем класса (не забывайте с учетом регистра символов). Назначение конструктора копий — дублирование объекта-аргумента для построения нового объекта. Одно из основных правил: если аргумент не должен изменяться, то его следует передавать как константу. В то же время, если аргумент не описан как константа, то нельзя копировать объекты-константы. Переменный объект всегда можно передать как постоянный аргумент, но не наоборот.
Вторая часть объявления аргумента, X, проста: копируется объект того же самого типа. Аргумент в целом читается как «постоянная ссылка на X». Ссылка существенна по нескольким соображениям. В первую очередь потому, что при передаче адреса объекта не создается копия вызывающего объекта (в отличие от передачи аргумента по значению). Если вам чудится здесь какой-то подвох, то будьте внимательны.
Работа конструктора копий — создание ранее не существовавшего объекта из уже существующего, а передача по значению (без использования операции получения адреса) требует создания копии аргумента, значит мы получаем бесконечную рекурсию. Точнее: при передаче объекта по значению создается его копия, если это произойдет в конструкторе копий, то он будет вызывать сам себя, пока не исчерпает все ресурсы системы.

  • Имя функции точно совпадает с именем класса.
  • Аргумент объявляется постоянным, что позволяет принимать как постоянные, так и переменные аргументы.
  • Тип аргумента является типом класса.
  • Аргумент передается по ссылке, т. е. с помощью операции получения адреса.
  • Среди членов класса нет указателей (*).
  • Среди членов класса нет ссылок (&).

class X
public:
Х(); // конструктор по умолчанию
virtual ~X(); // виртуальный деструктор
// Конструктор копии и операция присваивания не определены
// намеренно. Класс содержит только данные, размещаемые
// в стеке, поэтому предопределенных конструктора копий
//и операции присваивания достаточно.
private:
int data;
char moreData;
float no_Pointers;
>;

Если хотя бы одно из названных условий не выполняется, то следует определить как конструктор копий, так и операцию присваивания.

Определение операции присваивания

По функциональному назначению операция присваивания очень похожа на конструктор копий. Принципиальное отличие состоит в том, что конструктор копий создает новый (возможно, временный) объект, а операция присваивания работает с уже созданными. Вызывающий объект является левым операндом, объект-аргумент — правым.
Операция присваивания также имеет соответствующий синтаксис. Операция присваивания — это функция-член и одновременно двухместная операция. Следовательно, в работу вовлечены два объекта. Первый объект — вызывающий, доступный по указателю this, а второй — это аргумент. Как конструктор копий, так и операция присваивания используют в качестве аргумента постоянную ссылку. Для произвольного класса X мы имеем следующий синтаксис операции присваивания:

X& operator=(const X&); // синтаксис операции присваивания для
// произвольного класса

Присваивание — это операция, значит мы должны использовать ключевое слово operator и соответствующий символ операции. Так как C++ допускает цепочки присваивания
а = b = с = d; // C++ допускает последовательные присваивания,
// так что это свойство надо сохранить
то необходимо возвращать ссылку на объект; в противном случае цепочка прервется.
Итак, оператор-функция принимает постоянную ссылку, а возвращает ссылку на объект. Использование ключевого слова const позволяет функции работать как с постоянными объектами, так и с переменными.

  • Операция присваивания должна быть членом класса.
  • Она принимает постоянную ссылку на объект типа того же класса.
  • Она возвращает ссылку на объект типа того же класса.

В операции присваивания для любого класса надо учитывать один важный момент. Всегда надо проверять: не происходит ли присваивания самому себе. Оно может иметь место в том случае, когда объект прямо или косвенно вызывает операцию присваивания для себя. Прямое присваивание может выглядеть следующим образом:

POINT Pix;
Pix = Pix; // присваивание самому себе

Это самый тривиальный случай, он хорош для приведения примера, не более. В реальных программах такого обычно не бывает и эта ошибка, как правило, принимает далеко не столь очевидные обличия.
Присваивание самому себе порождает в программе утечки памяти. Такая ситуация может возникнуть, когда два объекта сообща используют некоторый ресурс и один из них этот ресурс освобождает. При этом состояние ресурса становится неопределенным, но второй объект продолжает на него ссылаться.
Есть много путей, ведущих к возникновению этой проблемы. Для ее предотвращения и следует предусмотреть в операции присваивания проверку на присваивание самому себе. Она очень проста и выглядит всегда совершенно одинаково:

POINT& POINT::operator=(const POINT& rhs)
if(this == &rhs) return *this; // проверка на присваивание себе
else < X=rhs.X; Y=rhs.Y; >//то, что делает оператор полезного
return *this; // возврат ссылки на объект
>

Сейчас мы попробуем в ней разобраться, благо для всех операций присваивания проверка на присваивание себе совершенно одинакова.
Оператор if(this == &rhs) проверяет, не совпадает ли аргумент с самим объектом. Указатель this содержит адрес вызывающего объекта, &rhs читается как «адрес rhs». Таким образом, сравниваются два адреса. Если они эквивалентны (==), то это один и тот же объект. В этом случае, в полном соответствии с требованием возврата ссылки на объект, просто возвращаем *this (заметьте, что в конце функции делается то же самое) и выходим из функции.
Вспомните, что this — это указатель. Значение указателя — это адрес. Чтобы получить значение указателя, его следует разыменовывать. Разыменование указателя выглядит так: *ptr. Указатель this разыменовывается точно так же: *this.
Помещая эти две строки в начале и в конце тела операции присваивания, мы уменьшаем вероятность возникновения утечек памяти из-за этой операции.
Если вы запомнили приведенный здесь синтаксис копирования и присваивания и, определяя новый класс, сразу будете определять и их тоже, то это уже полдела.

Зачем C++ требует определения этих функций-членов?

Язык C++ не слишком сильно ограничивает свободу программистов в методах разработки программного обеспечения. В частности, он не навязывает вам способы копирования и присваивания. Количество и разнообразие ситуаций, в которых происходит копирование объектов, удивительно велико. Для очень простых объектов, состоящих из одного-двух элементов, затраты на копирование незначительны, но для более сложных, таких как графический интерфейс пользователя или комплексные типы данных, оперирующие с динамической памятью, издержки на копирование существенно возрастают.
Во-первых, имейте в виду, что если вы не определите для нового класса конструктор копий, то C++ создаст его сам. Причина заключается в том, что компилятору самому может потребоваться возможность создания копий, значит, эти две функции обязаны быть определены. Во-вторых, вам может потребоваться заблокировать копирование, либо вести подсчет ссылок, или еще что-нибудь. Если вы не создадите эти функции, то C++ создаст для них версии по умолчанию.
Создаваемые компилятором версии обеих этих функций не всегда будут вас удовлетворять. Версии компилятора выполняют буквальное, или поразрядное, копирование. В некоторых случаях это неразумно. Не пожалейте времени на изучение ситуаций, которые могут вам встретиться при разработке программ.

Вот лишь некоторые из бесчисленного множества возможных ситуаций, в которых происходит копирование:

POINT х;
POINT у(х); // Прямой вызов конструктора копий.
POINT х = у; // Выглядит как присваивание, но на самом деле
// вызывает конструктор копий. Почему? См. ниже.
POINT a, b;
a = b; // Вызов операции присваивания
POINT Foo(); // Возврат по значению, вызывает копирование
void Foo(POINT); // Передача по значению, создает копию

Во всех этих случаях выполняется копирование. В ходе выполняемой компилятором оптимизации могут появиться и другие варианты. Это та область, где знание действительно сила, способная помочь вам избежать утечек памяти.
В операторе типа POINT х = у; не вызывается операция присваивания класса POINT, хотя на первый взгляд выглядит это именно так. Причина состоит в том, что операция присваивания — это функция-член, а значит может быть вызвана только для уже существующих объектов, в то время как в этом фрагменте происходит создание нового объекта х.
Если объект создается в той же строке, в которой он выступает в качестве левостороннего аргумента, то вызывается конструктор. Строка

Х х = у; // вызов конструктора копий
эквивалентна строке
Х х(у); // вызов конструктора копий
|что совсем не то же самое, что
Х х, у;
х = у; // вызов операции присваивания

Вам следует понимать, что же на самом деле вызывается, когда и почему. Это одна из тех особенностей, благодаря которым C++ труднее и интереснее, чем С.
В предыдущем разделе мы пришли к заключению, что не стоит определять операцию присваивания без конструктора копий и наоборот. Следовательно, напрашивается вывод, что основные рекомендации для операции присваивания справедливы также и для конструктора копий.

На этом, пожалуй, пока и остановимся. Небольшое резюме напоследок.
Если класс содержит указатели или ссылки, то скорее всего вам придется определять операцию присваивания и конструктор копий для этого класса самостоятельно, не полагаясь на компилятор.
В противном случае можно спокойно использовать созданные компилятором присваивание и копирование, но при этом полезно упомянуть об этом в комментариях к классу.

При написании статьи использованы материалы из книг
P.Kimmel
Using Borland C++ 5
Special Edition
перевод BHV — С.Петербург 1997

C++. Бархатный путь
Марченко А.Л.
Центр Информационных Технологий
www.citmgu.ru

Thinking in C++, 2nd ed. Volume 1
2000 by Bruce Eckel

Если вам интересно, или возникают вопросы пишите, разберемся.
Сергей Малышев (aka Михалыч).

Копирование конструкторов и операторов назначения копирования (C++)

Начиная с C++11, в языке поддерживаются два типа назначения: копирование назначения и назначение перемещения. В этой статье «присваивание» означает «присваивание копированием», если явно не указано другое. Сведения о назначении перемещения см. в разделе «Конструкторы перемещения» и «Операторы назначения перемещения» (C++).

Как при операции назначения, так и при операции инициализации выполняется копирование объектов.

  • Назначение: когда значение одного объекта назначается другому объекту, первый объект копируется во второй объект. Таким образом, этот код копирует значение b в a :
Point a, b; . a = b; 

Можно определить семантику копии объектов типа класса. Рассмотрим для примера такой код:

TextFile a, b; a.Open( "FILE1.DAT" ); b.Open( "FILE2.DAT" ); b = a; 

Предыдущий код может означать «копирование содержимого FILE1. DAT в FILE2. DAT или может означать «игнорировать FILE2″. DAT и сделайте b второй дескриптор в FILE1.DAT». Необходимо присоединить соответствующую семантику копирования к каждому классу следующим образом:

  • Используйте оператор operator= назначения, который возвращает ссылку на тип класса и принимает один параметр, передаваемый по const ссылке, например ClassName& operator=(const ClassName& x); .
  • Используйте конструктор копирования.

Если вы не объявляете конструктор копирования, компилятор создает конструктор копирования с правильным элементом. Аналогичным образом, если вы не объявляете оператор назначения копирования, компилятор создает для вас оператор назначения мудрее элементов. Объявление конструктора копирования не подавляет оператор назначения копирования, созданный компилятором, и наоборот. При реализации одного из них рекомендуется реализовать другой. При реализации обоих значений кода ясно.

Конструктор копирования принимает аргумент типа ClassName& , где ClassName — имя класса. Например:

// spec1_copying_class_objects.cpp class Window < public: Window( const Window& ); // Declare copy constructor. Window& operator=(const Window& x); // Declare copy assignment. // . >; int main()

По возможности сделайте тип аргумента const ClassName& конструктора копирования. Это предотвращает случайное изменение скопированного объекта конструктором копирования. Он также позволяет копировать из const объектов.

Конструкторы копии, создаваемые компилятором

Конструкторы копирования, созданные компилятором, например определяемые пользователем конструкторы копирования, имеют один аргумент типа «ссылка на имя класса». Исключение заключается в том, что все базовые классы и классы-члены имеют конструкторы копирования const , объявленные как принимающие один аргумент типа class-name&. В таком случае аргумент конструктора копирования, созданного компилятором, также const является .

Если тип аргумента конструктору копирования не const является, инициализация путем копирования const объекта создает ошибку. Обратный аргумент не имеет значения true: если аргумент const имеет значение, можно инициализировать, скопировав объект, который не const является.

Операторы назначения, созданные компилятором, соответствуют тому же шаблону const . Они принимают один аргумент типа ClassName& , если операторы назначения во всех базовых и классах членов не принимают аргументы типа const ClassName& . В этом случае созданный оператор назначения для класса принимает const аргумент.

Когда виртуальные базовые классы инициализированы конструкторами копирования, независимо от того, создается ли компилятор или определяемый пользователем, они инициализированы только один раз: в момент их создания.

Последствия аналогичны конструктору копирования. Если тип аргумента не const является, назначение из const объекта создает ошибку. Обратный аргумент не имеет значения: если const значение присваивается значению, которое не const является, назначение завершается успешно.

Дополнительные сведения о перегруженных операторах назначения см. в разделе «Назначение».

13.15 – Перегрузка оператора присваивания

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

Присваивание и конструктор копирования

Назначение конструктора копирования и оператора присваивания почти эквивалентны – оба копируют один объект в другой. Однако конструктор копирования инициализирует новые объекты, тогда как оператор присваивания заменяет содержимое существующих объектов.

Разница между конструктором копирования и оператором присваивания вызывает много путаницы у начинающих программистов, но на самом деле это не так уж и сложно. Обобщим:

  • Если новый объект должен быть создан до того, как может произойти копирование, используется конструктор копирования (примечание: это включает в себя передачу или возврат объектов по значению).
  • Если новый объект не нужно создавать до того, как может произойти копирование, используется оператор присваивания.

Перегрузка оператора присваивания

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

#include #include class Fraction < private: int m_numerator; int m_denominator; public: // Конструктор по умолчанию Fraction(int numerator=0, int denominator=1) : m_numerator(numerator), m_denominator(denominator) < assert(denominator != 0); >// Конструктор копирования Fraction(const Fraction ©) : m_numerator(copy.m_numerator), m_denominator(copy.m_denominator) < // здесь нет необходимости проверять знаменатель на 0, // поскольку copy уже должна быть корректным объектом Fraction // только, чтобы показать что это работает std::cout // Перегруженное присваивание Fraction& operator= (const Fraction &fraction); friend std::ostream& operator<<(std::ostream& out, const Fraction &f1); >; std::ostream& operator <<(std::ostream& out, const Fraction &f1) < out << f1.m_numerator << "/" << f1.m_denominator; return out; >// Упрощенная реализация operator= (улучшенную реализацию смотрите ниже) Fraction& Fraction::operator= (const Fraction &fraction) < // делаем копию m_numerator = fraction.m_numerator; m_denominator = fraction.m_denominator; // возвращаем существующий объект, чтобы // можно было включить этот оператор в цепочку return *this; >int main() < Fraction fiveThirds(5, 3); Fraction f; f = fiveThirds; // вызывает перегруженное присваивание std::cout

Эта программа печатает:

Теперь всё должно быть довольно просто. Наш перегруженный operator= возвращает *this , чтобы мы могли объединить несколько присваиваний в цепочку:

int main() < Fraction f1(5,3); Fraction f2(7,2); Fraction f3(9,5); f1 = f2 = f3; // цепочка присваиваний return 0; >

Проблемы из-за самоприсваивания

Здесь всё становится немного интереснее. C++ допускает самоприсваивание:

int main() < Fraction f1(5,3); f1 = f1; // самоприсваивание return 0; >

Этот код вызовет f1.operator=(f1) , и в упрощенной реализации, приведенной выше, все члены будут присвоены сами себе. В этом конкретном примере самоприсваивание приводит к тому, что каждый член присваивается самому себе, что ни на что не влияет, кроме потери времени. В большинстве случаев самоприсваиванию вообще ничего не нужно делать!

Однако в случаях, когда оператору присваивания необходимо динамически присваивать память, самоприсваивание может быть опасным:

#include class MyString < private: char* m_data<>; int m_length<>; public: MyString(const char* data = nullptr, int length = 0) : m_length(length) < if (length) < m_data = new char[length]; for (int i = 0; i < length; ++i) m_data[i] = data[i]; >> ~MyString() < delete[] m_data; >// Перегруженное присваивание MyString& operator= (const MyString& str); friend std::ostream& operator<<(std::ostream& out, const MyString& s); >; std::ostream& operator <<(std::ostream& out, const MyString& s) < out << s.m_data; return out; >// Упрощенная реализация operator= (не использовать) MyString& MyString::operator= (const MyString& str) < // если данные существуют в текущей строке, удалить их if (m_data) delete[] m_data; m_length = str.m_length; // копируем данные из str в неявный объект m_data = new char[str.m_length]; for (int i = 0; i < str.m_length; ++i) m_data[i] = str.m_data[i]; // возвращаем существующий объект, чтобы // можно было включить этот оператор в цепочку return *this; >int main() < MyString alex("Alex", 5); // Встречайте, это Алекс MyString employee; employee = alex; // Алекс - наш новый сотрудник std::cout 

Сначала запустите программу как есть. Вы увидите, что программа печатает " Alex ", как и должна.

Теперь запустите следующую программу:

int main() < MyString alex("Alex", 5); // Встречайте, это Алекс alex = alex; // Алекс сам по себе std::cout 

Вероятно, вы получите мусор. Что случилось?

Рассмотрим, что происходит в перегруженном operator= , когда неявный объект и переданный параметр ( str ) являются переменной alex . В этом случае m_data совпадает с str.m_data . Первое, что происходит, это то, что функция проверяет, есть ли уже у неявного объекта строка. Если это так, ее необходимо удалить, чтобы не произошло утечки памяти. В этом случае размещается m_data , поэтому функция удаляет m_data . Но поскольку str совпадает с *this , строка, которую мы хотели скопировать, была удалена, а m_data (и str.m_data ) стали висячими указателями.

Затем мы выделяем новую память для m_data (и str.m_data ). Поэтому, когда мы впоследствии копируем данные из str.m_data в m_data , мы копируем мусор, потому что str.m_data никогда не инициализировалась.

Обнаружение и обработка самоприсваивания

К счастью, мы можем определить, когда происходит самоприсваивание. Вот обновленная реализация нашего перегруженного operator= для класса MyString :

MyString& MyString::operator= (const MyString& str) < // проверка на самоприсваивание if (this == &str) return *this; // если данные существуют в текущей строке, удалить их if (m_data) delete[] m_data; m_length = str.m_length; // копируем данные из str в неявный объект m_data = new char[str.m_length]; for (int i = 0; i < str.m_length; ++i) m_data[i] = str.m_data[i]; // возвращаем существующий объект, // можно было включить этот оператор в цепочку return *this; >

Проверяя, совпадает ли адрес нашего неявного объекта с адресом объекта, переданного в качестве параметра, мы можем заставить наш оператор присваивания немедленно возвращаться без выполнения какой-либо другой работы.

Поскольку это просто сравнение указателей, оно должно быть быстрым и не требует перегрузки operator== .

Когда не обрабатывать самоприсваивание

Обычно проверка на самоприсваивание опускается в конструкторах копирования. Поскольку создаваемый объект для копирования создается заново, единственный случай, когда вновь созданный объект может быть равен копируемому, – это когда вы пытаетесь инициализировать новый определяемый объект самим собой:

someClass c < c >;

В таких случаях ваш компилятор должен предупредить вас, что c – неинициализированная переменная.

Во-вторых, проверка на самоприсваивание может быть опущена в классах, которые могут обрабатывать самоприсваивание естественным образом. Рассмотрим следующий оператор присваивания класса Fraction , который имеет защиту от самоприсваивания:

// Улучшенная реализация operator= Fraction& Fraction::operator= (const Fraction &fraction) < // защита от самоприсваивания if (this == &fraction) return *this; // делаем копию m_numerator = fraction.m_numerator; // может обрабатывать самоприсваивание m_denominator = fraction.m_denominator; // может обрабатывать самоприсваивание // возвращаем существующий объект, чтобы // можно было добавить этот оператор в цепочку return *this; >

Если бы защиты от самоприсваивания не было, эта функция всё равно работала бы правильно во время самоприсваивания (потому что все операции, выполняемые функцией, могут правильно обрабатывать самоприсваивание).

Поскольку самоприсваивание – редкое событие, некоторые видные гуру C++ рекомендуют опускать защиту самоприсваивания даже в тех классах, которым это было бы полезно. Мы не рекомендуем этого делать, поскольку считаем, что лучше использовать защитный код, а затем, при необходимости, выборочно оптимизировать его.

Идиома копирования и обмена

Лучший способ справиться с проблемами самоприсваивания – использовать так называемую идиому копирования и обмена. Статья о том, как эта идиома работает, появится чуть позже.

Оператор присваивания по умолчанию

В отличие от других операторов, компилятор предоставит для вашего класса открытый оператор присваивания по умолчанию, если вы не предоставите его сами. Этот оператор присваивания выполняет поэлементное присваивание (что по сути то же самое, что и поэлементная инициализация, которую выполняют конструкторы копирования по умолчанию).

Как и другие конструкторы и операторы, вы можете предотвратить выполнение присваивания, сделав свой оператор присваивания закрытым или используя ключевое слово delete :

#include #include class Fraction < private: int m_numerator; int m_denominator; public: // Конструктор по умолчанию Fraction(int numerator=0, int denominator=1) : m_numerator(numerator), m_denominator(denominator) < assert(denominator != 0); >// Конструктор копирования Fraction(const Fraction ©) = delete; // Перегруженное присваивание // никаких копий через присваивание! Fraction& operator= (const Fraction &fraction) = delete; friend std::ostream& operator<<(std::ostream& out, const Fraction &f1); >; std::ostream& operator <<(std::ostream& out, const Fraction &f1) < out << f1.m_numerator << "/" << f1.m_denominator; return out; >int main() < Fraction fiveThirds(5, 3); Fraction f; f = fiveThirds; // ошибка компиляции, operator= был удален std::cout

Чем отличается конструктор копирования от оператора присваивания

Оператор присваивания с перемещением (move assignment operator) призван решать те же задачи, что и конструктор перемещения. Подобный оператор имеет следующую форму:

MyClass& operator=(MyClass&& moved) < // код оператора return *this; // возвращаем текущий объект >

В качестве параметра передаем перемещаемый объект в виде rvalue-ссылки. В коде оператора выполняем некоторые действия

Определим и используем конструктор присваивания с перемещением:

#include // класс сообщения class Message < public: // обычный конструктор Message(const char* data, unsigned count) < size = count; text = new char[size]; // выделяем память for(unsigned i<>; i < size; i++) // копируем данные < text[i] = data[i]; >std::cout // обычный оператор присваивания Message& operator=(const Message& copy) < std::cout ; i < size; i++) // копируем данные < text[i] = copy.text[i]; >> return *this; // возвращаем текущий объект > // опрератор присваивания с перемещением Message& operator=(Message&& moved) < std::cout return *this; // возвращаем текущий объект > // деструктор ~Message() < std::cout char* getText() const < return text; >unsigned getSize() const < return size; >unsigned getId() const private: char* text<>; // текст сообщения unsigned size<>; // размер сообщения unsigned id<>; // номер сообщения static inline unsigned counter<>; // статический счетчик для генерации номера объекта >; int main() < char text1[] ; Message hello; char text2[] ; hello = Message; // присваивание объекта std::cout

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

Message& operator=(Message&& moved) < std::cout return *this; // возвращаем текущий объект >

В функции main присваиваем переменной hello объект Message:

char text2[] ; hello = Message;

Стоит отметить, что, как и в случае с конструктором перемещения, присваиваемое значение представляет rvalue - временный объект в памяти ( Message; ), который после выполнения операции (присовения) будет не нужен. И это как раз идеальный случай для применения оператора присваивания с перемещением. Консольный вывод данной программы:

Create message 1 Create message 2 Move assign message 2 to 1 Delete message 2 Message 1: Hi World! Delete message 1

Как видно, переменная hello представляет объект Message с номером 1. Стоит отметить, что если в классе определено несколько операторов присваивания (стандартный и присваивание с перемещением), то по умолчанию для rvalue будет применяться оператор присваивания с перемещением. При присвоении lvalue будет применять стандартный оператор присвоения (без перемещения):

Message hello; Message hi; hello = hi; // присвоение lvalue - обычный оператор присваивания hello = Message; // присвоение rvalue - оператор присваивания с перемещением

Стоит отметить, что мы можем применить функцию std::move() для преобразования lvalue в rvalue:

Message hello; Message hi; hello = std::move(hi); // преобразование lvalue в rvalue - оператор присваивания с перемещением

Здесь переменная hi преобразуется в rvalue, поэтому при присвоении будет срабатывать оператор присвоения с перемещением.

Стоит отметить, что компилятор сам компилирует оператор присваивания с перемещением по умолчанию, который перемещает значения всех нестатических переменных. Однако если мы определяем деструктор или конструктор копирования или конструктор перемещения или оператор присваивания, то компилятор не генерирует стандартный оператор присваивания с перемещением.

std::unique_ptr и перемещение значений

Поскольку smart-указатель std::unique_ptr уникально указывает на определенный адрес памяти, не может быть двух и более указателей std::unique_ptr , которые указывают на один и тот же участок памяти. Именно поэтому у типа unique_ptr нет конструктора копирования и оператора присваивания с копирования. Соотвественно при попытки их применить мы столкнемся с ошибками компиляции:

#include #include int main() < std::unique_ptrone< std::make_unique(123) >; std::unique_ptr other; // other = one; // Ошибка! оператор присваивания с копированием отсутствует // std::unique_ptr another< other >; // Ошибка! конструктор копированием отсутствует >

Однако unique_ptr имеет конструктор перемещения и оператор присвоения с перемещением, которые при необходимости перемещения данных из одного указателя в другой можно использовать

#include #include int main() < std::unique_ptrone< std::make_unique(123) >; std::unique_ptr other; other = std::move(one); // оператор копирования с перемещением // std::cout << *one << std::endl; // Данные из one перемещены в other std::cout << *other << std::endl; // 123 std::unique_ptranother< std::move(other) >; // конструктор перемещения std::cout 

Стоит отметить, что после того, как мы переместим значение из указателя, мы не сможем получить значения по данному указателю.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *