Всегда предпочитают стандартные контейнеры. Они имеют четко определенную семантику копирования, безопасны для исключительных ситуаций и выпускаются правильно.
Когда вы выделяете вручную, вы должны гарантировать, что код выпуска выполнен, и как члены вы должны написать правильное назначение копирования и конструктор копирования, который делает правильные вещи без утечки в случае исключения.
Руководство:
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_;
};
Неопределенное поведение. Программисты среднего уровня C ++ понимают, что используется неправильный оператор удаления. Исправьте это:
class Foo {
public:
Foo() : b_(new Bar[64]), f_(new Frob[64]) {}
~Foo() { delete [] f_; delete [] b_; }
private:
Bar *b_;
Frob *f_;
};
Плохой дизайн, утечки и двойное удаление скрываются там, если класс копируется. С копированием все в порядке, компилятор аккуратно копирует указатели для нас. Но компилятор не будет выдавать код для создания копий массивов .
Чуть более опытные программисты на C ++ признают, что не было соблюдено Правило Трех, которое говорит о том, что если вы явно написали какой-либо из деструктора, назначения копирования или конструктора копирования, вам, вероятно, также необходимо выписать другие или сделать их конфиденциальными. без реализации:
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_;
};
Правильно. ... При условии, что вы можете гарантировать, что никогда не закончится память и что ни Bar, ни Frob не смогут потерпеть неудачу при копировании. Веселье начинается в следующем разделе.
Страна чудес написания безопасного кода исключений.
Строительство
Foo() : b_(new Bar[64]), f_(new Frob[64]) {}
- Q: Что произойдет, если инициализация
f_
не удалась?
- A: Все
Frobs
, которые были построены, уничтожаются. Представьте, что 20 Frob
были построены, 21-го не получится. Тогда в порядке LIFO первые 20 Frob
будут правильно уничтожены.
Вот и все. Значит: у вас сейчас 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_);
}
Теперь мы можем использовать наш новый безопасный конструктор копирования и исключительную безопасную функцию подкачки, чтобы написать безопасный оператор копирования-назначения:
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
Что случилось? Сначала мы создаем новое хранилище и копируем в него rhs. Это может выдать, но если это произойдет, наше состояние не изменяется, и объект остается действительным.
Затем мы обмениваемся нашими кишками с временными. Временный получает то, что больше не нужно, и выпускает этот материал в конце области видимости. Мы эффективно использовали tmp в качестве мусорной корзины и правильно выбрали RAII в качестве службы сбора мусора.
Возможно, вы захотите посмотреть http://gotw.ca/gotw/059.htm или прочитать Exceptional C++
для получения более подробной информации об этой технике и о написании безопасного кода исключения.
Собираем все вместе
Краткое описание того, что нельзя бросить или не разрешено бросать:
- копирование примитивных типов никогда не выбрасывает
- деструкторам не разрешено выбрасывать (потому что в противном случае безопасный код исключения был бы невозможен вообще)
- swap функции не должны генерировать ** (и программисты на C ++, а также вся стандартная библиотека ожидают, что они не генерируют)
И вот, наконец, наша тщательно разработанная, безопасная, исправленная версия 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_;
};
Этот класс имеет чистую семантику копирования, безопасен для исключений (помните: быть безопасным для исключений не означает не выбрасывать, а скорее не пропускать и, возможно, иметь семантику транзакций), и не пропускает.