Ответ 1
Всегда предпочитайте стандартные контейнеры. Они имеют четко определенную семантику копирования, безопасны для исключений и правильно освобождаются.
При назначении вручную вы должны гарантировать, что код выпуска будет выполнен, а в качестве членов вы должны написать правильное назначение копии и конструктор копирования, который делает правильную вещь без утечки в случае исключения.
Руководство:
int *i = 0, *y = 0;
try {
i = new int [64];
y = new int [64];
} catch (...) {
delete [] y;
delete [] i;
}
Если мы хотим, чтобы наши переменные имели только ту область, в которой они действительно нужны, она становится вонючей:
int *i = 0, *y = 0;
try {
i = new int [64];
y = new int [64];
// code that uses i and y
int *p;
try {
p = new int [64];
// code that uses p, i, y
} catch(...) {}
delete [] p;
} catch (...) {}
delete [] y;
delete [] i;
Или просто:
std::vector<int> i(64), y(64);
{
std::vector<int> p(64);
}
Это ужас для реализации этого для класса с семантикой копирования. Копирование может быть брошено, распределение может бросить, и мы хотим, чтобы в семантике транзакции. Пример взломал бы этот ответ.
Хорошо здесь.
Класс для копирования - ручное управление ресурсами и контейнеры
У нас есть этот невинно выглядящий класс. Как оказалось, это довольно зло. Мне напомнили об американской Макги Алисе:
class Foo {
public:
Foo() : b_(new Bar[64]), f_(new Frob[64]) {}
private:
Bar *b_;
Frob *f_;
};
Утечки. Большинство начинающих программистов на C++ распознают, что там отсутствует удаление. Добавьте их:
class Foo {
public:
Foo() : b_(new Bar[64]), f_(new Frob[64]) {}
~Foo() { delete f_; delete b_; }
private:
Bar *b_;
Frob *f_;
};
Undefined. Промежуточные программисты на C++ признают, что используется неправильный оператор удаления. Исправьте это:
class Foo {
public:
Foo() : b_(new Bar[64]), f_(new Frob[64]) {}
~Foo() { delete [] f_; delete [] b_; }
private:
Bar *b_;
Frob *f_;
};
Плохой дизайн, утечки и двойные удаления скрываются там, если класс скопирован. Само копирование прекрасно, компилятор полностью копирует указатели для нас. Но компилятор не будет генерировать код для создания копий массивов.
Немножко более опытные программисты на C++ признают, что правило 3 не соблюдалось, в котором говорится, что если вы явно написали какой-либо деструктор, назначение копии или конструктор копирования, вам, вероятно, также придется выписывать остальные или делать их частными без реализации:
class Foo {
public:
Foo() : b_(new Bar[64]), f_(new Frob[64]) {}
~Foo() { delete [] f_; delete [] b_; }
Foo (Foo const &f) : b_(new Bar[64]), f_(new Frob[64])
{
*this = f;
}
Foo& operator= (Foo const& rhs) {
std::copy (rhs.b_, rhs.b_+64, b_);
std::copy (rhs.f_, rhs.f_+64, f_);
return *this;
}
private:
Bar *b_;
Frob *f_;
};
Правильно.... При условии, что вы можете гарантировать, чтобы никогда не хватало памяти, и ни одна из них, кроме Frob, не может работать при копировании. Fun начинается в следующем разделе.
Страна чудес написания кода исключения исключений.
Строительство
Foo() : b_(new Bar[64]), f_(new Frob[64]) {}
- В: Что произойдет, если не удалось выполнить инициализацию
f_
? - A: Все
Frobs
, которые были построены, уничтожены. Представьте себе, что 20Frob
были построены, а 21-й провалится. Затем, в порядке LIFO, первые 20Frob
будут правильно уничтожены.
Что это. Значит: теперь у вас 64 зомби Bars
. Сам объект Foos
никогда не оживает, поэтому его деструктор не будет называться.
Как сделать это исключение безопасным?
Конструктор должен всегда полностью или полностью завершаться. Он не должен быть полуживым или полумертвым. Решение:
Foo() : b_(0), f_(0)
{
try {
b_ = new Bar[64];
f_ = new Foo[64];
} catch (std::exception &e) {
delete [] f_; // Note: it is safe to delete null-pointers -> nothing happens
delete [] b_;
throw; // don't forget to abort this object, do not let it come to life
}
}
Копирование
Помните наши определения для копирования:
Foo (Foo const &f) : b_(new Bar[64]), f_(new Frob[64]) {
*this = f;
}
Foo& operator= (Foo const& rhs) {
std::copy (rhs.b_, rhs.b_+64, b_);
std::copy (rhs.f_, rhs.f_+64, f_);
return *this;
}
- В: Что произойдет, если какая-либо копия не удалась? Возможно,
Bar
придется копировать тяжелые ресурсы под капот. Он может потерпеть неудачу, он будет. - A: В момент исключения все объекты, скопированные до сих пор, останутся такими.
Это означает, что наш Foo
теперь находится в противоречивом и непредсказуемом состоянии. Чтобы дать ему семантику транзакций, нам нужно полностью или полностью создать новое состояние или вообще не использовать, а затем использовать операции, которые нельзя внедрить для имплантации нового состояния в наш Foo
. Наконец, нам нужно очистить временное состояние.
Решение состоит в использовании икону копирования и свопинга (http://gotw.ca/gotw/059.htm).
Сначала мы уточним наш конструктор копирования:
Foo (Foo const &f) : f_(0), b_(0) {
try {
b_ = new Bar[64];
f_ = new Foo[64];
std::copy (rhs.b_, rhs.b_+64, b_); // if this throws, all commited copies will be thrown away
std::copy (rhs.f_, rhs.f_+64, f_);
} catch (std::exception &e) {
delete [] f_; // Note: it is safe to delete null-pointers -> nothing happens
delete [] b_;
throw; // don't forget to abort this object, do not let it come to life
}
}
Затем мы определяем неперебрасываемую функцию свопинга
class Foo {
public:
friend void swap (Foo &, Foo &);
};
void swap (Foo &lhs, Foo &rhs) {
std::swap (lhs.f_, rhs.f_);
std::swap (lhs.b_, rhs.b_);
}
Теперь мы можем использовать наш новый безопасный экземпляр-конструктор исключаемых исключений и безопасную функцию swap для записи безопасного для исключения экземпляра оператора-копии:
Foo& operator= (Foo const &rhs) {
Foo tmp (rhs); // if this throws, everything is released and exception is propagated
swap (tmp, *this); // cannot throw
return *this; // cannot throw
} // Foo::~Foo() is executed
Что случилось? Сначала мы создаем новое хранилище и копируем его в него. Это может бросить, но если это произойдет, наше состояние не будет изменено, и объект останется в силе.
Затем мы обмениваем наши кишки с временными кишками. Временное получает то, что больше не нужно, и выпускает этот материал в конце области действия. Мы эффективно использовали tmp в качестве мусорной корзины и правильно выбираем RAII в качестве службы сбора мусора.
Вы можете посмотреть http://gotw.ca/gotw/059.htm или прочитать Exceptional C++
для получения дополнительной информации об этом методе и написании кода исключения исключений.
Объединение вместе
Резюме того, что нельзя бросить или не разрешено бросать:
- копировать примитивные типы никогда не бросает
- деструкторам не разрешено бросать (поскольку в противном случае исключающий код безопасности вообще невозможен)
- swap функции не должны бросать ** (а программисты на С++, а также вся стандартная библиотека ожидают, что они не будут бросать)
И вот, наконец, наша тщательно разработанная, безопасная, исправленная версия Foo:
class Foo {
public:
Foo() : b_(0), f_(0)
{
try {
b_ = new Bar[64];
f_ = new Foo[64];
} catch (std::exception &e) {
delete [] f_; // Note: it is safe to delete null-pointers -> nothing happens
delete [] b_;
throw; // don't forget to abort this object, do not let it come to life
}
}
Foo (Foo const &f) : f_(0), b_(0)
{
try {
b_ = new Bar[64];
f_ = new Foo[64];
std::copy (rhs.b_, rhs.b_+64, b_);
std::copy (rhs.f_, rhs.f_+64, f_);
} catch (std::exception &e) {
delete [] f_;
delete [] b_;
throw;
}
}
~Foo()
{
delete [] f_;
delete [] b_;
}
Foo& operator= (Foo const &rhs)
{
Foo tmp (rhs); // if this throws, everything is released and exception is propagated
swap (tmp, *this); // cannot throw
return *this; // cannot throw
} // Foo::~Foo() is executed
friend void swap (Foo &, Foo &);
private:
Bar *b_;
Frob *f_;
};
void swap (Foo &lhs, Foo &rhs) {
std::swap (lhs.f_, rhs.f_);
std::swap (lhs.b_, rhs.b_);
}
Сравните это с нашим первоначальным, невиновным кодом, который является злым для костей:
class Foo {
public:
Foo() : b_(new Bar[64]), f_(new Frob[64]) {}
private:
Bar *b_;
Frob *f_;
};
Лучше не добавлять к нему больше переменных. Рано или поздно вы забудете добавить правильный код в каком-либо месте, и весь ваш класс заболеет.
Или сделать его не скопированным.
class Foo {
public:
Foo() : b_(new Bar[64]), f_(new Frob[64]) {}
Foo (Foo const &) = delete;
Foo& operator= (Foo const &) = delete;
private:
Bar *b_;
Frob *f_;
};
Для некоторых классов это имеет смысл (потоки, для экземпляра, для обмена потоками, явное с std:: shared_ptr), но для многих это не так.
Реальное решение.
class Foo {
public:
Foo() : b_(64), f_(64) {}
private:
std::vector<Bar> b_;
std::vector<Frob> f_;
};
Этот класс имеет чистую семантику копирования, безопасен для исключения (помните: безопасное исключение не означает, что он не бросает, а скорее не течет и, возможно, имеет семантику транзакции) и не течет.