Часть 1. Он возвращает ссылку на *this. А почему?

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

Если у вас есть желание понять (по возможности более полно) тонкости применения operator=, то этот материал, несомненно, для вас.

Сразу скажу, если в вашей домашней библиотеке есть книга Скотта Мейерса "Эффективное использование С++" (издательство ДМК, Москва 2000), то всю эту информацию можно найти в ней. Если нет – читайте смело дальше. Все идеи, что тут излагаются, навеяны именно этой книгой, "пережеваны" и поданы может быть в более простом для усвоения стиле.


Он возвращает ссылку на *this. А почему?

Для начала давайте взглянем на встроенные типы данных. Тут все просто, понятно и приятно.
Код:
float a, b, c;
a = b = c = 3.14;

Для всех встроенных типов допускается применение последовательного присваивания. А, собственно, чем типы, определяемые пользователем хуже встраиваемых? Желательно, чтобы в применении они ни чем не отличались от встроенных (в идеальном случае, конечно, что иногда очень трудно). Значит и в нашем случае необходимо, чтобы для типов, которые мы сами придумаем, так же точно можно было применять последовательное присваивание.

Для примера возьмем уже известный вам (по статьям "Классы. Копирование и присваивание") класс POINT, описывающий точку на плоскости.
Код:
class POINT
{
public:
POINT() { X=0; Y=0; } //конструктор по умолчанию
POINT(int a, int b) { X=a; Y=b; } //еще конструктор
… //остальное пока опустим за ненадобностью
POINT& operator=(const POINT&);
int X; //координаты точки
int Y;
};

Теперь произведем последовательное присваивание:
Код:

POINT A(2,3); //точка с известными координатами
POINT b, c, d;
b=c=d=A;

Поскольку оператор присваивания ассоциативен, то цепочку присваивания можно представить следующим образом:
Код:
b=(c=(d=A));

А теперь посмотрим, как это будет выглядеть в эквивалентной функциональной форме:
Код:
b.operator=(c.operator=(d.operator=(A)));

Отсюда прекрасно видно, что аргументом для operator= является значение, которое возвращает предыдущий вызов функции operator=. Значит, тип, который возвращает функция operator=, должен быть таким же, как тип аргумента. Отсюда и нотация записи операции присваивания в общем виде:
Код:
X& operator=(const X&);

В принципе, можно возвращать значение типа void. Это будет работать, но не даст вам возможности проводить последовательное присваивание.

Какой же из двух объектов следует использовать в качестве возвращаемого? Тот, который стоит слева от знака присваивания и указывается при помощи указателя this, или объект, который стоит с правой стороны и содержится в списке аргументов?

Вот эти два варианта записи operator=.
Код:
POINT& POINT::operator=(const POINT& rhs)
{
… //тут остальное
return *this; // возврат ссылки на объект слева
}

POINT& POINT::operator=(const POINT& rhs)
{
… //тут остальное
return rhs; // возврат ссылки на объект справа
}

Сразу скажем, что версия, которая возвращает rhs, просто не будет компилироваться, будет выдаваться следующая ошибка:
Код:
Error: Reference initialized with 'const POINT', needs lvalue of type 'POINT'

Это происходит потому, что аргумент rhs – это ссылка на const POINT, а operator= возвращает ссылку на POINT. А для объекта, объявленного с const нельзя возвращать неконстантную ссылку. Такую ситуацию можно было бы обойти следующим образом:
Код:
POINT& POINT::operator=(POINT& rhs) {…}

То есть просто передать неконстантный аргумент. Для класса POINT этот трюк вполне пройдет. Но зато в других случаях это будет абсолютно недопустимо. Когда будет происходить неявное преобразование типов – начнутся проблемы. Классический пример – это строковый класс типа String. Смотрите:
Код:
String S;
S=“This is S”; //что аналогично такому S.operator=(“This is S”)

Аргумент справа имеет тип char[], а вовсе не String. В таком случае неявного преобразования типов компилятор создаст временный объект типа String (с помощью его конструктора) для передачи в качестве аргумента. Но компилятор всегда создает временные объекты как const, поскольку это предотвращает случайную передачу временного объекта в функцию, которая модифицирует аргументы. Такой вариант просто не должен компилироваться, так как произойдет попытка передать объект с const в функцию operator=, у которой соответствующий аргумент был объявлен без const.

Вот и все варианты! Выбора больше нет. Остается только возвращать ссылку на левосторонний аргумент *this.

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

В следующей части мы подробно рассмотрим вопрос – а зачем производить проверку на присваивание самому себе?

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

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