После статьи «ООП не мертво. Вы просто пользуетесь им как молотком по клавиатуре» комментарии кипели
Кто-то звал Smalltalk, кто-то бросал в нас Haskell, кто-то доставал из-под кровати подшивку статей «ECS лучше всего» — и всё это с праведной уверенностью.
Что ж…
Пора прекратить спор на словах. И начать спор в коде.
Code-Battle: MVP графического редактора
Задача: реализовать базовый графический редактор
Фигуры: точка, линия, круг, квадрат, прямоугольник, треугольник, ромб, овал
Функциональность: добавление фигур на канвас, отрисовка
Правила:
Один .cpp / .py / .exs / .c / .rs / .lisp / whatever файл
Язык и подход — любой
Главное: читаемость, понятность, архитектурная целостность
Пишите максимально просто, если надо - используйте псевдокод
MVP сейчас — фичи потом
Все решения оформляются одним Merge Request
В конце — разбор решений, сравнение подходов, и, как обычно: наказание невиновных и награждение непричастны
Для затравки: C++ / ООП реализация (v1)
В качестве точки отсчёта — наш базовый вариант.
ООП, без усложнений.
Всё в одном файле.
#include <iostream>
#include <string>
#include <vector>
class Shape {
public:
virtual void draw() const = 0;
virtual std::string name() const = 0;
virtual ~Shape() {}
};
class Point : public Shape {
int x, y;
public:
Point(int x, int y) : x(x), y(y) {}
void draw() const override {
std::cout << «Drawing Point at (« << x << «, « << y << «)\\n»;
}
std::string name() const override { return «Point»; }
};
class Circle : public Shape {
int x, y, r;
public:
Circle(int x, int y, int r) : x(x), y(y), r(r) {}
void draw() const override {
std::cout << «Drawing Circle at (« << x << «, « << y << «), r = « << r << «\\n»;
}
std::string name() const override { return «Circle»; }
};
class Rectangle : public Shape {
int x, y, w, h;
public:
Rectangle(int x, int y, int w, int h) : x(x), y(y), w(w), h(h) {}
void draw() const override {
std::cout << «Drawing Rectangle at (« << x << «, « << y
<< «), « << w << «x» << h << «\\n»;
}
std::string name() const override { return «Rectangle»; }
};
class Canvas {
std::vector<Shape*> shapes;
public:
void add(Shape* s) { shapes.push_back(s); }
void render() const {
for (auto s : shapes) s->draw();
}
~Canvas() {
for (auto s : shapes) delete s;
}
};
int main() {
Canvas canvas;
canvas.add(new Point(1, 1));
canvas.add(new Circle(5, 5, 3));
canvas.add(new Rectangle(0, 0, 6, 3));
canvas.render();
return 0;
}
Теперь — вы.
Реализуйте то же самое:
На любом языке и в любой парадигме
В своей манере — функциональной, процедурной, декларативной, минималистской
Без фреймворков. Без магии. Только архитектура
Это только начало.
Следующая итерация уже в пути. Требования изменятся. Канвас расширится. Архитектура проявит себя.
А мы, разберём каждую реализацию — и, возможно, найдём ответ на вопрос:
«To OOP or not to OOP — вот в чем загвоздка”.
Репозитарий с правилами, шаблоном и инструкциями:
Комментарии (121)
skovoroad
14.05.2025 12:29Это очень плохой пример, потому что для игрушечных задач ООП не нужен. Он нужен для сложных абстракций, для разделения когнитивной нагрузки между группами разработчиков. У вас никогда не будет отдельной группы разработчиков для кружочков, а отдельной - для треугольничков.
Давайте попробуем вернуться в реальность и представим, что эти квадратики - что-то более-менее сложное. Очевидно, функции draw() должны обращаться к каким-то связанным с рисованием зависимостям (как минимум, тянуть заголовочные файлы). Но сами прямоугольники могут использоваться в контекстах, которые не подразумевают никакой связи с рисованием. Например, я не знаю, библиотека, которая вычисляет пересечения прямоугольников. На кой ляд ей draw() и все её зависимости?
А значит, фигура у нас разбивается на две части: Rectangle и RectangleDrawer (если предположить, что у рисовальщика есть состояние, например, кэш, и просто функции недостаточно, то понадобится класс), а RectangleDrawer станет частью своей иерархии. Они будут связываться, к примеру, как визиторы, и тут...
И тут в помещение вбегают критики ООП и говорят, что вот наворотили дикий оверинжиниринг! И они правы, потому что для этой примера достаточно POD-структур, для их хранения - несколько векторов, а для рисования - перегруженных (и даже это необязательно) свободных функций. Плохой пример, негодный. Надо придумать пример реального размера, в котором ООП действительно нужно.
Kahelman Автор
14.05.2025 12:29Я с вами не согласен. ООП это подход к разработке. Демонстрация подходов и их применимости делается на простых примерах. Графический редактор это простейший приме, позволяющий выявить плюсы и минусы подходов. В частности, наследование квадрата от прямоугольника -классическая ошибка в ООП дизайне. Здесь вы можете доходчиво объяснить, почему так не следует делать.
Ваша критика похожа на заявление что Ньютоновская механика не подходит, поскольку не работает в масштабах Вселенной.
skovoroad
14.05.2025 12:29Ваш пример не убеждает, а разубеждает пользоваться ООП.
Он не подчёркивает его сильные стороны (разделение кода на изолированные куски с хорошо описанной изолированной ответственностью)
Но зато он отлично иллюстрирует аргументы критиков ООП: вы усложнили примитивную задачу.
(ну и плюс задача решена некорректно, рисование не является частью прямоугольника, как я уже выше и написал)
Kahelman Автор
14.05.2025 12:29Это баттл- я не предлагаю реализацию. Я предлагаю сравнить подходы. Ссылка на репозитарий в конце. Сделайте вашу идеальную реализацию и пришлите MR. Потом можем осудить…
skovoroad
14.05.2025 12:29Да тут даже мр не нужен, вот вам код, выполняющий совершенно ту же задачу, что и ваш пример.
По сравнению с вашим у него масса достоинств, начиная с того, что он компилируется.
struct Rectangle { int center[2] = {0, 0}; unsigned size[2] = {0, 0}; }; struct Circle { int center[2] = {0, 0}; int radius = 0; }; void draw(const Rectangle& r) { // draw rectangle } void draw(const Circle& c) { // draw circle } int main() { Rectangle r{{1, 2}, {10, 20}}; Circle c{{3, 4}, 5}; draw(r); draw(c); }
Kahelman Автор
14.05.2025 12:29Не хватает: треугольника, квадрата, овала, ромба. :)
Chamie
14.05.2025 12:29У вас тоже
К слову, знать бы ещё, что вы под ними подразумеваете, потому что в моей геометрии для рисования прямоугольника на плоскости нужно как минимум 5 чисел (крутит в руке телефон), а «овалов» там вообще гора разных.
Kahelman Автор
14.05.2025 12:29И не плохо бы было их в коллекцию запихнуть чтобы в main все не прописывать.
skovoroad
14.05.2025 12:29зачем в коллекцию? В вашем примере коллекция никак не используется
но если есть какая-то ломовая необходимость в коллекции конкретно в этом микроскопическом примере, у вас есть стандартный std::vector<std::variant>
Понимаете, вы сейчас начнёте накручивать требования, которых нет в примере. Моё замечание было не про ООП (это мощный и полезный подход во множестве случаев), а про ваш пример. Он не иллюстрирует сильные стороны ООП. Поэтому идея соревнования в решении этой задачи в разных парадигмах не имеет смысла.
Kahelman Автор
14.05.2025 12:29Тогда возьмете на себя задачу реализовать на чистом процедурном подходе? std:vectorstd:variant это хак по меньшей мере. Стандартные процедурные языки не используют понятие темплейтов. Поправьте если я не прав. Соответственно в рамках чисто процедурного подхода эта задача просто не решается. Вам надо либо с void указателями работать, либо «изобретать» свой rtti. Что имеет место быть, но как бы добавляет аргументов в сторону сторонников ООП подхода. Со своей стороны могу на AWK реализовать :)
eao197
14.05.2025 12:29Стандартные процедурные языки не используют понятие темплейтов.
Ada-83 как раз таким языком и была.
skovoroad
14.05.2025 12:29Да господи, стандартная библиотека в 2025-м году от РХ у него хак. Как будто кто-то запрещает писать на плюсах в процедурном стиле. Ну возьмите раст, там enum из коробки - тот же вариант. Код ещё в три раза короче станет, кстати.
Вы по-прежнему не туда воюете, я уже сороковой комментарий подряд пытаюсь вам объяснить, что проблема не в ООП, а в вашей статье, в которой выбран неудачный подход демонстрации достоинств ООП. Ладно, это бесполезно.
Comdiv
14.05.2025 12:29что проблема не в ООП, а в вашей статье
Замените слово «статья» на «код» и окажется, что вы описали причину, почему ООП как общая парадигма для написания кода несостоятельна.
skovoroad
14.05.2025 12:29Зачем я буду заменять слово "статья" на слово "код", если я имею а виду не код, а всю статью? Т.е. контекст использования этого кода.
Код, конечно, тоже плохой, но речь не об этом.
Comdiv
14.05.2025 12:29Затем, что статья в данном случае олицетворяет код на ООП, который опять не поняли и неправильно применили. Речь ведь не о коде из статьи.
Kahelman Автор
14.05.2025 12:29Замените слово shape на Message, а Canvas на messageQueue. Получите задачу обработки входящих сообщений. Не ожидал что абстрактное мышление это отдельная фича :)
skovoroad
14.05.2025 12:29Слушайте, если у вас всё надо заменить, чтобы вас правильно понять, может, вы уже признаете, что пример плохой? Задача организации очереди сообщений это другая задача и действительно служила бы лучше целям статьи.
nin-jin
14.05.2025 12:29Вот пример очереди на ООП. Но новое слово тут не в том, что ООП, а в том, что wait-free.
nin-jin
14.05.2025 12:29И правда, при чём тут ООП, если всё сделано на объектах.. Наверно это ФП такой.
Тест с 1000 потоками уже давно есть. А нежданчик не случится благодаря барьерам памяти.
cupraer
14.05.2025 12:29Вы считываете значение и обновляете его за двумя разными барьерами.
А ООП ни при чём, потому что не используется. Замените объекты на структуры — и ничего не изменится.
nin-jin
14.05.2025 12:29Вы, похоже, не понимаете как работают барьеры и для чего вообще нужны.
В ООП языках структуры от объектов мало чем отличаются в принципе. Есть лишь небольшие различия в дефолтном поведении.
skovoroad
14.05.2025 12:29Это новое слово в разработке.
Как пример для статьи про ООП на хабре? Почему, собственно, нет? Речь не идёт про промышленную универсальную реализацию, вы даже не знаете, какие требования будут к такой очереди, но уже лезете в бутылку. Вполне можно навертеть ООП-абстракций для примера, начиная от иерархии самих типов сообщений и заканчивая какими-нибудь, я не знаю, подписчиками. Это иллюстративный материал для статьи, он не обязан летать на утюгах.
Ах да, я вспомнил, вы тот самый любитель общественного внимания, который обращается к собеседнику "тупой дегенерат", когда его самого макают в, назовём это так, чрезмерную широту обобщений на фоне ограниченнго кругозора.
Тогда вопросов не имею.
cupraer
14.05.2025 12:29Графический редактор это простейший пример, позволяющий выявить плюсы и минусы подходов.
Смешно. На КОБОЛе написаны тонны кода, который работает десятилетиями, и у него — прикиньте — нет графических/десктопных примитивов.
При это КОБОЛ на момент создания был гигантским прыжком вперед, почти революцией. А вот десктопов не было, упс.
Кроме того, никому в здравом уме не придёт в голову писать графический редактор без использования библиотек в 2025 году. А еще на джаваскрипте это будет короче, понятнее, и быстрее. Упс².
Kahelman Автор
14.05.2025 12:29О написании графического редактора без библиотек и вообще о написании графического редактора речи не идёт. Речь о классическом примере реализации приложения на на ООП. Можно было бы последовать примеру товарища Буча и взять его вариант с Гидропонной схемой. Но тут у народ в 5 примитивах разбираться не может и ТЗ из трех строчек до конца прочитать. Вы кстати на Коболе писать собираетесь? Причём тут что на нем тонны кода написаны? На bash написано больше чем не COBOL, и продолжают писать и что?
cupraer
14.05.2025 12:29Я не собираюсь писать вообще, я свободное время трачу на OSS, а не на взнуздание сферических коней в вакууме. Но если бы собирался, написал бы на насквозь функциональном эликсире.
Реализация полиморфизма, который здесь нужен, в ООП — самая кривая.
Kahelman Автор
14.05.2025 12:29Давайте, я думал на erlang сам написать, но если в на elixir напишите -будет классно. Ссылка на репо внизу статьи. Ждём реализации
Dhwtj
14.05.2025 12:29для игрушечных задач ООП не нужен. Он нужен для сложных абстракций, для разделения когнитивной нагрузки между группами разработчиков
И для этого он тоже не нужен.
Снизить межкомандную когнитивную нагрузку можно, сведя общий API к компактному и редко меняющемуся ядру, а частные вариации вынеся в простые адаптеры.
Pardych
14.05.2025 12:29А мы пишем класс в ООП потому что ожидаем что его будет отдельная команда разрабатывать? Эвона чо.
skovoroad
14.05.2025 12:29Мы пишем класс (и прочими способами упрощаем поддержку кода), потому что писатель кода и читатель могут различаться, да (это может быть и один человек, но в разное время). Это действительно причина
Код, в котором никому никогда не надо будет разбираться и поддерживать, всё равно, в какой парадигме писать
Эвона чо
onets
14.05.2025 12:29Хорошо, что выбрали графический редактор - на нем можно показать плюсы ооп, вместо бизнес приложения, где центром являются данные и их согласованность
Kahelman Автор
14.05.2025 12:29За данные и их согласованность должна отвечать БД. Правило это не модно и молодёжно. Правда никто пока не показал как обеспечить гарантированную согласованность данных в распределенной системе. :)
askv
14.05.2025 12:29Потому что таких гарантий не существует? )
Kahelman Автор
14.05.2025 12:29Потому что задача «византийских генералов» не решаема :(
https://ru.m.wikipedia.org/wiki/Задача_византийских_генералов
cupraer
14.05.2025 12:29Да закопайте вы уже эту детскую страшилку. Прекрасно всё решаемо. CAP-теорема даже доказывает, что если выбросить A — то всё будет супер-консистентно.
Не нужно использовать термины, про которые вы краем уха слышали от Рабиновича.
ermouth
14.05.2025 12:29Редактор – это где редактировать можно. В примере какая-то надстройка для усложнения использования канваса, простите.
Kahelman Автор
14.05.2025 12:29Ага вы готовы за 20-30 минут сделать MVP редактора в вашем понимании?
ermouth
14.05.2025 12:29Зачем? По вашим условиям на любом языке получится примерно одно и то же. По вашим условиям по сути нужно через вызов add с кортежем {тегФигуры, ...параметры} положить его куда-то в стейт приложения, а потом по команде их все нарисовать.
В вашем примере ООП подход ничего вообще не добавляет, ну кроме километра ритуальной по сути писанины.
Kahelman Автор
14.05.2025 12:29Согласен. Идея в том что дальше идут «требования на изменения» и «дополнительные фичи» идея посмотреть как будут развиваться процедурный/функциональный/ООП подходы
eao197
14.05.2025 12:29Как же печально видеть в 2025-ом году C++ный код с ручными new/delete и детскими ошибками, описанными еще в первых изданиях "Язык программирования C++" :(((
Kahelman Автор
14.05.2025 12:29Это минимальная реализация - максимально близко к «классикам». Жду вашего MR с «правильной реализацией». Поскольку тут Баттл- одними комментариями не отделаетесь. Нет MR - слив засчитан :)
eao197
14.05.2025 12:29Я хз что такое MR, но на слабо вы решили взять не того. В последние годы часть моей работы состоит как раз в том, чтобы бить по рукам за говнокод с ручными new/delete, возвратом std::string-ов по значению и т.п.
Если бы вы свой Canvas определили хотя бы так:
using ShapeUptr = std::unique_ptr<Shape>; class Canvas { std::vector<ShapeUptr> shapes; public: void add(ShapeUptr s) { shapes.push_back(std::move(s)); } void render() const { for (auto & s : shapes) s->draw(); } };
То у вас бы получилось и короче, и надежнее.
А классики уже давным-давно рекомендуют бежать от голых владеющих указателей как от огня.
Для тех, кто все еще кипятит, вот рекомендации лучших собаководов для изучения:https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#r3-a-raw-pointer-a-t-is-non-owning
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#r11-avoid-calling-new-and-delete-explicitly
https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#r20-use-unique_ptr-or-shared_ptr-to-represent-ownershipKahelman Автор
14.05.2025 12:29Первоначальный вариант был с unique_ptr. Потом выбросил, т.к. вопрос про архитектуру ООП vs XXX а не про современные подходы и best practice в C++.
eao197
14.05.2025 12:29Демонстрация "как в этом вашем C++ отстрелить себе ногу не прилагая никаких усилий" получилась наотлично.
Cfyz
14.05.2025 12:29using
ShapeUptr = std::unique_ptr<Shape>;
Использование алиасов почём зря (например в подобных тривиальных случаях) ухудшает читаемость кода. Не надо так.
shapes.push_back(std::move(s))
Скорее наверное emplace_back()?
Я хз что такое MR
Это довольно иронично, что вы в 2025 не знаете что такое MR =). Подсказка: это как PR, только gitlab.
eao197
14.05.2025 12:29Не надо так.
Спасибо, но вредные советы (а это очень вредный совет) идут в сад. Желающие выписывать раз за разом
std::unique_ptr<что-то-там>
следуют туда же.Скорее наверное emplace_back()?
Когда у нас на входе готовый unique_ptr, то разницы быть не должно.
Cfyz
14.05.2025 12:29Спасибо, но вредные советы (а это очень вредный совет) идут в сад.
Ну так вы первый начали.
Любой алиас это сокрытие конкретного типа и небольшая, но дополнительная когнитивная нагрузка на удержание в памяти что это такое на самом деле и/или постоянные сверки то ли это, что ты думаешь.
Даже дюжина повторений в коде не стоит этого. Вы несколько раз экономите невероятные 10 символов, из которых половину все равно подставит IDE, а читаете это снова и снова и снова каждый раз, когда приходится вернуться к этому фрагменту кода.
Одно дело, если тип фундаментален для проекта и используется в нем повсеместно. Но в единичных и/или простых случаях использование всяких FooPtr, FooMap и прочих одному автору понятных типов только лишь потому что видите ли лень пару лишних клавиш нажать -- это вредная практика.
eao197
14.05.2025 12:29Любой алиас это сокрытие конкретного типа
Нет. Вы сразу ошибаетесь. А из ошибочных предпосылок происходят и ошибочные выводы.
Все, что вы написали -- это ерунда. Особенно про "видите ли лень пару лишних клавиш нажать -- это вредная практика".
Более того, когда в проекте есть устоявшиеся правила именования указателей (вроде ShapeUptr, ShapeShptr), то уже из названия типа видно, с каким указателем мы имеем дело и это отнимает гораздо меньше усилий, чем читать бесконечные std::unique_ptr. Особенно в сочетаниях типа
std::shared_ptr<std::vector<std::unique_ptr<Shape>>>
.Ну и главное, хорошие привычки нужно вырабатывать сразу, на простых ученических задачках.
ЗЫ. Минус на вашем комментарии не от меня. Я вообще на Хабре минусы не расставляю (за очень-очень-очень редким исключением).
Cfyz
14.05.2025 12:29Нет. Вы сразу ошибаетесь. Все, что вы написали -- это ерунда.
Вы меня конечно извините, но это аргументация уровня "нет ты".
когда в проекте есть устоявшиеся правила именования указателей (вроде ShapeUptr, ShapeShptr), то уже из названия типа видно
Это можно отнести к упомянутому мной частному случаю, когда типы настолько общеупотребимы в проекте, что без их знания все равно никуда. Кроме того, если в проекте исторически сложилось использование таких сокращений, то бессмысленно спорить. Даже если используется какая-нибудь венгерская нотация, все равно придется писать как уже написано.
Закавыка в том, что все равно у каждого второго проекта будут свои собственные устоявшиеся правила. Вам может показаться что ShapreUptr это что-то совершенно очевидное, но это не так.
Особенно в сочетаниях типа
std::shared_ptr<std::vector<std::unique_ptr<Shape>>>
По-моему это больше похоже на контр-пример.
Пользоваться такими типами очень тяжело и легко приводит к ошибкам, с алиасом или без. Если в коде постоянно нужно использовать и передавать что-то подобное, то вместо тяп-ляп спрятать имя под алиасом, по-хорошему надо задуматься о выделении соответствующей сущности в отдельный тип.
Потому что постоянное жонглирование вложенностью-разыменованием и безымянными полями абстрактных контейнеров это форменная жесть с какой стороны ни посмотри.
Имя, которое невыносимо печатать из-за его длины -- это чаще всего признак проблемы, которую надо решать не алиасом.
Ну и главное, хорошие привычки нужно вырабатывать сразу, на простых ученических задачках.
Как например использование emplace_back() вместо push_back() даже если в данном конкретном случае компилятор оптимизирует лишний move в ноль? =)
Но суть моей придирки к алиасам в том, что вы фактически производите небольшую обфускацию и называете это хорошей привычкой.
eao197
14.05.2025 12:29Вы меня конечно извините, но это аргументация уровня "нет ты".
Так если вы наговорили ерунды, то единственное, что можно сказать -- это назвать ерунду ерундой.
Я насмотрелся на программистов, которые не используют using-и. И еще больше насмотрелся на результаты их работы. Так, что больше не хочется. После этого любой персонаж, который мне начинает рассказывать про то, что "любой алиас -- это сокрытие типа", просто расписывается в своей профнепригодности. Пардон май френч.
Как например использование emplace_back() вместо push_back()
Для emplace_back есть очевидный сценарий применения -- это когда у нас на руках есть набор аргументов для конструирования нового объекта в векторе. Типа такого:
std::vector<std::string> lines; lines.emplace_back(45uz, '\t');
Для добавления же в конец вектора готового объекта предназначен push_back.
Все просто и очевидно. Использовать emplace_back как замену push_back -- ну такое себе, "сомнительно, но окай" (с)
Cfyz
14.05.2025 12:29Я насмотрелся на программистов, которые не используют using-и.
Ну а я насмотрелся на тех, которые пихают typedef и using где надо и не надо.
Явное всегда лучше неявного.
Если у вас программисты пишут плохой код потому что они не используют using почем зря -- дело совершенно точно не в using.
Для emplace_back есть очевидный сценарий применения <...> Для добавления же в конец вектора готового объекта предназначен push_back.
Вот тут вынужден согласиться, в данном случае почем зря emplace приплел.
eao197
14.05.2025 12:29Ну а я насмотрелся на тех, которые пихают typedef и using где надо и не надо.
Примеры можно? Что-то мне сложно представить, как using-ами можно код испортить.
Может из OpenSource что-нибудь?
Если у вас программисты пишут плохой код потому что они не используют using почем зря -- дело совершенно точно не в using.
В том-то и дело, что не у меня. Почитаешь профильные ресурсы, все профи просто высшего разряда. Как придешь какой-нибудь проект консультировать, так просто в шоке -- где все те монстры от программирования, которые себя пяткой в грудь в комментариях бьют. И код корявый получается не потому, что тупые неумехи его пишут, а потому, что просто вовремя не научили, как можно себе жизнь облегчить, а код -- упростить.
Cfyz
14.05.2025 12:29Примеры можно? Что-то мне сложно представить, как using-ами можно код испортить.
А примеры как испортили код отсутствием using там, где в этом нет явной на то необходимости? Сотый раз говорю, у алиасов конечно же есть применение. Иногда допустипо или даже надо их применять. Просто не надо пихать их там, где они лишь обфусцируют написанное.
Вообще вся дискуссия выглядит довольно сюрно:
std::unique_ptr<Shape> -- это ужас, прямо-таки признак профнепригодности. А вот ShapeUniquePtr -- это совсем другое дело!
Остается переименовать int в Signed, std::string в String и т. п. и вот тогда все станет просто и понятно, ведь
именованные типы (даже в виде алиасов) служат как дополнительный слой абстракции и скрывают лишние детали
Можно еще using namespace std полирнуть, потому что
уже из названия типа видно, с каким
указателемтипом мы имеем дело и это отнимает гораздо меньше усилий, чем читать бесконечные std::Да-да, reductio ad absurdum, но я уже не знаю как еще реагировать.
и главное, если со временем потребуется заменить тип за ShapeUptr на какой-то другой, то имя ShapeUptr все равно остается на месте
Или вот это. Давайте просто втихаря подменим реализацию.
Это кстати прекрасная иллюстрация одной из причин большей когнитивной нагрузки от излишнего применения алиасов: всегда надо быть начеку. Даже если ты читал этот код вчера и запомнил что есть что, сегодня кто-нибудь мог все поменять.
std::shared_ptr<std::vector<std::unique_ptr<Shape>>>
Или вот это. Давайте просто спрячем эту жесть под коврик и сделаем вид что так и надо.
А что пользоваться этим невозможно, так кому какое дело.
Что любопытно, действительно можно вообразить такие ситуации, когда ваши аргументы будут оправданы. Стандартная библиотека вон полна алиасов, например. Но какое это имеет отношение к 95% тривиальнейших случаев по типу std::unique_ptr<Shape> в примере выше?
Что, конечно же, вас ничуточку не переубедит. Поэтому здесь мои полномочия видимо все, окончены =(.
eao197
14.05.2025 12:29А примеры как испортили код отсутствием using там, где в этом нет явной на то необходимости?
Легко. Во-первых, уже был пример с
std::shared_ptr<std::vector<std::unique_ptr<Shape>>>
. Вместо которого был быShapeContainerShptr
.Во-вторых, давайте исходный пример чуть расширим и добавим в Canvas методы extract, insert и replace. Без алиаса получим (для простоты не расставлял
[[nodiscard]]
):class Canvas { std::vector<std::unique_ptr<Shape>> _shapes; ... public: void add(std::unique_ptr<Shape> s); std::unique_ptr<Shape> extract(std::size_t index); std::unique_ptr<Shape> replace(std::size_t index, std::unique_ptr<Shape> s); void insert(std::size_t index, std::unique_ptr<Shape> s); ... };
и тоже самое с алисом:
class Canvas { std::vector<ShapeUptr> _shapes; ... public: void add(ShapeUptr s); ShapeUptr extract(std::size_t index); ShapeUptr replace(std::size_t index, ShapeUptr s); void insert(std::size_t index, ShapeUptr s); ... };
Любой желающий может сам оценить какой из вариантов больше замусоривает смысл более низкоуровневыми деталями реализации. И какой более приспособлен к будущим изменениям. Например, если нам потребуется сменить обычный unique_ptr на unique_ptr с кастомным делетером.
Теперь ваши примеры ситуаций, когда алиасы мешают.
Остается переименовать int в Signed
Вам смешно, а я вот сейчас работаю с проектом, в котором для индексации задействовали int-ы, а не какой-либо из вариантов strong typedef. Поменять это задешево уже не получается и приходится разгребаться с предупреждениями о неявных конвертациях size_t в int, а иногда и double в int (промежуточно через size_t). Был бы изначально введен некий ItemIndex, пусть даже в виде простого using-а, сейчас стало бы гораздо проще.
А что пользоваться этим невозможно, так кому какое дело.
Интересно почему этим невозможно пользоваться?
Что, конечно же, вас ничуточку не переубедит
Есть немаленькая вероятность, что я программирую дольше, чем вы живете на свете. И есть еще большая вероятность, что говнокода пришлось разгрести тоже побольше. Так что да, не убеждают. А вот ощущение, что вы сперва сказали ерунду, а потом ее старательно защищаете, только усиливается.
Кстати говоря, профнепригодность относилась к совету не использовать алиасы в простых случаях. А не то, что вы написали выше.
skovoroad
14.05.2025 12:29Казалось бы, наоборот? Если я знаю, что FooPtr это некий уместный в данном контексте тип указателя, то я не думаю о нижележащем типе и когнитивная нагрузка падает?
Мне неважно, что он юник или шейред, мне важно, что он указывает на Foo и используется в заданной сигнатуре. Остаётся только нужная информация. Нагрузка падает.
Cfyz
14.05.2025 12:29Мне неважно, что он юник или шейред
Нюанс в том, что когда действительно неважно что это за тип, то как правило это будет typename T или auto =).
А вот что автор скрыл под Ptr -- unique, shared, QPointer, указатель из boost или вообще голый указатель -- это обычно существенно влияет на семантику владения, совместимость между типами и как с этим можно обращаться вообще помимо -> и *.
Да, внутри функции вам действительно часто все равно, что это за указатель. Ну так в названии переменной или поля тип и не указывается, shape и shape.
skovoroad
14.05.2025 12:29Семантика владения и прочие детали важны при написании кода (и то в большинстве случаев компилятор не даст ошибиться).
Но код читается кратно больше, чем пишется. И когнитивная нагрузка - это про чтение, а не про написание. Здесь алиасы работают, как хорошее именование переменных и даже как конструирование типов - упрощает код.
Cfyz
14.05.2025 12:29Но код читается кратно больше, чем пишется. И когнитивная нагрузка - это про чтение, а не про написание.
Именно. И поэтому аргумент мол не хочется выписывать раз за разом полное имя -- плохой.
Здесь алиасы работают, как хорошее именование переменных
Именно. И поэтому сокращение имен или использование нестандартных наименований без существенной на то причины -- усложняет чтение.
Семантика владения и прочие детали важны при написании кода
И при попытке понять что происходит в данном фрагменте кода. Особенно когда неочевидно это оно специально так написано или случайно получилось и возможно тут ошибка.
Но опять-таки, не все алиасы плохие. Дискуссия началась с использования using ShapeUptr = std::unique_ptr<Shape>. Это, положа руку на ногу, просто бесполезная перестановка символов местами, которая выигрывает несколько символов ценой замены явного, всем и каждому понятного типа на локальную историю, которую надо будет найти и запомнить (и сразу же забыть, потому что в каждом случае она разная). Зачем? Просто потому что.
eao197
14.05.2025 12:29Это, положа руку на ногу, просто бесполезная перестановка символов местами, которая выигрывает несколько символов
Блин, еще раз повторяю: экономия символов здесь не при чем от слова совсем.
Зачем?
Затем, что именованные типы (даже в виде алиасов) служат как дополнительный слой абстракции и скрывают лишние детали. Когда вам потребуется узнать что за ShapeUptr -- вы посмотрите. А пока это не потребовалось, то лучше использовать одно имя вместо std::unique_ptr.
Просто потому что.
Если вы чего-то не понимаете, то это не значит, что в этом нет смысла. Аргументов вам здесь уже привели в ассортименте. Причем разные люди.
Dooez
14.05.2025 12:29auto
также скрывает тип, но повсеместен в современном C++. Хотя может вы из секты людей, которые всегда явно пишет тип переменных?Хороший алиас позволяет в первую очередь меньше читать. Если он ограничен областью видимости, то когнитивная нагрузка вполне вероятно будет меньше чем при его отсутствии.
Алиасы значительно упрощают рефакторинг и позволяют избежать багов. Обобщенный код без алиасов я даже представлять не хочу.
Если говорить про границы API то конечно лучше иметь сигнатуры со стандартными именами. Но в реализациях функций и классов не вижу проблемы при грамотно выбранных именах.
Cfyz
14.05.2025 12:29auto
также скрывает тип, но повсеместен в современном C++. Хотя может вы из секты людей, которые всегда явно пишет тип переменных?Фундаментальное отличие auto в том, что он скрывает тип в очень небольшом контексте. Грубо говоря вот видим объявление auto i = vec.begin(), и вот в пределах экрана эта переменная используется.
То же самое с локальным using в пределах области видимости.
Проблема с теми алиасами, которые определены где-то в совершенно другом месте и надо вспоминать/искать что это такое. IDE еще могут быстро подсказать, но при чтении/ревью например в web-интерфейсе это очень мешает.
Заметьте, что я не агитирую за полный отказ от using вообще. Только там, где без этого можно легко обойтись.
Алиасы значительно упрощают рефакторинг и позволяют избежать багов. Обобщенный код без алиасов я даже представлять не хочу.
Обобщенный алиас алиасу рознь. Одно дело using ElementType = T; в контейнере. И совсем другое это сокрытие семантики, как это часто происходит с указателями или многоэтажными контейнерами. Бездумное использование алиасов запросто может наоборот стать причиной неочевидной ошибки.
Но в реализациях функций и классов не вижу проблемы при грамотно выбранных именах.
Вот казалось бы, std::unique_ptr<Shape> -- куда уж лучше. Указатель, уникальный, из стандартной библиотеки. Но нет, это слишком просто.
simplepersonru
14.05.2025 12:29Сделали в проекте такие глобальные юзинги (в условном common/core.h) :
template <class T> using U = std::unique_ptr<T>;
И также SH с шаредптр
Читаемость не теряется и не нужно руками делать на каждый такой класс отдельный юзинг.
Плюс такого подхода, что мы всегда видим оригинальный класс и например поиск по символу нужно делать не для каждого такого юзинга, а только для оригинального класса
eao197
14.05.2025 12:29Минусы такого подхода:
однобуквенные, но значимые идентификаторы. Запоминать чем отличается
U<T>
отSH<T>
отI<T>
отP<T>
и прочих одно-двух-трех-буквенных индентификаторов такое себе удовольствие. Еще хуже смотреть на код с такими вещами, особенно когда взгляд замыливается от усталости;угловые скобки, которые никуда не деваются и о которые ты все равно спотыкаешься. Может показаться, что
SH<std::vector<U<Shape>>>
-- это сильно лучше, чемstd::shared_ptr<std::vector<std::unique_ptr<Shape>>>
, но это, как по мне, тот же самый фрагмент автопортрета Фаберже, только в профиль;ну и главное, если со временем потребуется заменить тип за ShapeUptr на какой-то другой (вместо простого std::unique_ptr на какой-то хитрый собственный тип указателя), то имя ShapeUptr все равно остается на месте.
simplepersonru
14.05.2025 12:29Однобуквенность. Вопрос привычки, такая практика. Их всего 2 таких алиаса, как самые часто-используемые, понятно что если бы их было 10, это уже совсем другой разговор, но эти даже интуитивно понятные
В примере с ShapeUptr угловых скобок было бы на одну пару меньше, они не то чтобы куда-то испарятся все. Но вместе с тем появился и дополнительный символ, его нужно написать. Не сделать forward decl с оригинальным классом, чтобы использовать этот юзинг (т.к. он как правило рядом с определением класса), а в моем примере можно в U<MyClass> пихать такую декларацию при определённых условиях. Ну и то что говорил про поиск по символу
Про замену типа указателя. Для меня выглядит как исключительное событие и думаю писал полный тип хитрого указателя по месту, чтобы не вводить в заблуждение пользователя этого символа на предмет что под капотом. Вообще пример странный, не могу представить себе такое.
Приведу шутошную аналогию, это не аргумент и не всерьез:
Давайте в проекте определим и будем использовать
using Int = int;
Мало ли нам понадобится массово подменить целые числа, а имя Int все равно останется на месте
eao197
14.05.2025 12:29Не сделать forward decl с оригинальным классом, чтобы использовать этот юзинг (т.к. он как правило рядом с определением класса)
Бесконтрольные forward decl, к сожалению, прямой путь к хоть и мелкому, но геморрою. Такие определения следует держать в одном месте, а тогда и using-и не проблема от слова совсем.
Для меня выглядит как исключительное событие
Редкое, но не то, чтобы исключительное. Например, для unique_ptr -- был простой, стал unique_ptr с кастомным deleter-ом. Для shared_ptr -- был обычный std::shared_ptr, поменяли на кастомный без поддержки weak-references и с простым счетчиком ссылок место атомарного (по типу Rc в Rust, который отличается от Arc). Или же был обычный std::shared_ptr, а стал каким-нибудь boost::intrusive_ptr.
using Int = int;
А давайте сделаем вашу попытку пошутить более серьезной и возьмем такой пример:
using AxisX = int; using AxisY = int; using Width = int; using Height = int; using Radius = int; ... ShapeUptr makeRectangle(AxisX x, AxisY y, Width cx, Height cx); ShapeUptr makeCircle(AxisX x, AxisY y, Radius r);
Очевиднее ли это будет, чем обычные int-ы?
Для быстрого прототипирования сойдет. А потом можно будет в using-ах int-ы заменить на какой-то из вариантов strong typedef и компилятор еще и сам по рукам разработчикам бить начнет, когда они радиус с шириной начнут путать по недосмотру.Jijiki
14.05.2025 12:29не ну геморой, но у меня удалось добавить дабл кватернион с решением зависимости(он содержит 2 кватерниона и его методы тянут парочку функций) через ввод парочки в функций(так просто проще по итогу и я их переименовал просто дописал 1 в конец, более красивого решение чтобы замкнуть типы в неймспейсе и решить зависимость я не нашел покачто) прям в неймспейс, другие моменты с именованием еще геморнее я вчера смотрел(мне было проще продублировать внутрь парочку функций зато сохранил стиль либы так скажем)
у меня тоже именование, но оно закрывает неймспейс в неймспейсе только типы закрыты, получилось прикольно
namespace NameLib{ template<typename T> class V{ }; template<typename T> class V1{ }; } using v=NameLib::V<float>; template<typename T> T fucntion(T l,T r){ if constexpr(std::is_samev<T,v>){ return l+r; } }
cupraer
14.05.2025 12:29Не знал, что неиспользование гитлаба — смертный грех, караемый остракизмом.
Я, например, подавляющее большинство написанного кода открываю в OSS, у нас это неповоротливое говнище не в чести.
Cfyz
14.05.2025 12:29Автор исходного комментария посетовал (справедливо) мол как можно так писать на C++ в 2025. А еще он сказал что не знает, что такое MR.
Я поиронизировал мол как можно в 2025 не знать что такое pull/merge request, особенно если приходится часто ревьють код.
eao197
14.05.2025 12:29Я поиронизировал мол как можно в 2025 не знать что такое pull/merge request
Давайте будем точными: речь шла про MR, а не про PR.
И да, непонятно, если я не пользуюсь gitlab-ом от слова совсем, то чем это в 2025-ом отличается от 2015-го?
Cfyz
14.05.2025 12:29Есть такая штука, называется "здравый смысл". Необязательно все доводить до абсурда =/.
К нынешнему моменту наверное 99% опенсурса хостятся на двух платформах: github и gitlab. Сложно найти разработчика, который с ними не сталкивался.
Я конечно догадался что вы не признали термин merge request, потому и написал мол это как pull request, только gitlab.
И разумеется это мало чем отличается от 2015, вот как будто непонятно откуда в моем комментарии взялся 2025.
eao197
14.05.2025 12:29К нынешнему моменту наверное 99% опенсурса хостятся на двух платформах: github и gitlab. Сложно найти разработчика, который с ними не сталкивался.
Из которых только около 1% на gitlab-е.
Cfyz
14.05.2025 12:29Претензия не к ООП или архитектуре.
Претензия к использованию устаревших и не идиоматических конструкций. Как будто вы не очень хорошо знаете C++, но тогда возникает вопрос зачем вы пишете пример именно на нем?
Kahelman Автор
14.05.2025 12:29Я бы был менее категоричен: во-первых: «не очень хорошо знаете современный С++»,
Во-вторых: в предыдущей статье я ссылался на книгу Гради Буча, которая вышла некоторое время назад. Примеры там написаны на C++/Java без использования новейших фич языков и даже без темплейтов. Так что я старался держаться одного стиля с оригиналом.
MonkeyWatchingYou
14.05.2025 12:29Чтоб повысить заход на эту статью надо было добавить теги #c, #assembler и #xml.
И непонятно зачем и холиварно.
Шутка.
nin-jin
14.05.2025 12:29ermouth
14.05.2025 12:29Ширина/высота прямоугольника и радиус – по-хорошему uint. Ширина и высота это к слову не dx, dy – которые и правда могли бы быть int. То, что у автора везде int – скорее проблемы автора.
nin-jin
14.05.2025 12:29А это и не ширина и высота. Поправил нейминг. Ну и тип радиуса тоже, спасибо.
Kahelman Автор
14.05.2025 12:29Как в анекдоте: «я знал что дискуссия будет только по последнему вопросы ..»
Cfyz
14.05.2025 12:29Следующая итерация уже в пути. Требования изменятся. Канвас расширится. Архитектура проявит себя.
Не хватает: треугольника, квадрата, овала, ромба. :)
Спойлер: в итоге не будет никаких Circle, Rectangle и прочих треугольников, останется только Shape с набором кривых, описывающих контур фигуры.
Kahelman Автор
14.05.2025 12:29Все гораздо проще. Есть план «развития» продукта. Что вполне вписывается в «тестовую» архитектуру. Задача посмотреть как разные подходы будут себя вести. Опять-таки, задача сохранить „time to market“. Ниже привели пример, который вроде как позволит все безгранично расширять. Но как MVP это в любом случае оверинжиниринг.
Chamie
14.05.2025 12:29Вот вам безгранично расширяемый MVP без оверинжиниринга (TypeScript):
Скрытый текст
const create = { point: (x: number, y: number) => () => `Drawing a Point at (${x},${y})`, circle: (x: number, y: number, r: number) => () => `Drawing a Circle R=${r} at (${x},${y})`, rectangle: (x1: number, y1: number, x2: number, y2: number, width: number) => () => `Drawing a Rectangle (${x1}, ${y1}) to (${x2},${y2}) width=${width}`, }; const getCanvas = () => { const shapes: Function[] = []; return { add: (shape: Function) => shapes.push(shape), render: () => shapes.forEach(shape => shape()), } } const main = () => { const canvas = getCanvas(); canvas.add(create.circle(1, 2, 3)); canvas.add(create.point(1, 2)); canvas.add(create.rectangle(1, 2, 3, 4, 5)); canvas.render(); }
Есть к нему какие-нибудь претензии?
Dhwtj
14.05.2025 12:29Есть, конечно
Рисование пропало внутри create тут и конструктор и рисование
Значит, при добавлении или модификации нового метода будет всё сломано.
Сделай рисование красным. Сделай вычисление площадей
Chamie
14.05.2025 12:29Рисование пропало внутри create тут и конструктор и рисование
Ничего не пропало, фабричные функции (в терминах ООП) создают объекты (сущности) с требуемой от них функциональностью. Посколько требовалось пока только делать "render", то из одной этой функции сущности и состоят.
Значит, при добавлении или модификации нового метода будет всё сломано.
Да с чего бы сломано? Как там вообще что-то сделать, чтобы сломать?
Сделай рисование красным.
По хорошему, покраска уже после отрисовки формы должна быть. Типа такого:
type Color = "Black" | "Red" | "Green" | "Blue" | "Magenta" | "Salmon" | "Gold"; const getCanvas = () => { const shapes: Shape[] = []; return { add: (shape: Shape) => shapes.push(shape), render: (color?: Color) => shapes.map(shape => shape() + (color ? ` ${color}` : "")), } }
Сделай вычисление площадей
Тоже работы на 3 минуты.
Скрытый текст
type Shape = { render: () => string, getArea: () => number, } type Color = "Black" | "Red" | "Green" | "Blue" | "Magenta" | "Salmon" | "Gold"; const create = { point: (x: number, y: number): Shape => ({ render: () => `Drawing a Point at (${x},${y})`, getArea: () => 0, }), circle: (x: number, y: number, r: number): Shape => ({ render: () => `Drawing a Circle R=${r} at (${x},${y})`, getArea: () => r * r * Math.PI, }), rectangle: (x1: number, y1: number, x2: number, y2: number, width: number): Shape => ({ render: () => `Drawing a Rectangle (${x1}, ${y1}) to (${x2},${y2}) width=${width}`, getArea: () => Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2)) * width, }) }; const getCanvas = () => { const shapes: Shape[] = []; return { add: (shape: Shape) => shapes.push(shape), render: (color?: Color) => shapes.map(shape => shape.render() + (color ? ` ${color}` : "")), getTotalArea: () => shapes.reduce((sum, cur) => cur.getArea() + sum, 0), } } const main = () => { const canvas = getCanvas(); canvas.add(create.circle(1, 2, 3)); canvas.add(create.point(1, 2)); canvas.add(create.rectangle(1, 2, 3, 4, 5)); canvas.render(); canvas.render("Gold"); }
Хотя, можно, конечно, и поупарываться
Скрытый текст
type ShapeType = "circle" | "point" | "rectangle"; type Shape = [ShapeType, ...number[]]; type Color = "Black" | "Red" | "Green" | "Blue" | "Magenta" | "Salmon" | "Gold"; const create = { circle: (x: number, y: number, r: number): Shape => ["circle", x, y, r], point: (x: number, y: number): Shape => ["point", x, y], rectangle: (x1: number, y1: number, x2: number, y2: number, width: number): Shape => ["rectangle", x1, y1, x2, y2, width], }; const renderers: Record<ShapeType, (shape: Shape) => string> = { circle: ([_, x, y, r]) => `Drawing a Circle R=${r} at (${x},${y})`, point: ([_, x, y]) => `Drawing a Point at (${x},${y})`, rectangle: ([_, x1, y1, x2, y2, width]) => `Drawing a Rectangle (${x1}, ${y1}) to (${x2},${y2}) width=${width}`, } const render = (shape: Shape, color: Color = "Black") => (renderers[shape[0]] || (() => ""))(shape) + (color ? ` ${color}` : ""); const areaCalculators: Record<ShapeType, (shape: Shape) => number> = { circle: ([_, x, y, r]) => r * r * Math.PI, point: () => 0, rectangle: ([_, x1, y1, x2, y2, width]) => Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2)) * width, } const getArea = (shape: Shape) => (areaCalculators[shape[0]] || (() => NaN))(shape); const getCanvas = () => { const shapes: Shape[] = []; return { add: (shape: Shape) => shapes.push(shape), render: (color?: Color) => shapes.map(shape => render(shape, color)), getTotalArea: () => shapes.reduce((sum, cur) => getArea(cur) + sum, 0), } } const main = () => { const canvas = getCanvas(); canvas.add(create.circle(1, 2, 3)); canvas.add(create.point(1, 2)); canvas.add(create.rectangle(1, 2, 3, 4, 5)); canvas.render(); canvas.render("Gold"); canvas.getTotalArea(); }
Dhwtj
14.05.2025 12:29circle: (x: number, y: number, r: number): Shape => ({ render: () => `Drawing a Circle R=${r} at (${x},${y})`, getArea: () => r * r * Math.PI, }),
Нарушение принципа единственной ответственности, разные вещи а одном месте и разделить нельзя. То есть если один функционал пришел один человек, а другой другой, то они будут мешать друг другу. Даже если один, то вам придется постоянно переключать контекст внимания
Chamie
14.05.2025 12:29Так это что же, ООП — это сплошное нарушение принципа единственной ответственности?
Ну, и да, для этого есть второй вариант.
ermouth
14.05.2025 12:29не будет никаких Circle
Окружность не представима кривыми Безье. Так что может и будут, аппроксимация не всегда приемлема.
Cfyz
14.05.2025 12:29Ну значит возьмем B-сплайны. Какими кривыми аппроксимировать это детали реализации. В первом приближении можно вообще набором прямых обойтись.
ermouth
14.05.2025 12:29возьмем B-сплайны
Окружность точно не представима никакими сплайнами, хотя аппроксимируют почти всегда через них. Точно берут там, где важна гомогенность пространства, это типа чтобы когда вы вал в отверстии виртуально поворачиваете, его не заклинивало на узлах соотв производных из-за того, что мы аппроксимацию взяли вместо окружности.
Jijiki
14.05.2025 12:29у безье если она опишет круг на сколько я понял когда вникал(пока не глубоко), надо будет определять попадает точка в растеризаторе или не попадает(это я увидел в видео по шрифту), если попадает, то вставляем точку, но тут шейп это ББ скорее всего, соотв круг имеет радиус, потомучто я сомневаюсь что задача написать растеризатор, потомучто проще картинку с теми же точками грузануть, если
{
T P1; // Point 1
T C1; // Control 1T P2; // Point 2
T C2; // Control 2
}х2 соотв проверить можно в блендере, кароче есть туториал там ИК он крепит к ходу по безье и там можно создать круг канал забыл как называется polyfjord (Animate a Character in 15 Minutes in Blender)
ну и в конце надо по гаусу размыть край по сигме 1 или 2
ну и не знаю если это 100% круг можно и стандартной функцией пройтись с радиусом, а в конце гаусом
Скрытый текст
Jijiki
14.05.2025 12:29Скрытый текст
#include <print> #include <string> #include <vector> //include <vectormath> class Shape { public: virtual void draw() const = 0; virtual std::string name() const = 0; virtual ~Shape() {} }; template<typename T> class Point : public Shape { T p[2];//vec2//[] public: Point(T x, T y) { p[0]=x; p[1]=y; } void draw() const override { std::println("Drawing {} at {} {}",name(),p[0],p[1]); } std::string name() const override { return std::string("Point"); } }; template<typename T> class Circle : public Shape { T p[3];//x y r//vec3//[] public: Circle(T x, T y, T r) { p[0]=x; p[1]=y; p[2]=r; } void draw() const override { std::println("Drawing {} at {} {} {}",name(),p[0],p[1],p[2]); } std::string name() const override { return std::string("Circle"); } }; template<typename T> class Rectangle : public Shape { T p[4]; //x, y, w, h;//vec4//[] public: Rectangle(T x, T y, T w, T h){ p[0]=x; p[1]=y; p[2]=w; p[3]=h; } void draw() const override { std::println("Drawing {} at {} {} {} {}",name(),p[0],p[1],p[2],p[3]); } std::string name() const override { return std::string("Rectangle"); } }; //maybe variadic для прохода текущих шейпов class Canvas { std::vector<Shape*> shapes;//или по T std::vector/std::array текущего шейпа public: void add(Shape* s) { shapes.push_back(s); } void render() const { for (auto s : shapes) s->draw(); } ~Canvas() { for (auto s : shapes) delete s; } }; int main() { Canvas canvas; canvas.add(new Point<int>(1, 1)); canvas.add(new Circle<int>(5, 5, 3));//состоит из точек canvas.add(new Rectangle<int>(0, 0, 6, 3));//состоит из точек canvas.render(); return 0; }
clang++20 -std=c++26 main.cpp
во как бы я сделал, но в идеале точки бы были из библиотеки матеши - это будет влиять на архитектуру кстати
Kahelman Автор
14.05.2025 12:29На мой неискушенный взгляд вы тут слегка огород нагородили. Вместо понятного синтаксиса: тип: переменная вы все закинули в массив с неясной структурой. Далее, у вас все элементы массива оказались одного и того же типа. Что может быть нежелательным ограничением.
std::println("Drawing {} at {} {} {} {}",name(),p[0],p[1],p[2],p[3]);
}Я бы не хотел такой код поддерживать, когда автор давно работу поменял, а мне понять надо что там и где используется.
Jijiki
14.05.2025 12:29потомучто в вашем примере не было векторной математики, вывод переведён в print и отдебажен текущий вывод состояния, так у вас в определённом типе данных вы храните в вашей текущей версии позицию точки
ну значит позиция в точке, и 2 переменные
Скрытый текст
template <typename T> class Vec2{ private: public: T vec[2]; Vec2(T a,T b){ this->vec[0]=a; this->vec[1]=b; } T& operator [](int idx) { return vec[idx]; } const T operator [](int idx)const { return vec[idx]; } void qprint(){ std::println("{} {}",vec[0],vec[1]); } const f32* data() const { return vec[0]; } f32* data() { return vec[0]; } }; using vec2 = Vec2<f32>; using ivec2 = Vec2<int>;
у меня щас так, и мат и кватернион и двойной, перед отрисовкой даже span не нужен просто читаю из array/vector позиции точек и рисуется, значит у вас это тип только под позиции типо позиция картинки в 2д
сам я впервые даже new пока не использовал
так громоздко да, но удобно использовать
int main(){ vec3 v1(1.0f,2.0f,3.0f); vec3 v2(1.0f,2.0f,3.0f); vec3 v3=v1; v1.qprint(); mat4 m(5.0f,1.0f,1.0f,0.0f, 0.0f,9.0f,1.0f,1.0f, 6.0f,1.0f,8.0f,0.0f, 0.0f,0.0f,1.0f,2.0f); mat4 m1(1.0f); m.qprint();m1.qprint(); mat4 m2=m1*m; m2.qprint(); mat3 m3(5.0f,1.0f,1.0f,0.0f,9.0f,1.0f,6.0f,1.0f,8.0f); m3.qprint(); m3=inverse(m3); m3.qprint(); return 0; }
Jijiki
14.05.2025 12:29тоесть вам надо написать векторную математику (vec2-3-4, quaternion или rot, mat2-3-4)), а потом относительно её архитектуры накидывать архитектуру отрисовки и растеризации точек треугольников или позиций картинок, спасибо за минус - у меня кстати работает )
Kelbon
14.05.2025 12:29Не стал менять архитектуру (почти), ведь задачи как таковой нет и тут нечего архитектурить, просто переписал на новые технологии
#include <anyany/anyany.hpp> struct draw_m { static void do_invoke(const auto& self) { self.draw(); } template <typename CRTP> struct plugin { void draw() const { auto& self = *static_cast<const CRTP*>(this); aa::invoke<Foo>(self); } }; }; struct name_m { static std::string do_invoke(const auto& self) { self.name(); } }; using vec2d = ...; using shape = aa::any_with<draw_m, name_m>; struct point { vec2d pos; void draw() const; std::string name() const; }; struct circle { vec2d center; int r = 0; void draw() const; std::string name() const; }; struct rectangle { vec2d lefttop; vec2d sizes; void draw() const; std::string name() const; }; struct canvas { std::vector<shape> shapes; public: void add(shape s) { shapes.push_back(std::move(s)); } void render() const { for (shape& s : shapes) s.draw(); } }; int main() { canvas c; c.add(point({1, 1})); c.add(circle({5, 5}, 3)); c.add(rectangle({0, 0}, {6, 3})); c.render(); return 0; }
Dhwtj
14.05.2025 12:29Суета какая-то
{-# LANGUAGE GADTs #-} -- Определение типа Shape data Shape where Point :: Int -> Int -> Shape Circle :: Int -> Int -> Int -> Shape Rectangle :: Int -> Int -> Int -> Int -> Shape -- Функция для рисования draw :: Shape -> String draw (Point x y) = "Drawing Point at (" ++ show x ++ ", " ++ show y ++ ")" draw (Circle x y r) = "Drawing Circle at (" ++ show x ++ ", " ++ show y ++ "), r = " ++ show r draw (Rectangle x y w h) = "Drawing Rectangle at (" ++ show x ++ ", " ++ show y ++ "), " ++ show w ++ "x" ++ show h -- Определение типа Canvas data Canvas = Canvas [Shape] -- Функция для добавления фигуры add :: Shape -> Canvas -> Canvas add shape (Canvas shapes) = Canvas (shape : shapes) -- Функция для рендеринга render :: Canvas -> [String] render (Canvas shapes) = map draw shapes -- Пример использования main :: IO () main = do let canvas = add (Point 1 1) $ add (Circle 5 5 3) $ add (Rectangle 0 0 6 3) (Canvas []) mapM_ putStrLn (render canvas)
Kahelman Автор
14.05.2025 12:29Товарищи, пожалуйста, читайте ТЗ до конца:
Задача: реализовать базовый графический редактор
Фигуры: точка, линия, круг, квадрат, прямоугольник, треугольник, ромб, овал
Функциональность: добавление фигур на канвас, отрисовка
С таким подходом мы собеседование в FAAG ( или как его там, FB, Amazon,Apple,Google..) не пройдем :)
Все в детали углубились а базовый функционал никто не реализовал.
Dhwtj
14.05.2025 12:29{-# LANGUAGE GADTs #-} newtype Angle = Degrees Double deriving (Eq, Show) data Shape where Point :: Double -> Double -> Shape -- x, y Line :: Double -> Double -> Double -> Double -> Shape -- x1,y1,x2,y2 Triangle :: Double -> Double -> Double -> Double -> Double -> Double -> Shape -- x1,y1,x2,y2,x3,y3 Circle :: Double -> Double -> Double -> Shape -- centerX,centerY,radius Rectangle :: Double -> Double -> Double -> Double -> Angle -> Shape -- centerX,centerY,width,height,angle Square :: Double -> Double -> Double -> Angle -> Shape -- centerX,centerY,side,angle Rhombus :: Double -> Double -> Double -> Double -> Angle -> Shape -- centerX,centerY,diag1,diag2,angle Oval :: Double -> Double -> Double -> Double -> Angle -> Shape -- centerX,centerY,radiusX,radiusY,angle draw :: Shape -> String draw (Point x y) = "Drawing Point at (" ++ show x ++ ", " ++ show y ++ ")" draw (Line x1 y1 x2 y2) = "Drawing Line from (" ++ show x1 ++ ", " ++ show y1 ++ ") to (" ++ show x2 ++ ", " ++ show y2 ++ ")" draw (Triangle x1 y1 x2 y2 x3 y3) = "Drawing Triangle with vertices ("++show x1++","++show y1++"), ("++show x2++","++show y2++"), ("++show x3++","++show y3++")" draw (Circle x y r) = "Drawing Circle at ("++show x++","++show y++"), r = "++show r draw (Rectangle x y w h angle) = "Drawing Rectangle at ("++show x++","++show y++"), "++show w++"x"++show h ++ rotate angle draw (Square x y side angle) = "Drawing Square at ("++show x++","++show y++"), side = "++show side ++ rotate angle draw (Rhombus x y d1 d2 angle) = "Drawing Rhombus at ("++show x++","++show y++") with diagonals "++show d1++","++show d2 ++ rotate angle draw (Oval x y rx ry angle) = "Drawing Oval at ("++show x++","++show y++") with radii "++show rx++","++show ry ++ rotate angle rotate :: Angle -> String rotate (Degrees 0.0) = "" rotate (Degrees a) = ", rotated by " ++ show a ++ " degrees around its center" data Canvas = Canvas [Shape] add :: Shape -> Canvas -> Canvas add shape (Canvas shapes) = Canvas (shape:shapes) render :: Canvas -> [String] render (Canvas shapes) = map draw (reverse shapes) main :: IO () main = do let emptyCanvas = Canvas [] let canvas = add (Point 0 0) $ add (Line 0 0 5 5) $ add (Triangle 0 0 3 0 0 4) $ add (Circle 10 10 5) $ add (Rectangle 20 20 10 5 (Degrees 30)) $ add (Square 15 15 8 (Degrees 45.1)) $ add (Rhombus 25 25 14 10 (Degrees 60.760)) $ add (Oval 40 40 12 7 (Degrees 75.0876)) emptyCanvas mapM_ putStrLn (render canvas)
Пока всё идёт нормально
Dhwtj
14.05.2025 12:29F# более компактный, читаемый, тоже чисто функциональный подход
И заменил на точки вместо пар чисел
// Определяем типы для углов и точек type Angle = Degrees of float type Point2D = { X: float; Y: float } // Определяем тип Shape как discriminated union type Shape = | Point of p: Point2D | Line of p1: Point2D * p2: Point2D | Triangle of p1: Point2D * p2: Point2D * p3: Point2D | Circle of center: Point2D * radius: float | Rectangle of center: Point2D * width: float * height: float * angle: Angle | Square of center: Point2D * side: float * angle: Angle | Rhombus of center: Point2D * diag1: float * diag2: float * angle: Angle | Oval of center: Point2D * radiusX: float * radiusY: float * angle: Angle // Вспомогательная функция для отображения точки let showPoint (p: Point2D) = sprintf "(%g, %g)" p.X p.Y // Функция для строки поворота let rotate angle = match angle with | Degrees 0.0 -> "" | Degrees a -> sprintf ", rotated by %g degrees around its center" a // Функция "рисования" фигуры let draw shape = match shape with | Point { p = p } -> sprintf "Drawing Point at %s" (showPoint p) | Line ({ p1 = p1; p2 = p2 }) -> sprintf "Drawing Line from %s to %s" (showPoint p1) (showPoint p2) | Triangle ({ p1 = p1; p2 = p2; p3 = p3 }) -> sprintf "Drawing Triangle with vertices %s, %s, %s" (showPoint p1) (showPoint p2) (showPoint p3) | Circle ({ center = center; radius = r }) -> sprintf "Drawing Circle at %s, r = %g" (showPoint center) r | Rectangle ({ center = center; width = w; height = h; angle = angle }) -> sprintf "Drawing Rectangle at %s, %gx%g%s" (showPoint center) w h (rotate angle) | Square ({ center = center; side = side; angle = angle }) -> sprintf "Drawing Square at %s, side = %g%s" (showPoint center) side (rotate angle) | Rhombus ({ center = center; diag1 = d1; diag2 = d2; angle = angle }) -> sprintf "Drawing Rhombus at %s with diagonals %g,%g%s" (showPoint center) d1 d2 (rotate angle) | Oval ({ center = center; radiusX = rx; radiusY = ry; angle = angle }) -> sprintf "Drawing Oval at %s with radii %g,%g%s" (showPoint center) rx ry (rotate angle) // Тип для холста (список фигур) type Canvas = Shape list // Добавление фигуры на холст (в начало списка) let add shape (canvas: Canvas) = shape :: canvas // Рендеринг холста: применяет draw к каждой фигуре в обратном порядке добавления let render (canvas: Canvas) = canvas |> List.rev |> List.map draw // Главная функция [<EntryPoint>] let main argv = let emptyCanvas: Canvas = [] // Создаем точки для удобства let pt x y = { X = float x; Y = float y } let canvas = emptyCanvas |> add (Oval { center = pt 40 40; radiusX = 12.0; radiusY = 7.0; angle = Degrees 75.0876 }) |> add (Rhombus { center = pt 25 25; diag1 = 14.0; diag2 = 10.0; angle = Degrees 60.760 }) |> add (Square { center = pt 15 15; side = 8.0; angle = Degrees 45.1 }) |> add (Rectangle { center = pt 20 20; width = 10.0; height = 5.0; angle = Degrees 30.0 }) |> add (Circle { center = pt 10 10; radius = 5.0 }) |> add (Triangle { p1 = pt 0 0; p2 = pt 3 0; p3 = pt 0 4 }) |> add (Line { p1 = pt 0 0; p2 = pt 5 5 }) |> add (Point { p = pt 0 0 }) render canvas |> List.iter (printfn "%s") 0 // Возвращаем код выхода
Ну, побежали дальше. Что мы там должны развивать?
Kahelman Автор
14.05.2025 12:29Я не такой быстрый. Мне все ваши наброски ещё множить в репо чтобы потом разбираться можно было :) наконец-то функциональщики подключились :)
kreofil
14.05.2025 12:29Что-то уже было в предыдущем поколении. И споры. И даже с кодом: http://softcraft.ru/paradigm/dhp/
Дежавю, однако...Kahelman Автор
14.05.2025 12:29Одна из проблем отечественной школы - абсолютно кондовый язык. Как будто через дебри пробираешься. Причем в переводах «классиков» поточное не встречается, т.е. можно на русском нормально писать технические тексты.
kreofil
14.05.2025 12:29Для коллекции. Код на Си. Ну, почти чистом Си (http://softcraft.ru/ppp/ppc/).
#include <stdio.h> #include <stdlib.h> typedef struct Point {int x, y;} Point; void PointInit(Point* p, int x, int y) { p->x = x; p->y = y; } void PointDraw(Point* p) { printf("Drawing Point at (%d, %d)\n", p->x, p->y); } char* PointName() { return "Point"; } typedef struct Circle {int x, y, r;} Circle; void CircleInit(Circle* c, int x, int y, int r) { c->x = x; c->y = y; c->r = r; } void CircleDraw(Circle* c) { printf("Drawing Circle at (%d, %d), r = %d\n", c->x, c->y, c->r); } char* CircleName() { return "Circle"; } typedef struct Rectangle {int x, y, w, h;} Rectangle; void RectangleInit(Rectangle* r, int x, int y, int w, int h) { r->x = x; r->y = y; r->w = w; r->h = h; } void RectangleDraw(Rectangle* r) { printf("Drawing Rectangle at (%d, %d), %d*%d\n", r->x, r->y, r->w, r->h); } char* RectangleName() { return "Rectangle"; } typedef struct Shape {}<> Shape; Shape + <Point;>; Shape + <Circle;>; Shape + <Rectangle;>; void Draw<Shape* s>() = 0; void Draw<Shape.Point* s>() { PointDraw(&(s->@)); } void Draw<Shape.Circle* s>() { CircleDraw(&(s->@)); } void Draw<Shape.Rectangle* s>() { RectangleDraw(&(s->@)); } char* Name<Shape* s>() {return NULL;} // = 0; char* Name<Shape.Point* s>() { return PointName(); } char* Name<Shape.Circle* s>() { return CircleName(); } char* Name<Shape.Rectangle* s>() { return RectangleName(); } Shape* CreateShapeAsPoint(int x, int y) { struct Shape.Point* s = create_spec(Shape.Point); PointInit(&(s->@), x, y); return (Shape*)s; } Shape* CreateShapeAsCircle(int x, int y, int r) { struct Shape.Circle* s = create_spec(Shape.Circle); CircleInit(&(s->@), x, y, r); return (Shape*)s; } Shape* CreateShapeAsRectangle(int x, int y, int w, int h) { struct Shape.Rectangle* s = create_spec(Shape.Rectangle); RectangleInit(&(s->@), x, y, w, h); return (Shape*)s; } void DeleteShape(Shape* s) {free(s);} typedef struct Canvas { int len; Shape* shapes[100]; } Canvas; void CanvasInit(Canvas* c) {c->len = 0;} void CanvasClear(Canvas* c) { for(int i = 0; i < c->len; ++i) { DeleteShape(c->shapes[i]); } c->len = 0; } void CanvasAdd(Canvas* c, Shape* s) { c->shapes[c->len++] = s; } void CanvasRender(Canvas* c) { for(int i = 0; i < c->len; ++i) { Draw<c->shapes[i]>(); } } int main() { Canvas canvas; CanvasInit(&canvas); CanvasAdd(&canvas, CreateShapeAsPoint(1, 1)); CanvasAdd(&canvas, CreateShapeAsCircle(5, 5, 3)); CanvasAdd(&canvas, CreateShapeAsRectangle(10, 10, 10, 10)); CanvasRender(&canvas); CanvasClear(&canvas); return 0; }
kreofil
14.05.2025 12:29Если интерес касается разных эволюций кода, то можно посмотреть здесь:
https://github.com/kreofil/evo-situations/tree/main/evolution
Старое описание ситуаций можно почитать отсюда:
http://softcraft.ru/ppp/simplesituations/Из последнего: http://softcraft.ru/ppp/
Jijiki
14.05.2025 12:29это графический редактор надо добавлять шейп в растр в нужную точку(тоесть есть полотно формата и есть известный его размер)[w*h*4] - просто так шейпы собирать не надо в массивах, и по save сохранять в картинку, если это отрисовка в через массив шейпов это картинки шейпов с (pos left corner,w/h) для интерфейсов или функционал кисточки тоже с нужным офсетом на полотне
withkittens
Горшочек, не вари.