Язык программирования C++. Пятое издание - Стенли Липпман
Шрифт:
Интервал:
Закладка:
Если класс определяет собственную функцию swap(), алгоритм использует именно ее. В противном случае используется функция swap(), определенная библиотекой. Как обычно, хоть мы пока и не знаем, как реализуется функция swap(), концептуально несложно заметить, что обмен двух объектов задействует копирование и два присвоения. Например, код обмена двух объектов подобного значению класса HasPtr (см. раздел 13.2.1) мог бы выглядеть так:
HasPtr temp = v1; // сделать временную копию значения v1
v1 = v2; // присвоить значение v2 объекту v1
v2 = temp; // присвоить сохраненное значение v1 объекту v2
Этот код дважды копирует строку, которая первоначально принадлежала объекту v1: один раз, когда конструктор копий класса HasPtr копирует объект v1 в объект temp, и второй раз, когда оператор присвоения присваивает объект temp объекту v2. Он также копирует строку, которая первоначально принадлежала объекту v2, когда объект v2 присваивается объекту v1. Как уже упоминалось, копирование объекта, подобного значению класса HasPtr, резервирует новую строку и копирует строку, на которую указывает объект класса HasPtr.
В принципе ни одно из этих резервирований памяти не обязательно. Вместо того чтобы резервировать новые копии строки, можно было бы обменять указатели. Таким образом, имело бы смысл обменять два объект класса HasPtr так, чтобы выполнить следующее:
string *temp = v1.ps; // создать временную копию указателя в v1.ps
v1.ps = v2.ps; // присвоить указатель v2.ps указателю v1.ps
v2.ps = temp; // присвоить сохраненный указатель v1.ps
// указателю v2.ps
Написание собственной функции swap()Переопределить стандартное поведение функции swap() можно, определив в классе ее собственную версию. Вот типичная реализация функции swap():
class HasPtr {
friend void swap(HasPtr&, HasPtr&);
// другие члены, как в разделе 13.2.1
};
inline
void swap(HasPtr &lhs, HasPtr &rhs) {
using std::swap;
swap(lhs.ps, rhs.ps); // обмен указателями, а не строковыми данными
swap(lhs.i, rhs.i); // обмен целочисленными членами
}
Все начинается с объявления функции swap(), дружественной, чтобы предоставить ей доступ к закрытым переменным-членам класса HasPtr. Поскольку функция swap() предназначена для оптимизации кода, определим ее как встраиваемую (см. раздел 6.5.2). Тело функции swap() вызывает функции swap() каждой из переменных-членов заданного объекта. В данном случае сначала обмениваются указатели, а затем целочисленные члены объектов, связанных с параметрами rhs и lhs.
В отличие от функций-членов управления копированием, функция swap() никогда не бывает обязательной. Однако ее определение может быть важно для оптимизации классов, резервирующих ресурсы.
Функции swap() должны вызвать функции swap(), а не std::swap()В этом коде есть один важный нюанс: хотя в данном случае это не имеет значения, важно, чтобы функция swap() вызвала именно функцию swap(), а не std::swap(). В классе HasPtr переменные-члены имеют встроенные типы. Для встроенных типов нет специализированных версий функции swap(). В данном случае она вызывает библиотечную функцию std::swap().
Но если класс имеет член, тип которого обладает собственной специализированной функцией swap(), то вызов функции std::swap() был бы ошибкой. Предположим, например, что есть другой класс по имени Foo, переменная-член h которого имеет тип HasPtr. Если не написать для класса Foo собственную версию функции swap(), то будет использована ее библиотечная версия. Как уже упоминалось, библиотечная функция swap() осуществляет ненужное копирование строк, управляемых объектами класса HasPtr.
Ненужного копирования можно избежать, написав функцию swap() для класса Foo. Но версию функции swap() для класса Foo можно написать так:
void swap(Foo &lhs, Foo &rhs) {
// Ошибка: эта функция использует библиотечную версию
// функции swap(), а не версию класса HasPtr
std::swap(lhs.h, rhs.h); // обменять другие члены класса Foo
}
Этот код нормально компилируется и выполняется. Однако никакого различия в производительности между этим кодом и просто использующим стандартную версию функции swap() не будет. Проблема в том, что здесь явно запрошен вызов библиотечной версии функции swap(). Однако нужна версия функции не из пространства имен std, а определенная в классе HasPtr.
Правильный способ написания функции swap() приведен ниже.
void swap(Foo &lhs, Foo &rhs) {
using std::swap;
swap(lhs.h, rhs.h); // использует функцию swap() класса HasPtr
// обменять другие члены класса Foo
}
Все вызовы функции swap() обходятся без квалификаторов. Таким образом, каждый вызов должен выглядеть как swap(), а не std::swap(). По причинам, рассматриваемым в разделе 16.3, если есть специфическая для типа версия функции swap(), она будет лучшим соответствием, чем таковая из пространства имен std. В результате, если у типа есть специфическая версия функции swap(), вызов swap() будет распознан как относящийся к специфической версии. Если специфической для типа версии нет, то (с учетом объявления using для функции swap() в области видимости) при вызове swap() будет использована версия из пространства имен std.
У очень осторожных читателей может возникнуть вопрос: почему объявление using функции swap() не скрывает объявление функции swap() класса HasPtr (см. раздел 6.4.1). Причины, по которым работает этот код, объясняются в разделе 18.2.3.
Использование функции swap() в операторах присвоенияКлассы, определяющие функцию swap(), зачастую используют ее в определении собственного оператора присвоения. Эти операторы используют технологию, известную как копия и обмен (copy and swap)). Она подразумевает обмен левого операнда с копией правого:
// обратите внимание: параметр rhs передается по значению. Это значит,
// что конструктор копий класса HasPtr копирует строку в правый
// операнд rhs
HasPtr& HasPtr::operator=(HasPtr rhs) {
// обменивает содержимое левого операнда с локальной переменной rhs
swap(*this, rhs); // теперь rhs указывает на память, которую
// использовал этот объект
return *this; // удаление rhs приводит к удалению указателя в rhs
}
В этой версии оператора присвоения параметр не является ссылкой. Вместо этого правый операнд передается по значению. Таким образом, rhs — это копия правого операнда. Копирование объекта класса HasPtr приводит к резервированию новой копии строки данного объекта.
В теле оператора присвоения вызывается функция swap(), обменивающая переменные-члены rhs с таковыми в *this. Этот вызов помещает указатель, который был в левом операнде, в rhs, и указатель, который был в rhs,— в *this. Таким образом, после вызова функции swap() указатель-член в *this указывает на недавно зарезервированную строку, являющуюся копией правого операнда.
По завершении оператора присвоения параметр rhs удаляется и выполняется деструктор класса HasPtr. Этот деструктор освобождает память, на которую теперь указывает rhs, освобождая таким образом память, на которую указывал левый операнд.
В этой технологии интересен тот момент, что она автоматически отрабатывает присвоение себя себе и изначально устойчива к исключениям. Копирование правого операнда до изменения левого отрабатывает присвоение себя себе аналогично примененному в нашем первоначальном операторе присвоения (см. раздел 13.2.1). Это обеспечивает устойчивость к исключениям таким же образом, как и в оригинальном определении. Единственный код, способный передать исключение, — это оператор new в конструкторе копий. Если исключение произойдет, то это случится прежде, чем изменится левый операнд.
Операторы присвоения, использующие копию и обмен, автоматически устойчивы к исключениям и правильно отрабатывают присвоение себя себе.