Обзор
Зачем нам нужна идиома копирования и обмена?
Любой класс, который управляет ресурсом (оболочка, например интеллектуальный указатель), должен реализовать Большая тройка. В то время как цели и реализация копирующего конструктора и деструктора просты, оператор копирования-присваивания, возможно, является наиболее тонким и сложным. Как это сделать? Каких подводных камней нужно избегать?
Идиома копирования и обмена - это решение, которое элегантно помогает оператору присваивания достичь двух вещей: избежать дублирование кода и предоставление надежной гарантии исключения.
Как это работает?
Концептуально он работает с использованием конструктора копирования функциональность для создания локальной копии данных, затем берет скопированные данные с помощью функции swap
, заменяя старые данные новыми данными. Затем временная копия разрушается, забирая с собой старые данные. У нас осталась копия новых данных.
Чтобы использовать идиому копирования и обмена, нам нужны три вещи: рабочий конструктор копии, рабочий деструктор (оба являются основой любой оболочки, поэтому в любом случае должны быть завершены) и функция swap
.
Функция подкачки - это не вызывающая функция, которая меняет местами два объекта класса, член на член. У нас может возникнуть соблазн использовать std::swap
вместо того, чтобы предоставлять свои собственные, но это было бы невозможно; std::swap
использует в своей реализации конструктор копирования и оператор присваивания копии, и в конечном итоге мы попытаемся определить оператор присваивания в терминах самого себя!
(Не только это, но и неквалифицированные вызовы swap
будут использовать наш пользовательский оператор подкачки, пропуская ненужное построение и разрушение нашего класса, которое повлечет за собой std::swap
.)
Подробное объяснение
Цель
Рассмотрим конкретный случай. Мы хотим управлять динамическим массивом в бесполезном в противном случае классе. Начнем с рабочего конструктора, конструктора копирования и деструктора:
#include <algorithm> // std::copy
#include <cstddef> // std::size_t
class dumb_array
{
public:
// (default) constructor
dumb_array(std::size_t size = 0)
: mSize(size),
mArray(mSize ? new int[mSize]() : nullptr)
{
}
// copy-constructor
dumb_array(const dumb_array& other)
: mSize(other.mSize),
mArray(mSize ? new int[mSize] : nullptr)
{
// note that this is non-throwing, because of the data
// types being used; more attention to detail with regards
// to exceptions must be given in a more general case, however
std::copy(other.mArray, other.mArray + mSize, mArray);
}
// destructor
~dumb_array()
{
delete [] mArray;
}
private:
std::size_t mSize;
int* mArray;
};
Этот класс почти успешно управляет массивом, но для правильной работы ему требуется operator=
.
Неудачное решение
Вот как может выглядеть наивная реализация:
// the hard part
dumb_array& operator=(const dumb_array& other)
{
if (this != &other) // (1)
{
// get rid of the old data...
delete [] mArray; // (2)
mArray = nullptr; // (2) *(see footnote for rationale)
// ...and put in the new
mSize = other.mSize; // (3)
mArray = mSize ? new int[mSize] : nullptr; // (3)
std::copy(other.mArray, other.mArray + mSize, mArray); // (3)
}
return *this;
}
И мы говорим, что закончили; это теперь управляет массивом без утечек. Однако он страдает от трех проблем, последовательно отмеченных в коде как (n)
.
Первый - это тест самоназначения.
Эта проверка служит двум целям: это простой способ предотвратить запуск ненужного кода при самоназначении и защищает нас от мелких ошибок (таких как удаление массива только для попробуйте скопировать). Но во всех остальных случаях это просто служит для замедления программы и действует как шум в коде; Самостоятельное присвоение происходит редко, поэтому в большинстве случаев эта проверка бесполезна.
Было бы лучше, если бы оператор мог нормально работать без этого.
Во-вторых, он обеспечивает только базовую гарантию исключения. Если new int[mSize]
не удается, *this
будет изменен. (А именно, размер неправильный и данные пропали!)
Для надежной гарантии исключения это должно быть что-то вроде:
dumb_array& operator=(const dumb_array& other)
{
if (this != &other) // (1)
{
// get the new data ready before we replace the old
std::size_t newSize = other.mSize;
int* newArray = newSize ? new int[newSize]() : nullptr; // (3)
std::copy(other.mArray, other.mArray + newSize, newArray); // (3)
// replace the old data (all are non-throwing)
delete [] mArray;
mSize = newSize;
mArray = newArray;
}
return *this;
}
Код расширился! Это приводит нас к третьей проблеме: дублированию кода.
Наш оператор присваивания эффективно дублирует весь код, который мы уже написали где-то еще, и это ужасно.
В нашем случае его ядро составляет всего две строки (выделение и копия), но с более сложными ресурсами это раздувание кода может быть довольно неприятным. Мы должны стремиться никогда не повторяться.
(Кто-то может задаться вопросом: если для правильного управления одним ресурсом требуется столько кода, что, если мой класс управляет более чем одним?
Хотя это может показаться серьезной проблемой, и на самом деле это требует нетривиального _14 _ / _ 15_ пунктов, это не проблема.
Это потому, что класс должен управлять только одним ресурсом !)
Удачное решение
Как уже упоминалось, идиома копирования и замены решит все эти проблемы. Но прямо сейчас у нас есть все требования, кроме одного: swap
функция. Хотя Правило трех успешно влечет за собой существование нашего конструктора копирования, оператора присваивания и деструктора, его действительно следует называть Большой тройкой с половиной: каждый раз, когда ваш класс управляет ресурсом, также имеет смысл предоставить функцию swap
.
Нам нужно добавить в наш класс функцию подкачки, и мы делаем это следующим образом †:
class dumb_array
{
public:
// ...
friend void swap(dumb_array& first, dumb_array& second) // nothrow
{
// enable ADL (not necessary in our case, but good practice)
using std::swap;
// by swapping the members of two objects,
// the two objects are effectively swapped
swap(first.mSize, second.mSize);
swap(first.mArray, second.mArray);
}
// ...
};
(Вот объяснение, почему public friend swap
.) Теперь мы можем не только поменять местами наши dumb_array
, но свопы в целом могут быть более эффективными; он просто меняет местами указатели и размеры, а не выделяет и копирует целые массивы. Помимо этого бонуса в функциональности и эффективности, теперь мы готовы реализовать идиому копирования и обмена.
Без лишних слов наш оператор присваивания:
dumb_array& operator=(dumb_array other) // (1)
{
swap(*this, other); // (2)
return *this;
}
Вот и все! Одним махом можно элегантно решить сразу все три проблемы.
Почему это работает?
Сначала мы замечаем важный выбор: аргумент параметра берется по значению. Хотя с таким же успехом можно было бы сделать следующее (как и многие наивные реализации этой идиомы):
dumb_array& operator=(const dumb_array& other)
{
dumb_array temp(other);
swap(*this, temp);
return *this;
}
Мы теряем важная возможность оптимизации. Не только это, но и этот выбор критически важен для C ++ 11, который обсуждается позже. (В общем, замечательно полезное руководство выглядит следующим образом: если вы собираетесь сделать копию чего-либо в функции, позвольте компилятору сделать это в списке параметров. ‡)
В любом случае, этот метод получения нашего ресурса является ключом к устранению дублирования кода: мы можем использовать код из конструктора копирования для создания копии, и нам никогда не нужно повторять ни один его бит. Теперь, когда копия сделана, мы готовы к обмену.
Обратите внимание, что при входе в функцию все новые данные уже размещены, скопированы и готовы к использованию. Это то, что дает нам надежную бесплатную гарантию исключения: мы даже не войдем в функцию, если построение копии завершится неудачно, и, следовательно, невозможно изменить состояние *this
. (То, что мы раньше делали вручную для надежной гарантии исключения, теперь делает за нас компилятор; как любезно.)
На данный момент мы находимся без дома, потому что swap
не бросает. Мы меняем наши текущие данные на скопированные, безопасно изменяя свое состояние, а старые данные помещаются во временные. После возврата из функции старые данные удаляются. (Где заканчивается область действия параметра и вызывается его деструктор.)
Поскольку идиома не повторяет код, мы не можем вносить ошибки в оператор. Обратите внимание, что это означает, что мы избавляемся от необходимости проверки самостоятельного назначения, что позволяет использовать единую унифицированную реализацию operator=
. (Кроме того, у нас больше нет штрафа за выполнение заданий, не выполняемых самостоятельно.)
И это идиома копирования и обмена.
А как насчет C ++ 11?
Следующая версия C ++, C ++ 11, вносит одно очень важное изменение в способ управления ресурсами: Правило трех теперь называется Правило четырех (с половиной). Почему? Потому что нам не только нужно иметь возможность копировать-создавать наш ресурс, нам нужно переместить-построить его тоже.
К счастью для нас, это легко:
class dumb_array
{
public:
// ...
// move constructor
dumb_array(dumb_array&& other) noexcept ††
: dumb_array() // initialize via default constructor, C++11 only
{
swap(*this, other);
}
// ...
};
Что тут происходит? Вспомните цель move-Construction: взять ресурсы из другого экземпляра класса, оставив его в состоянии, которое гарантированно может быть присвоено и разрушаемо.
Итак, то, что мы сделали, очень просто: инициализировать с помощью конструктора по умолчанию (функция C ++ 11), а затем поменять местами с other
; мы знаем, что созданный по умолчанию экземпляр нашего класса может быть безопасно назначен и уничтожен, поэтому мы знаем, что other
сможет сделать то же самое после замены.
(Обратите внимание, что некоторые компиляторы не поддерживают делегирование конструктора; в этом случае мы должны вручную создать класс по умолчанию. Это неудачная, но, к счастью, тривиальная задача.)
Почему это работает?
Это единственное изменение, которое нам нужно внести в наш класс, так почему это работает? Помните важное решение, которое мы приняли, чтобы сделать параметр значением, а не ссылкой:
dumb_array& operator=(dumb_array other); // (1)
Теперь, если other
инициализируется rvalue, он будет сконструирован с перемещением. Идеально. Точно так же, как C ++ 03 позволяет нам повторно использовать наши функции конструктора копирования, принимая аргумент по значению, C ++ 11 автоматически выбирает конструктор перемещения, когда это необходимо. (И, конечно, как упоминалось в ранее связанной статье, копирование / перемещение значения можно просто полностью исключить.)
На этом заканчивается идиома копирования и обмена.
Сноски
* Почему мы устанавливаем mArray
равным нулю? Потому что, если какой-либо следующий код в операторе выдает ошибку, может быть вызван деструктор dumb_array
; и если это произойдет, не установив для него значение null, мы попытаемся удалить уже удаленную память! Мы избегаем этого, установив для него значение null, поскольку удаление null не является операцией.
† Есть и другие утверждения о том, что мы должны специализировать std::swap
для нашего типа, предоставить swap
внутри класса наряду со свободной функцией swap
и т. Д. Но это все ненужно: любое правильное использование swap
будет осуществляться через неквалифицированный вызов, и нашу функцию можно будет найти через ADL. Подойдет одна функция.
‡ Причина проста: если у вас есть ресурс, вы можете поменять его местами и / или переместить (C ++ 11) куда угодно. А сделав копию в списке параметров, вы максимизируете оптимизацию.
† † Конструктор перемещения обычно должен быть noexcept
, в противном случае некоторый код (например, std::vector
логика изменения размера) будет использовать конструктор копирования, даже если перемещение будет иметь смысл. Конечно, пометьте его как noexcept, только если код внутри не генерирует исключения.
person
GManNickG
schedule
19.07.2010