56. Обеспечьте бессбойную функцию обмена
56. Обеспечьте бессбойную функцию обмена
Резюме
Обычно имеет смысл предоставить для класса функцию swap в целях эффективного и бессбойного обмена внутреннего содержимого объекта с внутренним содержимым другого объекта. Такая функция может пригодиться для реализации ряда идиом, от простого перемещения объектов до реализации присваивания, легко обеспечивающего функцию принятия результатов работы со строгими гарантиями безопасности для вызывающего кода (см. также рекомендацию 51).
Обсуждение
Обычно функция swap выглядит примерно следующим образом (здесь U — некоторый пользовательский тип):
class T { // ...
public:
void swap(T& rhs) {
member1_.swap(rhs.member1_);
std::swap(member2_, rhs.member2_);
}
private:
U member1_;
int member2_;
};
Для примитивных типов и стандартных контейнеров можно использовать std::swap. Другие классы могут реализовывать обмен в виде функций-членов с различными именами.
Рассмотрим использование swap для реализации копирующего присваивания посредством копирующего конструктора. Приведенная далее реализация оператора operator= обеспечивает строгую гарантию (см. рекомендацию 71), хотя и ценой создания дополнительного объекта, что может оказаться неприемлемым, если имеется более эффективный способ выполнения безопасного присваивания объектов типа T:
T& T::operator=(const T& other) { // Вариант 1 (традиционный)
T temp(other);
swap(temp);
return *this;
}
T& T::operator=(T temp) { // Вариант 2 (см. рекомендацию 27)
swap(temp); // Обратите внимание на передачу
return *this; // temp по значению
}
Но что если тип U не имеет бессбойной функции обмена, как в случае многих существующих классов, но вам требуется поддержка функции обмена для типа T? Не все потеряно.
• Если копирующий конструктор и оператор копирующего присваивания U не дают сбоев, то с объектами типа U вполне справится std::swap.
• Если копирующий конструктор U может давать сбой, вы можете хранить (интеллектуальный) указатель на U вместо непосредственного члена. Указатели легко обмениваются. Следствием их применения являются дополнительные расходы на одно динамическое выделение памяти и дополнительную косвенность при обращении, но если вы храните все такие члены в едином Pimpl-объекте, то для всех закрытых членов дополнительные расходы вы понесете только один раз (см. рекомендацию 43).
Никогда не пользуйтесь трюком реализации копирующего присваивания посредством копирующего конструирования с использованием непосредственного вызова деструктора и размещающего new, несмотря на то, что такой трюк регулярно "всплывает" в форумах, посвященных С++ (см. также рекомендацию 99). Так что никогда не пишите:
T& T::operator=(const T& rhs) { // Плохо: анти-идиома
if (this != &rhs) {
this->~T(); // плохая методика!
new(this) T(rhs); // (см. [Sutter00] §41)
}
return *this;
}
Если объекты вашего типа можно обменять более эффективным способом, чем грубое присваивание, желательно предоставить функцию обмена, не являющуюся членом, в том же пространстве имен, где находится и ваш тип (см. рекомендацию 57). Кроме того, подумайте о специализации std::swap для ваших собственных нешаблонных типов:
namespace std {
template<> void swap(MyType& lhs, MyType& rhs) {
lhs.swap(rhs); // Для объектов MyType используется
} // MyType::swap
}
Стандарт не позволяет вам сделать это, если MyType сам является шаблонным классом. К счастью, иметь такую специализацию хорошо, но не обязательно; основная методика состоит в обеспечении функции swap, эффективно работающей с данным типом, в виде функции, не являющейся членом класса, в том же пространстве имен, в котором находится и ваш тип.
Исключения
Обмен важен для классов с семантикой значения. Существенно менее важна она для базовых классов, поскольку эти классы в любом случае используются посредством указателей (см. рекомендации 32 и 54).
Ссылки
[C++03] §17.4.3.1(1) • [Stroustrup00] §E.3.3 • [Sutter00] §12-13, §41