LINUX.ORG.RU

Деструкторы не нужны (?)

 


0

5

Тут в соседней теме один анон сказанул следующее:

Дело не в том. Сишка, она про написание руками + можно навесит абстракций, аки глиб/гобджект. Кресты же — изначально нагромождение абстракций со строками, срущими в кучу, функциональными объектами, методами, вызывающимися неявно (например, деструкторы, на которые жаловался кармак) и проч

Собственно, хотелось бы поговорить о выделенном.

Антон прикрылся ссылкой, по которой про деструкторы я так ничего и не нашёл. Более того, в твиттере Кармака всё выглядит с точностью до наоборот — https://twitter.com/id_aa_carmack/status/172340532419375104

RAII constructor / destructor pairing and templates for type safe collections are pretty high on my list of C++ benefits over C

Кто прав? Кто виноват? И нужны ли в итоге C++ деструкторы?

Ответ на: комментарий от dzidzitop

на C тоже можно «забыть» вызвать free(), но это потребует именно таких усилий.

Чтоб забыть на С вызвать free (fclose etc.) достаточно вставить return в середине функции, или break не там, или continue, или еще что-нибудь. И это одна из типичнейших ошибок, т.к., в отличие от С++, в С нет конструкторов/деструкторов, которые прячут такие операции. И значит приходится постоянно дергать руками различные функции для освобождения памяти и ресурсов. А в коде на С++, с которого началось обсуждение, вообще не нужен был ни new, ни delete, ни умные указатели. Нужна была только переменная на стеке. Это что касается «тоже можно „забыть“ - в С++ тоже можно писать на С и героически с этим бороться.

anonymous
()
Ответ на: комментарий от dzidzitop

Мне кажется, что любую нетривиальную инициализацию лучше в конструкторе не держать. Вокруг много разговоров о том, что исключения должны быть исключительными: malloc не удался, объект треда не создался и т.п. А в конструкторе любая ошибка — исключение. Лучше уж отдельную функцию создать, которая или порождает объект или возвращает ошибку.

uuwaan ★★
()
Ответ на: комментарий от uuwaan

Лучше уж отдельную функцию создать, которая или порождает объект или возвращает ошибку.

Можете пример показать?

eao197 ★★★★★
()
Ответ на: комментарий от eao197

отдельную функцию создать, которая или порождает объект или возвращает ошибку.

Можете пример показать?

Так любая фабрика объектов.

Кстати, про исключения: Google видимо не от хорошей жизни их вообще не использует (http://google.github.io/styleguide/cppguide.html#Exceptions)

monk ★★★★★
()
Ответ на: комментарий от monk

Так любая фабрика объектов.

Нормальные фабрики возвращают либо объект ([умный]указатель на объект), либо генерируют исключения. Чем это лучше простого конструирования объекта на месте с выбрасыванием исключений из конструктора не понятно. Ну и фабрики, вообще-то для других целей предназначены.

Мне интересно, как будет выглядеть работа с функцией, которая возвращает либо объект, либо ошибку. Причем не в языке, вроде Go, а именно в C++.

Кстати, про исключения: Google видимо не от хорошей жизни их вообще не использует (http://google.github.io/styleguide/cppguide.html#Exceptions)

Эта тема уже обсасывалась неоднократно. У них это по историческим причинам. И они сами пишут, что если бы все начинать с начала, то выбор мог бы быть и другим.

eao197 ★★★★★
()
Ответ на: комментарий от monk

Кстати, про исключения: Google видимо не от хорошей жизни их вообще не использует

Исключения для передачи ошибок, имхо, подходят плохо. Сквозь границы динамической библиотеки в общем случае не проходят, стек-трейса нет. Лично я за подход а-ля Golang: возвращай объект ошибки.

uuwaan ★★
()
Ответ на: комментарий от eao197

Нормальные фабрики возвращают либо объект ([умный]указатель на объект), либо генерируют исключения

Если класс сидит в динамической библиотеке, то фиг там, а не исключение.

Мне интересно, как будет выглядеть работа с функцией, которая возвращает либо объект, либо ошибку

Вариант 1: возвращаешь указатель на объект. Если видишь nullptr, то фабрика не шмагла.

Вариант 2: мутишь аналог Result из Rust (оно же Expected от Александреску, она же монада Option или Either, не помню). Эта шняга либо содержит сконструированный экземпляр класса, либо нет, о чём сообщает тебе через метод.

uuwaan ★★
()
Ответ на: комментарий от Dudraug

Говори, что мое видение ооп говно(с аргументами), но не мешай сюда троллинг на знание языка

Видение ООП у каждого свое и критерия истинности нет. А вот то, что в C++ не обязательно использовать ООП на каждом шагу — это медицинский факт. И непонимание этого может проистекать из-за недостаточного уровня владения инструментом.

Убеждает же в этом недостаточном уровне владения ваше стремление впендюрить работу с динамической памятью где не надо. Динамическая память в C++ — это, во-первых, дорого (причем по разным причинам) и, во-вторых, требует к себе повышенного внимания, т.к. сборщика мусора нет и приключения вроде забытого освобождения, повторного освобождения, обращения к объекту после уничтожения можно поиметь запросто.

Применять динамически созданные объекты имеет смысл, например, если время его жизни должно превышать время работы фрагмента, в котором он создан. Или если объект большой и на стеке его не разместить. Или если используется фабрика, которая создает какую-то реализацию интерфейса, о которой вы не знаете.

Ни в одном из ваших примеров надобности в этом нет. И их можно было бы переписать, скажем, вот так:

template<class ErrorHandler> class fstream {
  ErrorHandler h;
  ...
  ~fstream() {
    try {
      бла-бла
    } catch (бла-бла) {
      h.ProcessError(бла-бла);
    }
  }
};
...
int main()
{
	Test t;
        Test1 t1;

	TestF(t);
	TestF(t1);

        Test *p = &t, *p1 = &t1;

        TestF(*p);
        TestF(*p1);
}
Никаких дерганий new/delete, никаких приключений с утечкой ресурсов и т.д. Все просто и понятно.

А вот то, что вы не можете сделать просто сходу и намекает на то, что на C++ вы пока программируете как на C#. А раз так, то и наезды на свободные функции, вероятно, просто от неумения с ними обращаться.

eao197 ★★★★★
()
Ответ на: комментарий от uuwaan

Если класс сидит в динамической библиотеке, то фиг там, а не исключение.

Да ладно.

Вариант 2: мутишь аналог Result из Rust (оно же Expected от Александреску, она же монада Option или Either, не помню). Эта шняга либо содержит сконструированный экземпляр класса, либо нет, о чём сообщает тебе через метод.

В языках с АлгТД это хороший вариант, там есть паттерн-матчинг и компилятор будет бить по рукам, если не все варианты матчинга разработчик обработал.

В C++ это не так. Поэтому в C++ надежнее бросить исключение, чем возвращать Either с расчетом на то, что программист сделает проверку результата.

eao197 ★★★★★
()
Ответ на: комментарий от eao197

Мне интересно, как будет выглядеть работа с функцией, которая возвращает либо объект, либо ошибку. Причем не в языке, вроде Go, а именно в C++.

result<string> read_data() {
    ....
}

auto data = read_data();
if( data )
    cout << data->length() << *data;
else
    cerr << data.err();

Где err - по дефолту строка. но можно задать свой тип.

anonymous
()

И нужны ли в итоге C++ деструкторы?

Нужны. </thread>

Lavos ★★★★★
()
Ответ на: комментарий от eao197

Да ладно.

Угу. Чтобы это сработало, библиотека и код должны быть собраны одним компилятором, а ещё завязаны на общий динамический рантайм. В природе бывают также динамические библиотеки, которые скомпилированы со своим рантаймом, либо статически, либо динамически, но версия другая. Так что в общем случае полагаться на то, что исключение пробьёт барьер динамической библиотеки, не стоит.

Поэтому в C++ надежнее бросить исключение, чем возвращать Either

Ничто не мешает из Either кинуть исключение, если значение попытались взять без проверки.

uuwaan ★★
()
Ответ на: комментарий от uuwaan

Чтобы это сработало, библиотека и код должны быть собраны одним компилятором, а ещё завязаны на общий динамический рантайм.

В большом количестве приложений с этим вообще нет проблем. Все собирается из исходников конкретным компилятором.

Если же нужно сочинить что-то вроде COM-а, то там не только с исключениями будут проблемы, там еще и удалять объекты нужно будет той же самой библиотекой, которая объект создала.

eao197 ★★★★★
()
Ответ на: комментарий от anonymous

Ну так optional сейчас по похожему принципу работает. И мы либо получаем проверки на каждом обращении к result-> или *result, либо же пишем что-то вроде:

auto r = read_data();
if( r ) {
  auto & data = r.get();
  cout << data.length() << *data;
}
...
Что не добавляет особого удобства.

eao197 ★★★★★
()
Ответ на: комментарий от anonymous

Ну и добавлю, что подход на основе Either хорош, когда вероятность словить ошибку достаточно высока. Тогда лучше возвращать Either и проверять код возврата, чем обкладываться try/catch-ами.

Но вроде как речь шла про ситуацию, когда нетривиальные конструкторы лучше не писать, а иметь порождающую функцию для объекта. Вот с этим не очень понятно.

eao197 ★★★★★
()
Ответ на: комментарий от eao197

И мы либо получаем проверки на каждом обращении к result-> или *result, либо же пишем что-то вроде

Но ведь так и надо делать, если функция может вернуть ошибку, разве нет?

anonymous
()
Ответ на: комментарий от anonymous

Не уверен, что правильно понял тов. uuwaan. Мне показалось, что когда речь идет об объекте с полями, нуждающимися в сложной инициализации, то не правильно делать эту инициализацию в конструкторе. Т.е. неправильно писать так:

class complex_object {
  complex_field_one one_;
  complex_field_two two_;
  ...
public :
  complex_object()
    : one_(some_complex_init_one())
    , two_(some_complex_init_two())
    ...
  {}
  ...
};
...
complex_object obj;
А нужно писать как-то так:
class complex_object {
  friend either<complex_object, error_type> make_complex_object();
  complex_field_one one_;
  complex_field_two two_;
  ...
public :
  complex_object() {}
  ...
};
either<complex_object, error_type> make_complex_object() {
  complex_object obj;
  obj.one_ = some_complex_init_one();
  if(!obj.one_.succeed()) return either(some_error_code);
  obj.two_ = some_complex_init_two();
  if(!obj.two_.success()) return either(some_error_code);
  ...
  return either(obj);
}
...
auto r = make_complex_object();
if(r) { ... }
Что сильно напоминает two-phase init, который был популярен в начале 90-х.

Собственно, хотелось понять, правильно ли я понял uuwaan. И если правильно, то почему он думает, что так лучше?

eao197 ★★★★★
()
Ответ на: комментарий от eao197

Да, совершенно верно.

Что касается двухфазной инициализации, это выглядело так:

Foo x;

if (!x.init()) {
  return error;
}
И все печалились, что между созданием и вызовом Init() x является зомби-объектом. А ну как забудут вызвать? По-хорошему надо в каждом методе вставлять проверку, что объект не зомби.

Подход с Either устраняет этот недостаток и не навязывает использование исключений.

А если посмотреть презентацию Александреску «Sytematic Error Handling», то можно увидеть, что в Either можно вморозить исключение и зажечь его в нужном месте и в нужное время, а не непосредственно в месте облома. Например, исключение само по себе не может покинуть пределы треда. Но с Either его можно прикрепить к результату и прямое обращение к переменной результата либо вернёт результат, либо зажжёт именно то исключение, которое помешало этот результат получить.

uuwaan ★★
()
Ответ на: комментарий от uuwaan

Если исключения в проекте разрешены, то не вижу никаких преимуществ у данного подхода по сравнению с простым выбрасыванием исключений. Ведь основная проблема в том, что обламываться могут именно что some_complex_init_one(), some_complex_init_two() и т.д. Если они обламываются с исключениями, то объект complex_object автоматически «раскручивается взад», т.е. деинициализируются те поля, которые уже были инициализированы.

eao197 ★★★★★
()
Ответ на: комментарий от anonymous

No comments.

А можно пояснить как оценить качество библиотек по этим фрагментам кода? В смысле, я ничего криминального не увидел.

DarkEld3r ★★★★★
()
Ответ на: комментарий от Dudraug

Почему в более современных языках аля c# это считается нормой?

В «более современных c#» есть экстеншн методы, как раз чтобы не городить наследование там где не нужно.

DarkEld3r ★★★★★
()
Ответ на: комментарий от Dudraug

Свободные функции затрудняют понимание и осознание чужого кода.

Во первых, я повторюсь: эктеншн методы. Даже в шарпе дошли до понимания, что это бывает удобно.

Во вторых, ты говоришь лозунгами. Как насчёт функционального программирования, например? Или зайдём с другой стороны: затруднять понимание и расширение кода может что угодно. И развесистые/кривые иерархии классов и перегрузка операторов и шаблоны и макросы и т.д. И наоборот - это всё может упрощать код.

DarkEld3r ★★★★★
()
Ответ на: комментарий от Dudraug

Для меня задача создание функционала для сериализации вектора требует создание нового класса SerializableVector

Но ради чего? Вот используешь ты у себя в проекте какую-нибудь библиотеку и берёшь оттуда пусть этот самый Vector. И теперь тебе надо оборачивать его (и всё остальное) в новые классы, использовать их и т.д. То есть, на ровном месте добавляется куча кода, плюс постоянные конвертации туда/сюда. И это всё тебе кажется проще, чем написать одну свободную функцию?..

DarkEld3r ★★★★★
()
Ответ на: комментарий от tailgunner

В рамках дискуссии твое предложение - ровно такое же г.

Почему?

DarkEld3r ★★★★★
()
Ответ на: комментарий от DarkEld3r

А можно пояснить как оценить качество библиотек по этим фрагментам кода? В смысле, я ничего криминального не увидел.

Ты видишь что-то вроде #_new/#_end в коде на Python? Или использование строк для имен классов/методов, т.к. авторы решили особо не прятать потроха от QObject?

anonymous
()
Ответ на: комментарий от eao197

Если исключения в проекте разрешены, то не вижу никаких преимуществ у данного подхода по сравнению с простым выбрасыванием исключений.

Это снова спор о том, что лучше - исключения или коды возврата. Если твоя позиция «исключения лучше», то преимуществ нет.

tailgunner ★★★★★
()
Ответ на: комментарий от eao197

Дело не в том, дурак человек или нет. А в том, что нет возможности быть специалистом во всем.

По моему, как раз в этом. Именно дураки не замечают пробелы в своих познаниях и делают поспешные выводы. Умный человек не будет принимать решения основываясь (по крайней мере, только) на бреднях с форумов.

Ну вот тот же monk, с которым мы здесь спорим.

Да, пример неплохой. И что характерно, он не говорит ерунды типа «язык ни на что не пригоден» или что деструкторы не нужны.

Касательно Васи: если он ходит по форумам и без всякой конкретики просит совета за какой язык взяться, то пусть берёт что угодно. Всё равно или толку вообще не будет или потом он сам дойдёт до понимания.

Мне кажется, что ты такую критику сильно близко к сердцу принимаешь, хотя чего уж там - у каждого свои тараканы и я тут далеко не беспристрастный наблюдатель. Просто к С++ чуть спокойнее отношусь.

DarkEld3r ★★★★★
()
Ответ на: комментарий от anonymous

Я думаю, что речь о таком вот:

Хорошо, но ведь при переходе на новый стандарт код можно поправить. Или продолжать собирать старым компилятором. Как по мне, это не проблема.

DarkEld3r ★★★★★
()
Ответ на: комментарий от anonymous

В С++ все хорошо, пока ресурс гарантировано можно «закрыть».

Это можно расширить до «или неудачное освобождение случается крайне редко и/или не несёт особых проблем», что несколько расширяет зону применения.

DarkEld3r ★★★★★
()
Ответ на: комментарий от anonymous

Для тех, кому не нужно ничего не изменится.

Как сказать. Библиотекам ведь желательно подстроиться под возможность промежуточные исключения сохранять. То есть, как минимум, код местами усложнится. Хотя я всё-таки думаю, что основная причина: отсутствие заинтересованных в таком предложении, кто сделал бы тестовую реализацию и продвигал бы его в комитете.

DarkEld3r ★★★★★
()
Ответ на: комментарий от eao197

В том, что логика сильно усложняется. Плюс появляется try/catch почти на каждый чих.

Ну да, геморроя прибавилось бы, согласен. Или (что, наверное, даже хуже) библиотеки налагали бы разные требования и предоставляли разные гарантии.

Ведь фактического удаления из первой коллекции могло и не произойти.

Ну это от того как код написан зависит. И автору библиотеки виднее как поступать. Но если объект всё-таки разрушился, пусть и с исключением, то логично его бы из коллекции всё-таки убрать.

DarkEld3r ★★★★★
()
Ответ на: комментарий от tailgunner

Это снова спор о том, что лучше - исключения или коды возврата

Лучше их сочетание. Что-то вроде Expected<T> из Folly.

anonymous
()
Ответ на: комментарий от anonymous

Это снова спор о том, что лучше - исключения или коды возврата

Лучше их сочетание. Что-то вроде Expected<T> из Folly.

Не знаком с Folly, но мне нравится модель Rust - ожидаемые исправимые ошибки через Result, фатальные неожиданные - через panic (в Си++ для этого можно использовать исключение).

tailgunner ★★★★★
()
Ответ на: комментарий от monk

Я с трудом представляю себе fstream, у которого в зависимостях WinAPI.

Как это можно сделать в плюсах уже показали, но мне интересно как делается (делалось бы) в том же ракете. Можно минимальный пример?

DarkEld3r ★★★★★
()
Ответ на: комментарий от tailgunner

Не знаком с Folly

Folly (acronymed loosely after Facebook Open Source Library) is a library of C++11 components designed with practicality and efficiency in mind. Folly contains a variety of core library components used extensively at Facebook

https://github.com/facebook/folly

П.С. просто, чтоб было в треде для тех кому будет интересно что такое folly

anonymous
()
Ответ на: комментарий от tailgunner

Expected<T> хранит значение или std::exception_ptr на произошедшее исключение. Т.е. дает возможность возвратить значение или полную информацию о произошедшем исключении. Можно проверить на наличие значения, можно проверить на наличие конкретного типа исключения, можно заставить бросить исключение. Так же кинет сохраненное исключение, если попытаться взять значение, когда его там нет.

Сейчас мне кажется такой подход самым разумным. Тут и «коды возврата» своего рода и отсутствие проверок на каждый чих и гарантия того, что ошибка не будет молча проигнорирована.

Кстати, Вот доклад Александреску(Systematic Error Handling in C++).

anonymous
()
Ответ на: комментарий от Dudraug

В идеале сериализация в xml( и желательно json) должна быть в стандартной либе

Почему?

DarkEld3r ★★★★★
()
Ответ на: комментарий от anonymous

Folly (acronymed loosely after Facebook Open Source Library) is a library of C++11 components

Facebook... у меня предубеждение против поделий Александреску.

tailgunner ★★★★★
()
Ответ на: комментарий от uuwaan

Так что в общем случае полагаться на то, что исключение пробьёт барьер динамической библиотеки, не стоит.

Я не согласен, что это общий случай, а не наоборот, ну да ладно. В любом случае, у такой библиотеки будет сишный интерфейс, ведь у объектов точно так же может вылезти несовместимость. И всякие там Option/Result тоже идут лесом.

DarkEld3r ★★★★★
()
Ответ на: комментарий от tailgunner

Facebook... у меня предубеждение против поделий Александреску.

Не очень конструктивно. Что скажешь о подходе Expected<T>?

anonymous
()
Ответ на: комментарий от anonymous

Ты видишь что-то вроде #_new/#_end в коде на Python?

Что делают #_new/#_endя вообще не знаю, потому и спрашиваю. (:

Касательно остального: по прежнему не понимаю почему не «прятать потроха», в данном случае, плохо. Ну да, вылазит больше нюансов реализации, но ведь в Qt оно тоже так.

DarkEld3r ★★★★★
()
Ответ на: комментарий от DarkEld3r

Что делают #_new/#_endя вообще не знаю

Очевидно же - это тупо new/delete, авторы обертки настолько ленивы, что решили в языке с GC заставить пользователей вручную управлять памятью. Причем даже в С++ для данного примера не нужны ни new, ни delete,

Ну да, вылазит больше нюансов реализации, но ведь в Qt оно тоже так.

Тут есть сразу несколько недостатков. Во-первых компилятор ничего тебе не проверит. Во-вторых IDE/редактор тебе ничего не подскажет. В-третьих разбираться в ошибках будет труднее. В-четвертых писать кода надо больше. И т.д.

anonymous
()
Ответ на: комментарий от anonymous

Что скажешь о подходе Expected<T>?

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

tailgunner ★★★★★
()
Ответ на: комментарий от tailgunner

Expected может сохранить любой exception. Rust вроде не умеет(тип «ошибки» указан в типе). Expected не обязательно проверять, можно просто использовать, исключение прилетит только если значения нет.

Таким образом мы получаем лучшее из двух миров обработки ошибок.

anonymous
()
Ответ на: комментарий от DarkEld3r

Я не согласен, что это общий случай, а не наоборот

Словосочетание «в общем случае» подразумевает, что высказываемле утверждение верно для абсолютно всех случаев в рассматриваемом контексте. А контекст у нас очень широкий: «просто C++» без ссылок на конкретные компиляторы и ОС. В таком контексте легко воображается ситуация, когда ты п ытаешься из C++ кода, собираемого MSVC, дёргать C++ либу, собранную GCC. Исключения в таком случае не пройдут через границу.

В любом случае, у такой библиотеки будет сишный интерфейс

Не обязательно. Помимо сишного интерфейса портабельной мжду компиляторами является vtable. Поэтому из либы можно экспортировать чисто виртуальные классы, то бишь интерфейсы. В таком случае экземпляр создаётся функцией-конструктором, а для уничтожения указатель на интерфейс отдаётся ф-ции-финализатору. Такая пара «указатель + финализатор» в современном C++ прекрасно заворачивается в unique_ptr, для этого там есть Deleter как параметр шаблона. Об этом Мейерс писал в книжке своей.

Поскольку ф-ция у нас всё же в сишном стиле должна быть объявлена, то она, например, может возвращать указатель. Её мы заворачиваем в inline-обёртку, которая сырой указатель кладёт в умный с нужным Deleter. inline функции уже вольны использовать C++ на полную. Мы можем даже виртуальные методы интерфейса, принимающие и отдающие с переменные в стиле C, сделать приватными, а в открытой части объявить inline обёртки методов, которые упаковывают-распаковывают умные переменные из C++. Работа с таким классом, в принципе, не отличается от работы с «обычным» плюсовым экземпляром интерфейса, где всякие поинтеры, векторы и функторы используются на полную как аргументы и возвращаемые значения.

Что касается Expected, то в описанном мной примере unique_ptr может содержать nullptr, и его это нисколько не будет колыхать. А мы можем возвращать Expected<unique_ptr> и в случае nullptr делать Expected «не содержащим полезного результата». При попытке получить значение у пустого Expected он даст нам по рукам в рантайме.

В своём коде я использую Either<T,Error>. Error это самописный интерфейс для ошибок. Можно получить сообщение и цеплять такие ошибки в цепочку, чтобы наверху иметь поэтапное описание проблемы, что из-за чего отвалилось.

Пишу с телефона, так что без примера кода.

uuwaan ★★
()
Ответ на: комментарий от uuwaan

Помимо сишного интерфейса портабельной мжду компиляторами является vtable.

Кто это гарантирует?

anonymous
()
Ответ на: комментарий от anonymous

Да, не гарантируется в общем случае, сорян.

На Винде это так, потому что на этом построен COM, там это распрекрасно работает. На Линуксе надо будет потестировать сочетаемость gcc и clang на практике.

uuwaan ★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.