Конвекс Хал — даются точки, надо их обвязать, как бы, резинкой вокруг.
Я придумал свой алгоритм (велосипед), реализовал на плюсах, опубликовал в гит хабе. Даже написал описание алгоритма с картинками. Все в академических целях. Помогите, пожалуйста. Сделайте код ревью. Код не длинный, должен быть понятен.
Цель: читабельность, легкая модификация (расширение) в будущем, быстродействие в рамках разумного, но не в ущерб всему остальному.
Программа консольная. Берет файл с картинкой, окружает точки конвекс халом, сохраняет результат в другой файл. Екзешник, кстати, проверен софтпедией, вирусов и малваре нету. Смотрите там есть ссылка на сертификат.
Зачем это нужно?
Хочется пообщаться на технические темы. Научиться чему-то новому и самому поделиться опытом с товарищами.
Здравствуйте, greenpci, Вы писали:
G>Конвекс Хал — даются точки, надо их обвязать, как бы, резинкой вокруг.
"Выпуклая оболочка", дорогой Ватсон!
G>Я придумал свой алгоритм (велосипед), реализовал на плюсах, опубликовал в гит хабе. Даже написал описание алгоритма с картинками. Все в академических целях. Помогите, пожалуйста. Сделайте код ревью. Код не длинный, должен быть понятен.
G>Цель: читабельность, легкая модификация (расширение) в будущем, быстродействие в рамках разумного, но не в ущерб всему остальному.
G>Программа консольная. Берет файл с картинкой, окружает точки конвекс халом, сохраняет результат в другой файл. Екзешник, кстати, проверен софтпедией, вирусов и малваре нету. Смотрите там есть ссылка на сертификат.
Есть ряд моментов, которые стоит отшлифовать.
1. Странное разбиение на файлы:
— исключение в common.h, почему бы не в ch_exception.h?
— SharedPoint опять в common.h, тогда как PointVector и SharedPointVector — в point.h, а PointList и SharedPointList — в convex_hull.h
2. По-моему, изрядный перебор с абстрагированием. Особенно странно он смотрится в сочетании с типами контейнеров, прибитыми гвоздями.
Интерфейс можно было как-то попроще сделать
struct Point { int x, y; }; // для начала, зафиксируем тип точки
// в будущем можно его сделать произвольным - а алгоритм, соответственно, полиморфным
// для тех, кому нравится работать с произвольными контейнерами:template<class InIter, class OutIter>
OutIter convex_hull(InIter begin, InIter end, OutIter dst)
{
std::vector<Point> points(begin, end);
do_convex_hull(points);
std::copy(points.begin(), points.end(), dst);
}
// нешаблонная функция - реализацию можно вынести в отдельный .cpp
// тип рабочего контейнера зафиксирован - vector<Point>
// (в будущем можно сделать произвольным - например, vector<void*>
// плюс передавать стратегию для работы с точками: функции
// get_x(void const*), get_y(void const*), remove(void*)
// )void do_convex_hull(std::vector<Point>& points)
{
// отсортировать в полярных координатах
// удалить точки, образующие вогнутые углы
}
3. Можем подумать об инкрементных алгоритмах.
Если все точки поступают на вход, отсортированные по y и x (по сканлиниям), то выпуклую оболочку можно наращивать.
— для верхней-левой точки оболочка тривиальна и состоит из неё
— для оболочки, состоящей из левой и правой полилиний, новая точка либо просто добавляется к полилиниям, либо заставляет последовательно отбросить некоторые предыдущие вершины (образующие вогнутую дугу) и, опять же, добавляется
То есть, нам потребуются два вектора.
4. shared_ptr'ы нигде не нужны. Откройте для себя передачу аргументов по ссылке
То есть, моё мнение: код нужно переделать вдребезги пополам
Либо очень тщательно обосновать каждый пункт: зачем ЭТО сделано ТАК и вынесено СЮДА (или не вынесено, а помещено в общую кучу).
I read about the problem of finding Convex Hull in Algorithm Design Manual and knew that the complexity should be N for points sorted by one coordinate. Before reading about existing solutions I determined to find my own. “Invent the wheel” in other words. I spent hours thinking about a solution. In the end I found one:
Зачем писать об этом статью?
В плане твоей личной практики это безусловно полезно, но все остальным это ∥
После беглого просмотра:
1. Класс без данных, с одним значимым публичным методом (зачем пустые конструткор и деструктор?) с кучей shared_ptr, — это какой-то жуткий C#/Java-style — но даже там применяют статические методы.
В C++ есть такая дефицитная роскошь как non-member functions — и их нужно применять в первую очередь, а к классам переходить только при практически значимой обоснованности (например инвариант).
Тебе нужно прочитать Страуструпа — TC++PL (можешь немного подождать, 20 мая выходит 4я редакция с C++11).
Пару месяцев назад я реализовывал Convex Hull for fun на основе известного алгоритма монотонных цепочек. Вся реализация это 39 строчек:
Причём третий шаблон функции это чистый синтаксический сахар — без него 27 строчек.
Эти 27 строчек работают с любыми выходными и любыми выходными range'ми, с любыми типами точек. + половину алгоритма, можно использовать отдельно — monotone_chain.
Feel the difference.
2. Изучи CMake, вместо заболоченных файлов студии у тебя будет один аккуратненький CMakeLists.txt — на основе которого можно генерировать как студийные проекты, так и Makefiles для Unix.
Здравствуйте, Кодт, Вы писали:
К>Здравствуйте, greenpci, Вы писали:
G>>Конвекс Хал — даются точки, надо их обвязать, как бы, резинкой вокруг.
К>"Выпуклая оболочка", дорогой Ватсон!
да, правильно. Я представлял что выпуклый корпус (hull — the body or frame of a ship, most of which goes under the water (Cambridge Dictionary), но оболочка гораздо лучше.
...
К>Есть ряд моментов, которые стоит отшлифовать. К>1. Странное разбиение на файлы: К>- исключение в common.h, почему бы не в ch_exception.h? К>- SharedPoint опять в common.h, тогда как PointVector и SharedPointVector — в point.h, а PointList и SharedPointList — в convex_hull.h
Принимаю. Вспоминаю, сам чувствовал вину, когда разместил в common.h вместо отдельного файла. В C# обычно правила жесткие — каждый клас, интерфейс, даже нестед в отдельный файл. В си плюс плюс также надо похоже.
К>2. По-моему, изрядный перебор с абстрагированием. Особенно странно он смотрится в сочетании с типами контейнеров, прибитыми гвоздями. К>Интерфейс можно было как-то попроще сделать К>
К>struct Point { int x, y; };
// для начала, зафиксируем тип точки К>// в будущем можно его сделать произвольным — а алгоритм, соответственно, полиморфным
Я использовал инкрементальный алгоритм, как ты говорил ниже. Мотивация моя была в следующем: меньше памяти (ведь точек может быть очень много) и не надо делать два прохода: сохранить точки в векторе, пройтись по ним для нахождения выпуклой оболочки. Я 1*N сэкономил этим, не считая расширения вектора при вставке (чтобы обойти расширение, мне бы пришлось объявлять вектор на все точки — увеличить потребление памяти в два раза).
Ты тоже упоминаешь инкрементальный алгоритм ниже, тольео у тебя "Push", а у меня "Pull". Сказались годы работы на шарпе с его IEnumerable. Ты считаешь этот интерфейс (см ниже) перебор с абстрагированием? Хм... Возможно. Я просто думал, как же си шарпный IEnumerable<Point> на плюсах сделать. Ничего лучшего чем это не придумал.
Прибитые гвоздями — это похоже про shared_ptr. Обосную ниже.
К>// для тех, кому нравится работать с произвольными контейнерами: К>template<class InIter, class OutIter> К>OutIter convex_hull(InIter begin, InIter end, OutIter dst) К>{ К> std::vector<Point> points(begin, end); К> do_convex_hull(points); К> std::copy(points.begin(), points.end(), dst); К>}
Хороший интерфейс, а ля STL. Только слишком много N добавляет. Два копирования. Кстати в моей первой версии кода я тоже передавал vector. Потом поменял на "Pull-IEnumerable" при мыслях о производительности и памяти.
К>// нешаблонная функция — реализацию можно вынести в отдельный .cpp К>// тип рабочего контейнера зафиксирован — vector<Point> К>// (в будущем можно сделать произвольным — например, vector<void*> К>// плюс передавать стратегию для работы с точками: функции К>// get_x(void const*), get_y(void const*), remove(void*) К>// ) К>void do_convex_hull(std::vector<Point>& points) К>{ К> // отсортировать в полярных координатах К> // удалить точки, образующие вогнутые углы К>} К>[/c]
По поводу void* тут сразу два нарушения:
C++ Coding Standards 101 Rules by Alexandresku and Sutter 90. Avoid type switching; prefer polymorphism 92. Avoid using reinterpret_cast
Я думаю все плюсники эту книжку должны любить пламенной любовью.
Хотя с другой стороны в дот нете WPF постоянно Object грызет. Не type safe нифига.
К>3. Можем подумать об инкрементных алгоритмах. К>Если все точки поступают на вход, отсортированные по y и x (по сканлиниям), то выпуклую оболочку можно наращивать. К>- для верхней-левой точки оболочка тривиальна и состоит из неё К>- для оболочки, состоящей из левой и правой полилиний, новая точка либо просто добавляется к полилиниям, либо заставляет последовательно отбросить некоторые предыдущие вершины (образующие вогнутую дугу) и, опять же, добавляется К>То есть, нам потребуются два вектора. К>
Это твой "Push" как я понимаю. То есть код, читающий точки, должен будет получить ConvexHullBuilder. Тебе не кажется, что это нарушение Rule Of Demeter (Least knowledge) и Separation of Concern. Получается, что тот код будет tightly coupled с конвекс халом. А что если я захочу просто точки получить для чего-нибудь другого, применяя тот же клас? Смысл моих абстракций вверху был "разлучить" код читающий точки и код делающий оболочку. Чтобы они были независимыми.
К>4. shared_ptr'ы нигде не нужны. Откройте для себя передачу аргументов по ссылке
C++ Coding Standards 101 Rules by Alexandresku and Sutter 79. Store only values and smart pointers in containers
Я не хотел передавать Point по значению, потому, что мой поинт был с двумя double — 16 байт. Копировать их при передаче значения было бы долго. Использовать референсы можно было бы, но у меня они уже должны были быть обернутыми в shared_ptr, из-за Александреску с его правилами (см. выше)
Еще вот это
virtual SharedPoint NextPointInternal() = 0;
не понятно как сделать без SharedPoint.
Ты в своем void add(Point pt) вообще копируешь по значению. Я кстати оставил double на будущее. Вдруг захотят дробные на вход подять.
К>То есть, моё мнение: код нужно переделать вдребезги пополам
блин, ты бы видел какой пишут код тут у нас. Поубивал бы.
По поводу книжки. Ищите в гугле: C++ Coding Standards: 101 Rules, Guidelines, and Best Practices
By Herb Sutter, Andrei Alexandrescu
Здравствуйте, Abyx, Вы писали:
A>using namespace std; это плохо
я использовал его только в cpp файлах. Зметь ни во одном h файле его нет.
Использовать using namespace нельзя только в headers. В cpp его даже рекомендуется использовать. Ты с этим не согласен?
A>f(void) в C++ не нужно
принимаю
A>пустые конструкторы/деструкторы в .cpp — зачем?
принимаю.
A>используйте #pragma once
Обучен по старым книжкам. Может какой компилятор вдруг не поймет эту прагму. Я хотел сделать максимально портабл.
A>С++03 не нужен, пишите на С++11
принимаю. Я консервативен. Хотя я даже ламбду там использовал в одном месте.
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Зачем писать об этом статью? EP>В плане твоей личной практики это безусловно полезно, но все остальным это ∥
Это вообще вопрос о целесообразности писания блогов. Цель статьи, в первую очередь, описать алгоритм и сказать, что это новый (т.е. придуманный мной) алгоритм, чтобы люди знали, чего ожидать.
EP>После беглого просмотра: EP> EP>1. Класс без данных, с одним значимым публичным методом (зачем пустые конструткор и деструктор?) с кучей shared_ptr, — это какой-то жуткий C#/Java-style — но даже там применяют статические методы.
По поводу shared_ptr я ответил Кодт. Почерпнул из книг. Хотел бы посмотреть на интерфейс без них, который соответствует книжке Code Standards (см ответ на Кодт.)
EP>В C++ есть такая дефицитная роскошь как non-member functions — и их нужно применять в первую очередь, а к классам переходить только при практически значимой обоснованности (например инвариант).
Принимаю. +1
Совершенно справедливо. Когда создавал клас, еще не знал, что он там будет не нужен и привычка из си шарпа сработала. Потом про это не подумал.
EP>Тебе нужно прочитать Страуструпа — TC++PL (можешь немного подождать, 20 мая выходит 4я редакция с C++11).
Почитаю, если обратно на си шарп не загонят.
EP>Пару месяцев назад я реализовывал Convex Hull for fun на основе известного алгоритма монотонных цепочек. Вся реализация это 39 строчек: EP>Feel the difference.
У меня сам конвекс тоже будет очень маленьким. Там еще много чего есть: чтение файла, обработка параметров командной строки, рисование отрезка по точкам (тоже велосипед кстати), плюс всякие патерные трюки для абстракций. Я, как бы, представлял себе для академических целей, что это большой проект и он будет постоянно обновляться.
EP>2. Изучи CMake, вместо заболоченных файлов студии у тебя будет один аккуратненький CMakeLists.txt — на основе которого можно генерировать как студийные проекты, так и Makefiles для Unix.
Это полезный совет. Согласен про заболоченные файлы студии. Я иногда сам вычищаю проекты из студии в текстовом редакторе.
Здравствуйте, greenpci, Вы писали:
A>>using namespace std; это плохо G>я использовал его только в cpp файлах. Зметь ни во одном h файле его нет. G>Использовать using namespace нельзя только в headers. В cpp его даже рекомендуется использовать. Ты с этим не согласен?
кем рекомендуется o_O?
о using namespace std; хорошо написано тут — http://stackoverflow.com/a/1453605/343443
A>>используйте #pragma once G>Обучен по старым книжкам. Может какой компилятор вдруг не поймет эту прагму. Я хотел сделать максимально портабл.
все основные компиляторы понимают. а вот <windows.h> — уже не все.
Здравствуйте, greenpci, Вы писали:
EP>>Зачем писать об этом статью? EP>>В плане твоей личной практики это безусловно полезно, но все остальным это ∥ G>Это вообще вопрос о целесообразности писания блогов. Цель статьи, в первую очередь, описать алгоритм и сказать, что это новый (т.е. придуманный мной) алгоритм, чтобы люди знали, чего ожидать.
То есть так: "Я не изучил существующие алгоритмы, не ждите сравнений и подчёркивание различий, а тем более устоявшейся терминологии. Так как я не знал уже готовые алгоритмы, отныне и навеки нарекаю приведённый алгоритм НОВЫМ ПРИДУМАННЫМ МНОЙ, ибо clean room design. Любые совпадения с реальными существующими алгоритмами являются чистой случайностью"
G>По поводу shared_ptr я ответил Кодт. Почерпнул из книг.
G>Я не хотел передавать Point по значению, потому, что мой поинт был с двумя double — 16 байт.
1. на x64 sizeof(shared_ptr<Point>)==16
2. реально памяти выделяется больше
3. много дорогих аллокаций
4. из-за индерекций последовательный доступ к элементам vector<shared_ptr<Point>> примерно до 4 раз медленней чем у vector<Point>
5. ref-counting у shared_ptr является thread-safe, со всеми вытекющими
6. я не увидел ответа на вопрос, что мешает предавать по ссылке
G>Хотел бы посмотреть на интерфейс без них, который соответствует книжке Code Standards (см ответ на Кодт.)
Я показал реализацию без shared_ptr выше
EP>>Пару месяцев назад я реализовывал Convex Hull for fun на основе известного алгоритма монотонных цепочек. Вся реализация это 39 строчек: EP>>Feel the difference. G>У меня сам конвекс тоже будет очень маленьким. Там еще много чего есть: чтение файла, обработка параметров командной строки, рисование отрезка по точкам (тоже велосипед кстати), плюс всякие патерные трюки для абстракций.
В тех 27 строчках выше абстракций больше — они применимы к на порядки большему количеству входных типов(разные входные/выходные ranges, разные точки).
Здравствуйте, Abyx, Вы писали:
A>>>using namespace std; это плохо G>>я использовал его только в cpp файлах. Зметь ни во одном h файле его нет. G>>Использовать using namespace нельзя только в headers. В cpp его даже рекомендуется использовать. Ты с этим не согласен? A>кем рекомендуется o_O? A>о using namespace std; хорошо написано тут — http://stackoverflow.com/a/1453605/343443
Та же книжка:
Coding Standards 101 Rules Sutter and Alexandresku 59. Don't write namespace usings in a header file or before an #include
...
In short: You can and should use namespace using declarations and directives liberally in your implementation files after #include directives and feel good about it. Despite repeated assertions to the contrary, namespace using declarations and directives are not evil and they do not defeat the purpose of namespaces. Rather, they are what make namespaces usable.
In short: You can and should use namespace using declarations and directives liberally in your implementation files after #include directives and feel good about it. Despite repeated assertions to the contrary, namespace using declarations and directives are not evil and they do not defeat the purpose of namespaces. Rather, they are what make namespaces usable.
Namespaces deliver the powerful advantage of unambiguous name management. Most of the time, different programmers don't choose the very same name for a type or function; but on the off chance that they do so, and that the two pieces of code end up being used together, having those names in separate namespaces prevents them from colliding. (We don't, after all, want the global namespace pollution experienced by default in languages like C.) In the rare case when there is such an actual ambiguity, calling code can explicitly qualify a name to say which one it wants. But the vast majority of the time there is no ambiguity: And that is why namespace using declarations and directives are what make namespaces usable, because they greatly reduce code clutter by freeing you from having to tediously qualify every name every time (which would be onerous, and frankly people just won't put up with it) and still letting you qualify names only in those rare cases when you need to resolve an actual ambiguity.
Еще в далеком 2004 году, Александреску знал о том, что Abyx и sbi на stackoverflow будут убеждать меня, что namespace in cpp after headers это зло. Поэтому он добавил "Despite repeated assertions to the contrary". Может это был Сатер. У книжки два автора.
A>>>используйте #pragma once G>>Обучен по старым книжкам. Может какой компилятор вдруг не поймет эту прагму. Я хотел сделать максимально портабл. A>все основные компиляторы понимают. а вот <windows.h> — уже не все.
Про виндовс понятно. Использовал его только для вывода OutputDebugString();
Про прагму приму на вооружение.
@Evgeny.Panasyuk
Я нарушил правило
Coding Standards 101 Rules Sutter and Alexandresku 44. Prefer writing nonmember nonfriend functions
Avoid membership fees: Where possible, prefer making functions nonmember nonfriends.
мне оно сразу понравилось после си шарпа. Надоел уже этот каплинг и блоат с классами. По поводу статических методов. Они там тоже не очень в почете из-за юнит тестов, моков и прочего. Если твой статический метод использует другой клас, то по законам ТиДиДи, ты должен создать фейк. А это делается с помощью инхеританс и т.д. Так что в мире Си шарпа мои коллеги пихают все в классы кода надо и не надо. Плюс там нету замечательной изоляции cpp файлов.
...
Since then, I've been battling programmers who've taken to heart the lesson that being object-oriented means putting functions inside the classes containing the data on which the functions operate. After all, they tell me, that's what encapsulation is all about.
10.3.2 Helper Functions
Typically, a class has a number of functions associated with it that need not be defined in the class itself because they don’t need direct access to the representation. For example:
int diff(Date a,Date b); // number of days in the range [a,b) or [b,a)bool leapyear(int y);
Date next_weekday(Date d);
Date next_saturday(Date d);
Defining such functions in the class itself would complicate the class interface and increase the number of functions that would potentially need to be examined when a change to the representation was considered. How are such functions "associated" with class Date? Traditionally, their declarations were simply placed in the same file as the declaration of class Date, and users who needed Dates would make them all available by including the file that defined the interface.
While we could make a member function to return length, it is better to make it a global friend function. If we do that, we will be able eventually to define the same function to work on built-in arrays and achieve greater uniformity of design. I made size into a member function in STL in an attempt to please the standard committee. I knew that begin, end and size should be global functions but was not willing to risk another fight with the committee. In general, there were many compromises of which I am ashamed. It would have been harder to succeed without making them, but I still get a metallic taste in my mouth when I encounter all the things that I did wrong while knowing full how to do them right. Success, after all, is much overrated. I will be pointing to the incorrect designs in STL here and there: some were done because of political considerations, but many were mistakes caused by my inability to discern general principles.)
Здравствуйте, greenpci, Вы писали:
G>Конвекс Хал — даются точки, надо их обвязать, как бы, резинкой вокруг. G>Я придумал свой алгоритм (велосипед), реализовал на плюсах, опубликовал в гит хабе. Даже написал описание алгоритма с картинками. Все в академических целях. Помогите, пожалуйста. Сделайте код ревью. Код не длинный, должен быть понятен.
G>Цель: читабельность, легкая модификация (расширение) в будущем, быстродействие в рамках разумного, но не в ущерб всему остальному.
G>Программа консольная. Берет файл с картинкой, окружает точки конвекс халом, сохраняет результат в другой файл. Екзешник, кстати, проверен софтпедией, вирусов и малваре нету. Смотрите там есть ссылка на сертификат.
G>Зачем это нужно?
G>Хочется пообщаться на технические темы. Научиться чему-то новому и самому поделиться опытом с товарищами.
G>Вот ссылка: http://github.com/evpo/ConvexHull
G>Заранее прошу прощение за английский язык для тех кому трудно читать и других кто замечает ошибки. Кстати замечания по этому поводу тоже принимаются.
G>Спасибо заранее.
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>>>Зачем писать об этом статью? EP>То есть так: "Я не изучил существующие алгоритмы, не ждите сравнений и подчёркивание различий, а тем более устоявшейся терминологии. Так как я не знал уже готовые алгоритмы, отныне и навеки нарекаю приведённый алгоритм НОВЫМ ПРИДУМАННЫМ МНОЙ, ибо clean room design. Любые совпадения с реальными существующими алгоритмами являются чистой случайностью"
да, совершенно верно. Это действительно был Clean Room Design. Я не бью себя кулаком в грудь. Задача считается простой и думаю многие тоже бы придумали свой алгоритм. Не велика заслуга.
G>>По поводу shared_ptr я ответил Кодт. Почерпнул из книг.
EP>
G>>Я не хотел передавать Point по значению, потому, что мой поинт был с двумя double — 16 байт.
EP>1. на x64 sizeof(shared_ptr<Point>)==16
ну ты x64 натянул для увеличения размера.
Если серьезно на 64 ожидал падение производительности.
EP>2. реально памяти выделяется больше EP>3. много дорогих аллокаций EP>4. из-за индерекций последовательный доступ к элементам vector<shared_ptr<Point>> примерно до 4 раз медленней чем у vector<Point>
С пунктами 2, 3, 4 согласен. У Stephan Lavavej кстати спросили почему он рекомендует использовать shared_ptr в своей лекции, если они такие тормозные. Он сказал, что они не "бесплатные", но того стоят.
EP>5. ref-counting у shared_ptr является thread-safe, со всеми вытекющими EP>6. я не увидел ответа на вопрос, что мешает предавать по ссылке
Ничего не мешает, но с учетом этого правила
79. Store only values and smart pointers in containers
и этих объяснений:
В некоторые функции кстати можно было бы передавать по ссылке. В тот же compare_vectors. Хотя я хотел consistency с остальным кодом, который был закручен на vector<shared_ptr<Point>> и на assumption что shared_ptr дешевле, чем Point копировать. Я еще, кстати, думал — а вдруг в Point потом добавят цвета или еще какую-нибудь хрень. Поэтому заложил shared_ptr везде.
Вообще с учетом тормознутости shared_ptr, о которой говорят люди и ты описал, твой ход мысли мне нравится. Надо замерить копирование Point а. Я это обязательно проверю.
G>>Хотел бы посмотреть на интерфейс без них, который соответствует книжке Code Standards (см ответ на Кодт.)
EP>Я показал реализацию без shared_ptr выше
Принимаю. Я просто не подумал сначала, что итераторами можно организовать аналог IEnumerable aka Pull. Это, похоже, и есть "IEnumerable" в плюсах.
EP>>>Пару месяцев назад я реализовывал Convex Hull for fun на основе известного алгоритма монотонных цепочек. Вся реализация это 39 строчек: EP>>>Feel the difference. G>>У меня сам конвекс тоже будет очень маленьким. Там еще много чего есть: чтение файла, обработка параметров командной строки, рисование отрезка по точкам (тоже велосипед кстати), плюс всякие патерные трюки для абстракций.
EP>В тех 27 строчках выше абстракций больше — они применимы к на порядки большему количеству входных типов(разные входные/выходные ranges, разные точки).
Принимаю. С учетом того, что итератор может выдавать по одной точке, то что ты говоришь правда.
Здравствуйте, Trrrrr, Вы писали:
T>Исправь свой кодстайл, сразуже бросилось в глаза:
T> double compare_vectors(SharedPoint p1, SharedPoint p2, SharedPoint p3); T>void ProcessPoint(SharedPointList list, SharedPoint p, SharedPoint next_p, bool is_top); T>Функции то с большой буквы, то маленькие, почем б не назвать CompareVectors?
принимаю. Должен быть CompareVectors.
T>Аналогично с дефайнами, в одном месте T>#ifndef line_h__ T>#define line_h__
T>в другом T>#ifndef CONVEX_HULL_H__ T>#define CONVEX_HULL_H__
принимаю. В одном месте ручками писал, в дргом томато использовал.
T>а вообще #pragma once рулит
Здравствуйте, greenpci, Вы писали:
G>>>Я не хотел передавать Point по значению, потому, что мой поинт был с двумя double — 16 байт. EP>>1. на x64 sizeof(shared_ptr<Point>)==16 G>ну ты x64 натянул для увеличения размера.
Все проекты над которыми работают, имеют x64 сборки, причём уже много лет, поэтому при расчётах памяти всегда равняюсь на 8-ми байтные указатели.
G>Если серьезно на 64 ожидал падение производительности.
За счёт чего именно?
EP>>2. реально памяти выделяется больше EP>>3. много дорогих аллокаций EP>>4. из-за индерекций последовательный доступ к элементам vector<shared_ptr<Point>> примерно до 4 раз медленней чем у vector<Point> G>С пунктами 2, 3, 4 согласен. У Stephan Lavavej кстати спросили почему он рекомендует использовать shared_ptr в своей лекции, если они такие тормозные. Он сказал, что они не "бесплатные", но того стоят.
Дело в том, что настоящая shared семантика встречается крайне редко.
Обычно объекты имеют детерминированные точки удаления, особо не зависящие от внешних условий — использовать shared_ptr в таких случаях, учитывая худшую производительность, является ошибкой.
С другой стороны, если трудно определить точку удаления (или они действительно могут быть разные) и нет каких-то жёстких требований по производительности к данному участку, то shared_ptr вполне приемлемый вариант (например даже если его thread-safe ref counting не нужно в этом случае).
Скажем так, shared_ptr это нормальный инструмент, но приоритет его использования самый низкий среди альтернатив:
1. value (with copy-only)
2. value + move
3. unique_ptr
4. boost::intrusive_ptr (или любой другой без thread-safe ref counting)
5. shared_ptr
EP>>5. ref-counting у shared_ptr является thread-safe, со всеми вытекющими EP>>6. я не увидел ответа на вопрос, что мешает предавать по ссылке
G>Ничего не мешает, но с учетом этого правила
79. Store only values and smart pointers in containers
и этих объяснений:
В первую очередь — values, потом unique_ptr-like, а потом уже shared_ptr-like.
G>В некоторые функции кстати можно было бы передавать по ссылке. В тот же compare_vectors. Хотя я хотел consistency с остальным кодом, который был закручен на vector<shared_ptr<Point>> и на assumption что shared_ptr дешевле, чем Point копировать. Я еще, кстати, думал — а вдруг в Point потом добавят цвета или еще какую-нибудь хрень. Поэтому заложил shared_ptr везде.
Какие именно копии смущают?
Если при передачи в функцию — то тут ссылки убирают все копии.
Если же смущают копии в результирующий массив, и по условиям index'ы/указатели приемлемы (то есть не нужен полностью независимый набор точек), то в первую очень нужно рассмотреть использование обычных non-owning указателей, если они не подходят, то только тогда переходить к shared семантике
G>Вообще с учетом тормознутости shared_ptr, о которой говорят люди и ты описал, твой ход мысли мне нравится. Надо замерить копирование Point а. Я это обязательно проверю.
Ещё обязательно замерь:
1. Создание, и проход по массиву vector<Point> vs vector<shared_ptr<Point>>.
2. Передача Point в функцию по значению vs по ссылке + сравни asm код
3. Передача shared_ptr<Point> в функцию по значению (у тебя же по значению он передаётся?) vs по ссылке vs просто Point
G>>>Хотел бы посмотреть на интерфейс без них, который соответствует книжке Code Standards (см ответ на Кодт.) EP>>Я показал реализацию без shared_ptr выше G>Принимаю. Я просто не подумал сначала, что итераторами можно организовать аналог IEnumerable aka Pull. Это, похоже, и есть "IEnumerable" в плюсах.
По тому что я вижу в доке, IEnumerable — это слабенький аналог паре ForwardIterator.
В STL есть 5 разных категорий итераторов, для каждой из которых есть много разных эффективных алгоритмов — спасибо Александру Степанову.
Здравствуйте, greenpci, Вы писали:
К>>"Выпуклая оболочка", дорогой Ватсон! G>да, правильно. Я представлял что выпуклый корпус (hull — the body or frame of a ship, most of which goes under the water (Cambridge Dictionary), но оболочка гораздо лучше.
Это не просто гораздо лучше, это соответствует общепринятой русской терминологии.
G>...
К>>Есть ряд моментов, которые стоит отшлифовать. К>>1. Странное разбиение на файлы: К>>- исключение в common.h, почему бы не в ch_exception.h? К>>- SharedPoint опять в common.h, тогда как PointVector и SharedPointVector — в point.h, а PointList и SharedPointList — в convex_hull.h
G>Принимаю. Вспоминаю, сам чувствовал вину, когда разместил в common.h вместо отдельного файла. В C# обычно правила жесткие — каждый клас, интерфейс, даже нестед в отдельный файл. В си плюс плюс также надо похоже.
Можно и в общую кучу, но со смыслом. Point и ChException не имеют ничего общего, используются в разных местах программы.
Либо всё в один заголовок положить — для такой маленькой программы это вполне реально.
G>Я использовал инкрементальный алгоритм, как ты говорил ниже. Мотивация моя была в следующем: меньше памяти (ведь точек может быть очень много) и не надо делать два прохода: сохранить точки в векторе, пройтись по ним для нахождения выпуклой оболочки. Я 1*N сэкономил этим, не считая расширения вектора при вставке (чтобы обойти расширение, мне бы пришлось объявлять вектор на все точки — увеличить потребление памяти в два раза).
G>Ты тоже упоминаешь инкрементальный алгоритм ниже, тольео у тебя "Push", а у меня "Pull". Сказались годы работы на шарпе с его IEnumerable. Ты считаешь этот интерфейс (см ниже) перебор с абстрагированием? Хм... Возможно. Я просто думал, как же си шарпный IEnumerable<Point> на плюсах сделать. Ничего лучшего чем это не придумал.
G>
В отличие от шарпа, в плюсах нет особой нужды делать ООП-интерфейс итератора. Вот это и есть излишнее абстрагирование. Можно ведь передать итератор (готового контейнера) как параметр шаблона — ничего руками не придётся писать.
При этом абстрагировании, кстати говоря, теряется
Инкрементный алгоритм может быть построен 3 способами:
— когда активным является источник данных, а вычислитель — подобие последовательного вывода
class AlgoServer
{
public:
void init();
void step(Point);
void done();
void somehow_utilize_results(.....);
};
void algo_client()
{
AlgoServer a;
a.init();
while(!end_of_file) { a.step(read_point()); }
// на деле, там может быть вот такоеfor(current_point.x = ....) for(current_point.y = ......)
{
if(color(current_point) == what_we_need)
a.step(current_point);
}
a.done();
a.somehow_utilize_results(.....);
}
— когда активным является вычислитель, а источник данных — подобие последовательного ввода
class AlgoClient
{
bool eof() const;
Point read();
};
void algo_server(AlgoClient& client)
{
my_init();
while(!client.eof()) my_step(client.read());
// здесь сложно представить, что цикл будет затейливым (в отличие от предыдущего)
// но может оказаться наоборот: что он банальный - например, просто прочитать точки во внутренний контейнер,
// а вся жирная логика переедет в my_done()
my_done();
somehow_utilize_results();
}
— наконец, когда источник и вычислитель пассивны, а отдельно существует инфраструктура, реализующая весь этот конвеер
Я, понятное дело, в этих эскизах никак не раскрываю деталей.
(Кстати, сейчас пришла в голову мысль, что на сопрограммах это было бы изящно. Но сопрограммы, особенно в С++, — излишество).
G>Прибитые гвоздями — это похоже про shared_ptr. Обосную ниже.
Это про shared_ptr<vector> и shared_ptr<list>.
К>>// для тех, кому нравится работать с произвольными контейнерами:
К>>template<class InIter, class OutIter>
К>>OutIter convex_hull(InIter begin, InIter end, OutIter dst)
К>>{
К>> std::vector<Point> points(begin, end);
К>> do_convex_hull(points);
К>> std::copy(points.begin(), points.end(), dst);
К>>}
G>Хороший интерфейс, а ля STL. Только слишком много N добавляет. Два копирования. Кстати в моей первой версии кода я тоже передавал vector. Потом поменял на "Pull-IEnumerable" при мыслях о производительности и памяти.
Этот алгоритм изначально не инкрементный, поскольку ниже говорилось: "отсортировать все точки по полярным координатам"...
Так что, если бы был выбран он, то ничего поделать нельзя.
Но можно же напрямую вызвать нешаблонную функцию do_convex_hull, тогда никаких лишних копирований.
К>>// нешаблонная функция — реализацию можно вынести в отдельный .cpp К>>// тип рабочего контейнера зафиксирован — vector<Point> К>>// (в будущем можно сделать произвольным — например, vector<void*> К>>// плюс передавать стратегию для работы с точками: функции К>>// get_x(void const*), get_y(void const*), remove(void*) К>>// ) К>>void do_convex_hull(std::vector<Point>& points) К>>{ К>> // отсортировать в полярных координатах К>> // удалить точки, образующие вогнутые углы К>>} К>>[/c] G>По поводу void* тут сразу два нарушения: G>C++ Coding Standards 101 Rules by Alexandresku and Sutter G>90. Avoid type switching; prefer polymorphism G>92. Avoid using reinterpret_cast
G>Я думаю все плюсники эту книжку должны любить пламенной любовью.
Расскажи это разработчикам boost::shared_ptr, boost::any и boost::function.
Никаких type switching, и даже никаких reinterpret_cast. Это эзотерическая техника неинтрузивных виртуальных функций. Она и в сях используется (qsort), и в плюсах (shared_ptr), а уж как она используется в хаскелле... но это уже дело хаскельного компилятора, пользователь о ней не думает.
G>Хотя с другой стороны в дот нете WPF постоянно Object грызет. Не type safe нифига.
Тут фокус в том, чтобы не отпускать объекты со стёртым типом в свободное плаванье.
Т.е. никаких vector<void*>, а только vector<void*> + IPointTypeTraits*
G>Это твой "Push" как я понимаю. То есть код, читающий точки, должен будет получить ConvexHullBuilder. Тебе не кажется, что это нарушение Rule Of Demeter (Least knowledge) и Separation of Concern. Получается, что тот код будет tightly coupled с конвекс халом. А что если я захочу просто точки получить для чего-нибудь другого, применяя тот же клас? Смысл моих абстракций вверху был "разлучить" код читающий точки и код делающий оболочку. Чтобы они были независимыми.
Для разлучения — нужна инфраструктура конвеера, см. выше.
К>>4. shared_ptr'ы нигде не нужны. Откройте для себя передачу аргументов по ссылке G>C++ Coding Standards 101 Rules by Alexandresku and Sutter G>79. Store only values and smart pointers in containers
G>Я не хотел передавать Point по значению, потому, что мой поинт был с двумя double — 16 байт. Копировать их при передаче значения было бы долго. Использовать референсы можно было бы, но у меня они уже должны были быть обернутыми в shared_ptr, из-за Александреску с его правилами (см. выше)
Александреску — очень плохой кумир для фанатизма.
Тем более, посчитай, сколько стоит копировать два дабла, и сколько — играть с подсчётом ссылок (он потокобезопасный, атомарный, и поэтому создаёт барьеры), а также — какие накладные расходы на блоки памяти.
G>Еще вот это G>virtual SharedPoint NextPointInternal() = 0; G>не понятно как сделать без SharedPoint.
Очень просто: virtual Point NextPointInternal().
И помолись о том, что компилятор умеет делать RVO (а компилятор умеет).
А если нет веры в RVO, то сделать out-параметр, как в старом злобром OLE/COM.
virtual bool NextPointInternal(Point& dst);
Кстати, если речь о том, чтобы возвращать значение или признак конца, так для этого есть много подходов:
— зарезервировать особое "сигнальное" значение, например, Point(NaN,NaN) или Inf,Inf
— возвращать обёртку — не обязательно shared_ptr, но можно boost::optional<Point> или std::pair<bool,Point>
— возвращать bool (или int) и out-параметр, как это делается во всех файловых функциях — int fread(void*dst,.....)
— вместо одного метода Read ввести два: Point Get()const и bool Next(), а то и три метода — void Next(), bool Eof()const — это стиль стльных итераторов
— кидать исключение, как это делает питон
G>Ты в своем void add(Point pt) вообще копируешь по значению. Я кстати оставил double на будущее. Вдруг захотят дробные на вход подять.
Ну и что в этом ужасного? Чай, не строка
Опять же, это эскиз. Можно переделать на Point const& pt.
К>>То есть, моё мнение: код нужно переделать вдребезги пополам G>блин, ты бы видел какой пишут код тут у нас. Поубивал бы.
G>По поводу книжки. Ищите в гугле: G>C++ Coding Standards: 101 Rules, Guidelines, and Best Practices G>By Herb Sutter, Andrei Alexandrescu
Дело не в том, чтобы следовать гайдлайнам кодирования. А в том, чтобы трезво отнестись к архитектуре программы, и затем закодировать её так, чтоб она была видна.
Но по архитектуре я не знаю, какую книгу предложить. Явно же не Банду Четырёх или Гради Буча?
Тем более, что и банда, и Буч — приверженцы ООП, а мне кажется, что здесь больше ФП и юникс-вей подойдёт.
(Но если в команде разработчиков практикуется ООП, то нужно помнить, что ФП — это ООП для бедных, и делать ООП, ибо есть такой принцип — "наименьшего удивления").
Здравствуйте, Кодт, Вы писали:
A>>С++03 не нужен, пишите на С++11
К>Тут бы я поспорил. К>Ubuntu 12.04 LTS по-прежнему с gcc 4.6, который тянет максимум std=gnu++0x (03 с некоторыми плюшками из 11).
Там что, нельзя дополнительно поставить свою версию?
Переубедить Вас, к сожалению, мне не удастся, поэтому сразу перейду к оскорблениям.
Здравствуйте, Кодт, Вы писали:
A>>С++03 не нужен, пишите на С++11
К>Тут бы я поспорил. К>Ubuntu 12.04 LTS по-прежнему с gcc 4.6, который тянет максимум std=gnu++0x (03 с некоторыми плюшками из 11).
всмысле "с gcc 4.6"?
этот ваш линукс настолько плох, что там нельзя поставить нормальный компилятор, или как?
в винде вообще по умолчанию компилятор не установлен, fyi