Pull to refresh

Comments 58

Начнём по-прядку -> Начнём по-порядку
опечатались
Отличная статья для ввода наCИльников и функциональных программистов в ООП C++ :)
Тяжело обойтись при работе с динамическими данными без динамической памяти.
RAII этому не мешает.
В статье эта техника и описана, но только с частным случаем — умными указателями. Точно так же можно работать с любыми ресурсами, которые надо гарантированно освободить.
UFO just landed and posted this here
В тексте, который вы заменили "..." (там целый абзац) я говорю о том, что C++ сам управляет ресурсами. А если вы используете new/delete, то вы берёте управление в свои руки и отказываетесь от услуг С++. Я не делаю акцента на словах new и delete; акцент на слове «управлять».
UFO just landed and posted this here
new в контексте безопасности выделения памяти в первом приближении — это всего лишь гибрид malloc и sizeof.

Кроме того, new/delete позволяют переопределить алгоритм выделения памяти «на лету», тогда как malloc гвоздями зашит в стандартную библиотеку C.

Понятно, что malloc/free в конструкторах/деструкторах использовать в общем случае ещё хуже, чем new/delete.
UFO just landed and posted this here
Еще напишите, почему нельзя так:
class Cnt {
private:
  X* ia;
  X* ib;
public:
  Cnt(int a, int b) : ia(new X(a)), ib(new X(b)) {
Интересная идея, мне как-то в голову не приходило :-)
Может быть оставить этот вопрос читателям для самостоятельно изучения ,-)
Подсказка:
чем отличается
Cnt(int a, int b): ia(new X(a)), ib(new X(b)) {}
и
Cnt(int a, int b) { ia = new X(a); ib = new X(b); }
?
> чем отличается…
Практически ничем. Утечка памяти происходит и там и там.
Я имею ввиду, почему так нельзя:

class Cnt {
private:
X* ia;
X* ib;
public:
Cnt(int a, int b): ia(new X(a)), ib(new X(b)) {
}
~Cnt() {
delete a; delete b;
}
}
Потому что в этом случае также не вызывается деструктор для объекта, на который указывает ia, если при вызове «new X(b)» выбрасывается исключение.
И ещё почему нельзя так:
foo(auto_ptr<T>(new T), auto_ptr<T>(new T));
По-идее по завершение функции эти указатели, как временные объекты, будут удалены, а следовательно в их деструкторах будут удалены данные. Наверное, если вы не будете в дальнейшем использовать эти объекты, то так вызывать можно.
Порядок вычисления аргументов в функциях неопределён, поэтому в принципе возможна ситуация, когда оба new выполнятся до конструкции соответствующих им auto_ptr и при бросании исключения вторым из них, память после первого уже не освободится.
Случай описан у Саттера, но не помню в какой именно книге.
Еще хорошо описана безопасность конструкторов в книжке Брюса Эккеля «Философия C++. Том 2: Практическое программирование.»
у Мэйерса это описано, в эффективном использовании с++
Это все хорошо, что индейцы пользуются auto_ptr

но что произойдет при присваивании?
в вашем варианте нет приватных перегруженных конструктора копирования(КК) и оператора = (ОП)

после вызова

Cnt a(1,2);
Cnt b(3,4);

a=b;

объект b передаст владение памятью, содержащей 3 и 4 объекту a

вы бы поостереглись советы такие давать.

Если уж индейцы такие настоящие то у них несколько вариантов

1) запретить копирование объекта закрыв КК и ОП
2) перегрузить КК и ОП и предотвратить слепое копирование auto_ptr
3) использовать boost::shared_ptr

UFO just landed and posted this here
о том и речь

этот абзац относится к параграфу
«Вариант первый — с указателями (осторожно, опасный код!)»

и в данном случае вариант с auto_ptr выглядит как абсолютная панацея, хотя указать на проблему копирования интеллектуальных указателей намного важнее — она не так очевидна.

Коллеги! Спасибо за комментарии. Копирование и присвоение — очень важно. Я напишу про это, когда будет время. Но эта заметка немного про другое.
UFO just landed and posted this here
>Это делает код запутанным и трудным для понимания и поддержки.
А мне казалось что это делает код понятным, так как не надо держать в голове все хитрости крестов.
Может у меня память плохая, но когда делаю что-то на крестах, то не в состоянии в голове удерживать все эти мелочи, которые описаны в куче in-depth книг. А как у вас с этим?
Мне кажется, мы говорим о разных вещах. Что вы понимаете под словом «Это»? (Я — разделение нормального хода программы и обработки ошибок; разве это разделение делает код запутанным?) Что вы понимаете под «хитрости крестов»? Мне не кажется, что конструкторы, указатели или исключения являются какими-то «хитростями»… в этой заметке нет никаких хитростей (вот в прошлой моей заметке были хитрости, а тут — нет).
>(Я — разделение нормального хода программы и обработки ошибок; разве это разделение делает код запутанным?)
На Си обычно тоже разделяют ход програмы и обработку ошибок. Тут уже больше зависит от того кто пишет код.

>Мне не кажется, что конструкторы, указатели или исключения являются какими-то «хитростями»…
Ну конструкторы и указатели не являются хитростями, а вот писать «exception-safe» код на Си++ — это уже для меня шаманство, из-за которого мои програмы текут, падают итд :) Я понимаю, что это недостаток моих знаний, но сколько ++ников реально умеет писать «exception-safe» код?
> На Си обычно тоже разделяют ход програмы и обработку ошибок. Тут уже больше зависит от того кто пишет код.

Погодите. Вот у вас есть кусок кода, в котором есть несколько wirte. В C вы должны непосредственно(!) после каждого проверить его результат, и если там -1, то хорошо бы проверить errno. В С++ вы можете заключить всё это дело в один большой try и отделить обработку исключений.

Больше того, вы можете написать библиотеку с вашими write, а обработку ошибок (исключений) оставить на усмотрение пользователя вашей библиотеки. А он пускай решает, валиться ли ему, или пытаться восстановиться… Это гуманно по отношению к пользователю :-)

>… писать exception-safe код на Си++…

Тут спорить с вами не буду. Надо ли восстанавливаться после исключений — это большой и неоднозначный вопрос. Но иногда очень хочется восстановиться :-) И это возможно!
>В С++ вы можете заключить всё это дело в один большой try и отделить обработку исключений
Втыкаем label exception: отвечающий за обработку ошибок и после каждого write делаем if..goto прямиком туда (некоторые заварачивают всё в красивые макросы). Скорее всего код получится тормознее из-за кучи if'ов, если используются zcx exceptions(вроде так их обозвали в gcc — плохо разбираюсь в этой теме).
И с райтом плохой пример, так как он возвращает всякие EAGAIN(или вы их тоже эксепшенами ловите?)… но это редкое исключение из правил :)

>а обработку ошибок (исключений) оставить на усмотрение пользователя вашей библиотеки.
В наших кодинг гайдлайнс уже много лет нет пунктика — игнорировать ошибки ;)

>Надо ли восстанавливаться после исключений — это большой и неоднозначный вопрос.
хм… всегда казалось, что в большинстве случаев надо восстанавливаться.
Но я про другой «exception-safe»

Ну и может кто-то ещё не видел FQA :)
C++ FQA >> exceptions
>Скорее всего код получится тормознее из-за кучи if'ов, если используются zcx exceptions(вроде так их обозвали в gcc — плохо разбираюсь в этой теме).
Плохо сформулировал :) Код на Си с ifами получится тормознее, чем на крестах с zero-cost исключениями.
1.1) goto — зло. аргументировать?
1.2) макросы — зло :-) аргументировать?
1.3) вот на счёт тормознутости — очень не уверен. Исключения — не бесплатная вещь. Для их обработки в стеке создаются специальные конструкции… Сейчас компиляторы очень далеко продвинулись и многое создаётся во время компиляции, но не всё. Думаю ответ на вопрос «что быстрее» будет зависеть и от компилятора и от характера кода (кол-во вложенных блоков, кол-во переменных и прочего); и if-ы вполне могут рассчитывать на победу в этой гонке :-)
1.4) я не считаю EAGAIN исключениями… как и write я не считаю ОО-путём :-) Я привёл пример из С.

2.1) Не игнорировать ошибки — это хорошо. Вопрос, что и когда(!) с ними делать. Исключения позволяют ответить на «когда» и «внутри библиотеки» и «вне библиотеки». Это очень удобно.

3.1) На сколько я понимаю. В С++ исключения разрабатывались… универсальными, но всё же больше невосстанавливающимися, чем восстанавливающимися. Достаточно вспомнить про проблемы с исключениями в деструкторах.
>1.1) goto — зло. аргументировать?
>1.2) макросы — зло :-) аргументировать?
Не надо :) Этими аргументами из книжек по плюсам воспитывают крестовых новобранцев, но мы то с вами уже понимаем в каких ситуациях их использование оправдано ;) Мы не живём в идеальном миру о котором мечтают идеологи плюсов, нам приходиться решать реальные проблемы с тормозными, багнутыми цпп компиляторами итд.

>вот на счёт тормознутости — очень не уверен.
я про zero-cost (zcx) исключения, которые с помощью libunwind'а реализованы(про виндовые средства разработки ничего не знаю). Там большой оверхед только когда происходит исключение, зато в обычной ситуации всё шустренько.

>Вопрос, что и когда(!) с ними делать.
Если пишите «exception-safe» код, то да… Но моя практика общения с типичными кодерами, вроде меня, показала что писать «exception-safe» код на ++ многие не умеют.
А иначе код ничем не будет отличаться от варианта на Си.

>3.1) На сколько я понимаю. В С++ исключения разрабатывались… универсальными, но всё же больше невосстанавливающимися, чем восстанавливающимися. Достаточно вспомнить про проблемы с исключениями в деструкторах.
Проблем у них много, но это не значит что от исключений приложение должно умирать (или мы про какое-то другое восстановление?)
Нуу… Вы меня убедили! Спасибо!
Жизнь действительно не такая, как хотелось бы; тут спорить не с чем. Но я надеюсь чуть-чуть бриблизить её к идеалу :-) Поэтому и написал это :-)
UFO just landed and posted this here
«Пусть у нас есть некий класс, конструктор которого, в некоторых случаях, может вызывать исключение»
На мой взгляд, лучше не писать конструкторов, которые могут приводить к исключениям. Я бы предложил использовать связку конструктор + дополнительный инициализирующий метод (например, setup()).
UFO just landed and posted this here
Для этого примера ответ классический — ничего. Все опасные действия (грозящие исключением) будут в setup().
Дело в том, что, на мой взгляд, при создании «опасных» конструкторов (с возможностью возникновения исключений в них), программист идет против «философии» языка. Конструктор в C++ должен создавать объект, имеющий некое стабильное состояние (даже если он в этом состоянии непригоден для использования). Все исключения — после, когда объект уже создан, в инициализирующем методе.
Исключения в конструкторах — прерогатива языков со сборщиками мусора.
Ну это вопрос спорный. Во-первых, если назвать «стабильным» непригодное состояние, то что же такое «нестабильное состояние»? Во-вторых… вы считаете, что создавать непригодные для использования объекты это хорошее решение? Мне кажется, что это весьма опасно. При создании такого объекта, каждый раз (каждый!) надо не забыть исполнить определённый ритуальный танец. Мне кажется, это очень коварные объекты.

Кроме того, нельзя полностью избежать исключений в конструкторах. Такова уж природа конструкторов. Объекту для создания может чего-то не хватить. Даже самом недоделанному.
Не опасно. Поведение объектов документируется, и ритуальный танец (состоящий из одного вызова) описывается.
Эти объекты ничуть не коварнее прочих. Вы ведь вызываете open перед началом работы с файлом? Если созданный объект непригоден для использования, то это должно тут же обнаруживаться, например, с помощью assertions.

Кроме того, нельзя полностью избежать исключений в конструкторах
Как минимум, пустой конструктор не бросит исключения, если такового не бросает базовый класс.
> Не опасно.
Три «если»: 1) документация есть и не устарела, 2) документация была прочитана 3) про танец не забыли. Не много ли?
Кроме того, если логика программы хоть сколько-то нетривиальна, то можно получить двойную инициализацию, а это бывает ещё хуже.

> Вы ведь вызываете open перед началом работы с файлом?
Нет:
#include #include using namespace std;
int main() {
ifstream in(«io.cpp»);
if (!in) {
cout << «ERROR» << endl;
return 1;
}
char buf[1024];
in.get(buf, 1024);
cout << «First line: » << buf << endl;
return 0;
}
(хотя это скорее контр-пример :-))

> пустой конструктор не бросит исключения, если такового не бросает базовый класс

И это снова не единственное «если».

Код, который работает только если всё написали, всё прочитали, ничего не забыли, никто ничего не бросил… такой код я и считаю небезопасным. статья как раз про то, как такого избежать.
assert (я_инициализирован)
assertions позволяют вам иметь столько контроля над проинициализированностью объекта, сколько вам хочется.

Код, который работает только если всё написали, всё прочитали, ничего не забыли, никто ничего не бросил… такой код я и считаю небезопасным. статья как раз про то, как такого избежать.
Вообще-то это довольно точное определение работающего кода. Я бы с большим интересом прочитал статью о методах написания кода, всегда работающего правильно, если указанные выше условия не выполнены :)
> Безумное решение с указателями…

По-моему не такое уж и безумное.

  Cnt(int a, int b, int c, int d) {
	xa = xb = xc = xd = 0;
	try {
	  xa = new X(a);
	  xb = new X(b);
	  xc = new X( c);
	  xd = new X(d);
	} catch (...) {
	  if (xa) delete xa;
	  if (xb) delete xb;
	  if (xc) delete xc;
	  if (xd) delete xd;
	  throw;
	}
  }

Немного криво выглядит, но экспоненциального роста сложности кода при увеличении числа членов класса Cnt, как видите, не происходит.
Кстати
if (xa) delete xa;

можно заменить на
delete xa;

потому что «delete 0» совершенно валидная констукция, которая просто ничего не удаляет :)
Хм… тут не поспоришь :-) Но почему-то это решение не кажется мне самым красивым :-)
А такое?

class Clazz {
public:
Clazz() try
{
xa = xb = xc = xd = 0;
xa = new X(a);
xb = new X(b);
xc = new X( c);
xd = new X(d);
}
catch(...)
{
_CleanUp();
}

public:
~Clazz()
{
_CleanUp();
}

private:
void _CleanUp() { /*тут корректно чистим*/ }
};
Постарался причесать.

class Clazz
{
public:
Clazz() try
{
xa = xb = xc = xd = 0;
xa = new X(a);
xb = new X(b);
xc = new X( c);
xd = new X(d);
}
catch(...)
{
_CleanUp();
}

public:
virtual ~Clazz()
{
_CleanUp();
}

private:
NOTHROW void _CleanUp() { /*тут корректно чистим*/ }
};
Даа… причесали вы не на шутку… стало очень прилично… но всё же лично я не стал бы так делать :-) Хотя, за виртуальный деструктор — отдельный респект! :-)
UFO just landed and posted this here
Есть такая парадигма в программировании, как exception-safe constructor. Я ее придерживаюсь и вам рекомендую. Накладные расходы — дополнительный метод Init.
Даже странно, что ещё никто не вспомнил Скотта Мейерса с его 10м советом из книги «Наиболее эффективное использование с++». Та же логика рассуждений, те же std::auto_ptr от которых он в следующей книге отрекается в пользу boost::shared_ptr.

Вообщем как это не прискорбно говорить, но классику жанра всё-таки нужно читать.
Под следующей книгой Майерса имеется в виду Effective STL?
Как только вы написали «new», вы обязали себя написать «delete» везде, где это нужно.
Неправильно. Как только вы написали new, вы обязаны завернуть результат в умный указатель и забыть про delete вообще.
Sign up to leave a comment.

Articles