Часть 2. Проверка на присваивание самому себе. А зачем?

Итак, продолжим подробное рассмотрение функции operator=. В этот раз разберемся, зачем же нужна проверка на присваивание самому себе.

Когда возникает ситуация с присваиванием самому себе? Ситуация "по определению проблемы" выглядит так:
Код:
class Something { … //содержимое класса … };
Something S;
S=S;

Трудно придумать, зачем такая конструкция может понадобиться. Тем не менее, она вполне допустима правилами С++. Более приемлемой выглядит такая запись:
Код:
Something &W = S;
S=W;

Несколько менее очевидно, но то же самое. Здесь W – это просто другое обозначение для S. Это ссылка, инициализированная S. То есть объект S просто имеет теперь два разных "имени". Такое совмещение имен может возникнуть в самых разных случаях, поэтому такую возможность просто необходимо учитывать.

Теперь давайте посмотрим, к чему приводит отсутствие проверки на присваивание самому себе.

В статье "Классы. Копирование и присваивание. Часть 4" уже был рассмотрен вопрос, как реализовать копирование через присваивание для объектов производных классов. Там говорилось о том, что оператор присваивания производного класса должен вызывать оператор присваивания своего базового класса. А если базовый класс для него и сам является производным от другого? И такая иерархия очень длинная? В таком случае, если в самом начале оператора присваивания обнаружится ситуация с присваиванием самому себе, можно (и нужно) просто сразу выйти из него, избегая целого ряда ненужных функциональных вызовов. При этом как минимум может повыситься эффективность.

Но не это главное. Вспомните – оператор присваивания перед тем как выделить новый блок памяти, должен для начала освободить ресурсы, выделенные объекту ранее (то есть избавиться от старых значений). При присваивании самому себе освобождение памяти может просто "угробить" старые ресурсы, которые могут понадобиться для создания новых.

Смотрим на примере. Возьмем класс целочисленного массива, который мы рассматривали все в той же статье о классах.
Код:
class INT_ARRAY
{
public:
INT_ARRAY(unsigned int sz = 100);
~INT_ARRAY();
INT_ARRAY& operator=(const INT_ARRAY &);// Объявление операции присваивания
int& operator[](unsigned int index);
private:
unsigned int max;
unsigned int dummy;
int *data;
};

И напишем оператор присваивания, который не производит проверку на присваивание объекта самому себе.
Код:
INT_ARRAY& INT_ARRAY::operator=(const INT_ARRAY &rhs)
{
delete [] data; //очистим старую память
data = new int[dummy]; //выделим новую память
max = rhs.max;
dummy = rhs.dummy;
for(int j=0; j<dummy; j++)
data[j] = rhs.data[j]; //копируем в новый блок памяти значения из rhs
return *this;
}

Теперь объявим массив А на 5 значений и заполним его.
Код:
INT_ARRAY A(5);
for(int k=0; k<5; k++) A[k]=k; // здесь работает operator[]
A=A; //или, что тоже самое, A.operator=(A)

Посмотрим, что же получится.

Внутри оператора присваивания *this и rhs вроде бы кажутся разными объектами. Но в данном случае они являются просто разными идентификаторами одного и того же объекта. Первое, что делает наш оператор присваивания – вызывает delete[] для указателя data. В результате полностью пропадают данные из массива. И далее присваивать нам вроде как уже и нечего. Это произошло потому, что указатели data, this->data и rhs.data на самом деле один и тот же указатель! И мы просто удалили rhs.data.
Вот вам и ответ на вопрос "А ЗАЧЕМ?".

Все бы было хорошо, но возникает не самый простой, если подумать, вопрос – а какие же объекты считать одинаковыми. Где критерий "одинаковости"?

Скотт Мейерс отмечает 2 основных подхода к данному вопросу.
Давайте посмотрим первый вариант. Одинаковыми считаются два объекта, если они имеют одинаковые значения. Для нашего примера это означает, что элементы массива одного и другого объектов должны иметь абсолютно одинаковые значения и порядок расположения в массиве. То есть нам необходимо внутри оператора присваивания провести поэлементное сравнение массивов (любым из известных методов) и сделать вывод об идентичности объектов.

Вообще говоря, для подобного сравнения объектов удобнее всего использовать operator== (не путайте оператор сравнения с оператором присваивания!). Поэтому, если правильно реализовать operator==, запись оператора присваивания упростится до такого вида:
Код:
INT_ARRAY& INT_ARRAY::operator=(const INT_ARRAY &rhs)
{
if(*this == rhs) return *this; //работает operator==
… //все остальное
}

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

Теперь второй вариант. Сравнение объектов по их адресам в памяти. Если два объекта занимают в памяти одно и то же место – они считаются одинаковыми. Это определение более распространено, чем сравнение по значению, поскольку более легко реализуется. При использовании эквивалентности адресов оператор присваивания будет таким:
Код:
INT_ARRAY& INT_ARRAY::operator=(const INT_ARRAY &rhs)
{
if(this == &rhs) return *this;
… //все остальное
}

И в большинстве случаев этот вариант вполне приемлем.

Вот и все размышления на тему "зачем" нужна проверка на присваивание объекта самому себе.

Если у вас есть вопросы – пишите, будем разбираться.

Сергей Малышев (aka Михалыч).
Information
  • Posted on 31.01.2010 20:48
  • Просмотры: 530