После статьи «ООП не мертво. Вы просто пользуетесь им как молотком по клавиатуре» комментарии кипели
Кто-то звал 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)
 - skovoroad14.05.2025 12:29- Это очень плохой пример, потому что для игрушечных задач ООП не нужен. Он нужен для сложных абстракций, для разделения когнитивной нагрузки между группами разработчиков. У вас никогда не будет отдельной группы разработчиков для кружочков, а отдельной - для треугольничков. - Давайте попробуем вернуться в реальность и представим, что эти квадратики - что-то более-менее сложное. Очевидно, функции draw() должны обращаться к каким-то связанным с рисованием зависимостям (как минимум, тянуть заголовочные файлы). Но сами прямоугольники могут использоваться в контекстах, которые не подразумевают никакой связи с рисованием. Например, я не знаю, библиотека, которая вычисляет пересечения прямоугольников. На кой ляд ей draw() и все её зависимости? - А значит, фигура у нас разбивается на две части: Rectangle и RectangleDrawer (если предположить, что у рисовальщика есть состояние, например, кэш, и просто функции недостаточно, то понадобится класс), а RectangleDrawer станет частью своей иерархии. Они будут связываться, к примеру, как визиторы, и тут... - И тут в помещение вбегают критики ООП и говорят, что вот наворотили дикий оверинжиниринг! И они правы, потому что для этой примера достаточно POD-структур, для их хранения - несколько векторов, а для рисования - перегруженных (и даже это необязательно) свободных функций. Плохой пример, негодный. Надо придумать пример реального размера, в котором ООП действительно нужно.  - Kahelman Автор14.05.2025 12:29- Я с вами не согласен. ООП это подход к разработке. Демонстрация подходов и их применимости делается на простых примерах. Графический редактор это простейший приме, позволяющий выявить плюсы и минусы подходов. В частности, наследование квадрата от прямоугольника -классическая ошибка в ООП дизайне. Здесь вы можете доходчиво объяснить, почему так не следует делать. - Ваша критика похожа на заявление что Ньютоновская механика не подходит, поскольку не работает в масштабах Вселенной.  - skovoroad14.05.2025 12:29- Ваш пример не убеждает, а разубеждает пользоваться ООП. - Он не подчёркивает его сильные стороны (разделение кода на изолированные куски с хорошо описанной изолированной ответственностью) - Но зато он отлично иллюстрирует аргументы критиков ООП: вы усложнили примитивную задачу. - (ну и плюс задача решена некорректно, рисование не является частью прямоугольника, как я уже выше и написал)  - Kahelman Автор14.05.2025 12:29- Это баттл- я не предлагаю реализацию. Я предлагаю сравнить подходы. Ссылка на репозитарий в конце. Сделайте вашу идеальную реализацию и пришлите MR. Потом можем осудить…  - skovoroad14.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- Не хватает: треугольника, квадрата, овала, ромба. :)  - Chamie14.05.2025 12:29- У вас тоже - К слову, знать бы ещё, что вы под ними подразумеваете, потому что в моей геометрии для рисования прямоугольника на плоскости нужно как минимум 5 чисел (крутит в руке телефон), а «овалов» там вообще гора разных. 
 
  - Kahelman Автор14.05.2025 12:29- И не плохо бы было их в коллекцию запихнуть чтобы в main все не прописывать.  - skovoroad14.05.2025 12:29- зачем в коллекцию? В вашем примере коллекция никак не используется 
- но если есть какая-то ломовая необходимость в коллекции конкретно в этом микроскопическом примере, у вас есть стандартный std::vector<std::variant> 
 - Понимаете, вы сейчас начнёте накручивать требования, которых нет в примере. Моё замечание было не про ООП (это мощный и полезный подход во множестве случаев), а про ваш пример. Он не иллюстрирует сильные стороны ООП. Поэтому идея соревнования в решении этой задачи в разных парадигмах не имеет смысла.  - Kahelman Автор14.05.2025 12:29- Тогда возьмете на себя задачу реализовать на чистом процедурном подходе? std:vectorstd:variant это хак по меньшей мере. Стандартные процедурные языки не используют понятие темплейтов. Поправьте если я не прав. Соответственно в рамках чисто процедурного подхода эта задача просто не решается. Вам надо либо с void указателями работать, либо «изобретать» свой rtti. Что имеет место быть, но как бы добавляет аргументов в сторону сторонников ООП подхода. Со своей стороны могу на AWK реализовать :)  - eao19714.05.2025 12:29- Стандартные процедурные языки не используют понятие темплейтов. - Ada-83 как раз таким языком и была. 
  - skovoroad14.05.2025 12:29- Да господи, стандартная библиотека в 2025-м году от РХ у него хак. Как будто кто-то запрещает писать на плюсах в процедурном стиле. Ну возьмите раст, там enum из коробки - тот же вариант. Код ещё в три раза короче станет, кстати. - Вы по-прежнему не туда воюете, я уже сороковой комментарий подряд пытаюсь вам объяснить, что проблема не в ООП, а в вашей статье, в которой выбран неудачный подход демонстрации достоинств ООП. Ладно, это бесполезно.  - Comdiv14.05.2025 12:29- что проблема не в ООП, а в вашей статье - Замените слово «статья» на «код» и окажется, что вы описали причину, почему ООП как общая парадигма для написания кода несостоятельна.  - skovoroad14.05.2025 12:29- Зачем я буду заменять слово "статья" на слово "код", если я имею а виду не код, а всю статью? Т.е. контекст использования этого кода. - Код, конечно, тоже плохой, но речь не об этом. 
  - Comdiv14.05.2025 12:29- Затем, что статья в данном случае олицетворяет код на ООП, который опять не поняли и неправильно применили. Речь ведь не о коде из статьи. 
 
  - Kahelman Автор14.05.2025 12:29- Замените слово shape на Message, а Canvas на messageQueue. Получите задачу обработки входящих сообщений. Не ожидал что абстрактное мышление это отдельная фича :)  - skovoroad14.05.2025 12:29- Слушайте, если у вас всё надо заменить, чтобы вас правильно понять, может, вы уже признаете, что пример плохой? Задача организации очереди сообщений это другая задача и действительно служила бы лучше целям статьи. 
  - nin-jin14.05.2025 12:29- Вот пример очереди на ООП. Но новое слово тут не в том, что ООП, а в том, что wait-free. 
  - nin-jin14.05.2025 12:29- И правда, при чём тут ООП, если всё сделано на объектах.. Наверно это ФП такой. - Тест с 1000 потоками уже давно есть. А нежданчик не случится благодаря барьерам памяти. 
  - cupraer14.05.2025 12:29- Вы считываете значение и обновляете его за двумя разными барьерами. - А ООП ни при чём, потому что не используется. Замените объекты на структуры — и ничего не изменится. 
  - nin-jin14.05.2025 12:29- Вы, похоже, не понимаете как работают барьеры и для чего вообще нужны. - В ООП языках структуры от объектов мало чем отличаются в принципе. Есть лишь небольшие различия в дефолтном поведении. 
  - skovoroad14.05.2025 12:29- Это новое слово в разработке. - Как пример для статьи про ООП на хабре? Почему, собственно, нет? Речь не идёт про промышленную универсальную реализацию, вы даже не знаете, какие требования будут к такой очереди, но уже лезете в бутылку. Вполне можно навертеть ООП-абстракций для примера, начиная от иерархии самих типов сообщений и заканчивая какими-нибудь, я не знаю, подписчиками. Это иллюстративный материал для статьи, он не обязан летать на утюгах. 
- Ах да, я вспомнил, вы тот самый любитель общественного внимания, который обращается к собеседнику "тупой дегенерат", когда его самого макают в, назовём это так, чрезмерную широту обобщений на фоне ограниченнго кругозора. 
 Тогда вопросов не имею.
 
 
 
 
 
 
 
 
 
  - cupraer14.05.2025 12:29- Графический редактор это простейший пример, позволяющий выявить плюсы и минусы подходов. - Смешно. На КОБОЛе написаны тонны кода, который работает десятилетиями, и у него — прикиньте — нет графических/десктопных примитивов. - При это КОБОЛ на момент создания был гигантским прыжком вперед, почти революцией. А вот десктопов не было, упс. - Кроме того, никому в здравом уме не придёт в голову писать графический редактор без использования библиотек в 2025 году. А еще на джаваскрипте это будет короче, понятнее, и быстрее. Упс².  - Kahelman Автор14.05.2025 12:29- О написании графического редактора без библиотек и вообще о написании графического редактора речи не идёт. Речь о классическом примере реализации приложения на на ООП. Можно было бы последовать примеру товарища Буча и взять его вариант с Гидропонной схемой. Но тут у народ в 5 примитивах разбираться не может и ТЗ из трех строчек до конца прочитать. Вы кстати на Коболе писать собираетесь? Причём тут что на нем тонны кода написаны? На bash написано больше чем не COBOL, и продолжают писать и что?  - cupraer14.05.2025 12:29- Я не собираюсь писать вообще, я свободное время трачу на OSS, а не на взнуздание сферических коней в вакууме. Но если бы собирался, написал бы на насквозь функциональном эликсире. - Реализация полиморфизма, который здесь нужен, в ООП — самая кривая.  - Kahelman Автор14.05.2025 12:29- Давайте, я думал на erlang сам написать, но если в на elixir напишите -будет классно. Ссылка на репо внизу статьи. Ждём реализации 
 
 
 
 
  - Dhwtj14.05.2025 12:29- для игрушечных задач ООП не нужен. Он нужен для сложных абстракций, для разделения когнитивной нагрузки между группами разработчиков - И для этого он тоже не нужен. - Снизить межкомандную когнитивную нагрузку можно, сведя общий API к компактному и редко меняющемуся ядру, а частные вариации вынеся в простые адаптеры. 
  - Pardych14.05.2025 12:29- А мы пишем класс в ООП потому что ожидаем что его будет отдельная команда разрабатывать? Эвона чо.  - skovoroad14.05.2025 12:29- Мы пишем класс (и прочими способами упрощаем поддержку кода), потому что писатель кода и читатель могут различаться, да (это может быть и один человек, но в разное время). Это действительно причина - Код, в котором никому никогда не надо будет разбираться и поддерживать, всё равно, в какой парадигме писать - Эвона чо 
 
 
 - onets14.05.2025 12:29- Хорошо, что выбрали графический редактор - на нем можно показать плюсы ооп, вместо бизнес приложения, где центром являются данные и их согласованность  - Kahelman Автор14.05.2025 12:29- За данные и их согласованность должна отвечать БД. Правило это не модно и молодёжно. Правда никто пока не показал как обеспечить гарантированную согласованность данных в распределенной системе. :)  - askv14.05.2025 12:29- Потому что таких гарантий не существует? )  - Kahelman Автор14.05.2025 12:29- Потому что задача «византийских генералов» не решаема :( - https://ru.m.wikipedia.org/wiki/Задача_византийских_генералов  - cupraer14.05.2025 12:29- Да закопайте вы уже эту детскую страшилку. Прекрасно всё решаемо. CAP-теорема даже доказывает, что если выбросить A — то всё будет супер-консистентно. - Не нужно использовать термины, про которые вы краем уха слышали от Рабиновича. 
 
 
 
 
 - ermouth14.05.2025 12:29- Редактор – это где редактировать можно. В примере какая-то надстройка для усложнения использования канваса, простите.  - Kahelman Автор14.05.2025 12:29- Ага вы готовы за 20-30 минут сделать MVP редактора в вашем понимании?  - ermouth14.05.2025 12:29- Зачем? По вашим условиям на любом языке получится примерно одно и то же. По вашим условиям по сути нужно через вызов add с кортежем {тегФигуры, ...параметры} положить его куда-то в стейт приложения, а потом по команде их все нарисовать. - В вашем примере ООП подход ничего вообще не добавляет, ну кроме километра ритуальной по сути писанины.  - Kahelman Автор14.05.2025 12:29- Согласен. Идея в том что дальше идут «требования на изменения» и «дополнительные фичи» идея посмотреть как будут развиваться процедурный/функциональный/ООП подходы 
 
 
 
 - eao19714.05.2025 12:29- Как же печально видеть в 2025-ом году C++ный код с ручными new/delete и детскими ошибками, описанными еще в первых изданиях "Язык программирования C++" :(((  - Kahelman Автор14.05.2025 12:29- Это минимальная реализация - максимально близко к «классикам». Жду вашего MR с «правильной реализацией». Поскольку тут Баттл- одними комментариями не отделаетесь. Нет MR - слив засчитан :)  - eao19714.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-ownership - Kahelman Автор14.05.2025 12:29- Первоначальный вариант был с unique_ptr. Потом выбросил, т.к. вопрос про архитектуру ООП vs XXX а не про современные подходы и best practice в C++.  - eao19714.05.2025 12:29- Демонстрация "как в этом вашем C++ отстрелить себе ногу не прилагая никаких усилий" получилась наотлично. 
 
  - Cfyz14.05.2025 12:29- using- ShapeUptr = std::unique_ptr<Shape>;- Использование алиасов почём зря (например в подобных тривиальных случаях) ухудшает читаемость кода. Не надо так. - shapes.push_back(std::move(s))- Скорее наверное emplace_back()? - Я хз что такое MR - Это довольно иронично, что вы в 2025 не знаете что такое MR =). Подсказка: это как PR, только gitlab.  - eao19714.05.2025 12:29- Не надо так. - Спасибо, но вредные советы (а это очень вредный совет) идут в сад. Желающие выписывать раз за разом - std::unique_ptr<что-то-там>следуют туда же.- Скорее наверное emplace_back()? - Когда у нас на входе готовый unique_ptr, то разницы быть не должно.  - Cfyz14.05.2025 12:29- Спасибо, но вредные советы (а это очень вредный совет) идут в сад. - Ну так вы первый начали. - Любой алиас это сокрытие конкретного типа и небольшая, но дополнительная когнитивная нагрузка на удержание в памяти что это такое на самом деле и/или постоянные сверки то ли это, что ты думаешь. - Даже дюжина повторений в коде не стоит этого. Вы несколько раз экономите невероятные 10 символов, из которых половину все равно подставит IDE, а читаете это снова и снова и снова каждый раз, когда приходится вернуться к этому фрагменту кода. - Одно дело, если тип фундаментален для проекта и используется в нем повсеместно. Но в единичных и/или простых случаях использование всяких FooPtr, FooMap и прочих одному автору понятных типов только лишь потому что видите ли лень пару лишних клавиш нажать -- это вредная практика.  - eao19714.05.2025 12:29- Любой алиас это сокрытие конкретного типа - Нет. Вы сразу ошибаетесь. А из ошибочных предпосылок происходят и ошибочные выводы. - Все, что вы написали -- это ерунда. Особенно про "видите ли лень пару лишних клавиш нажать -- это вредная практика". - Более того, когда в проекте есть устоявшиеся правила именования указателей (вроде ShapeUptr, ShapeShptr), то уже из названия типа видно, с каким указателем мы имеем дело и это отнимает гораздо меньше усилий, чем читать бесконечные std::unique_ptr. Особенно в сочетаниях типа - std::shared_ptr<std::vector<std::unique_ptr<Shape>>>.- Ну и главное, хорошие привычки нужно вырабатывать сразу, на простых ученических задачках. - ЗЫ. Минус на вашем комментарии не от меня. Я вообще на Хабре минусы не расставляю (за очень-очень-очень редким исключением).  - Cfyz14.05.2025 12:29- Нет. Вы сразу ошибаетесь. Все, что вы написали -- это ерунда. - Вы меня конечно извините, но это аргументация уровня "нет ты". - когда в проекте есть устоявшиеся правила именования указателей (вроде ShapeUptr, ShapeShptr), то уже из названия типа видно - Это можно отнести к упомянутому мной частному случаю, когда типы настолько общеупотребимы в проекте, что без их знания все равно никуда. Кроме того, если в проекте исторически сложилось использование таких сокращений, то бессмысленно спорить. Даже если используется какая-нибудь венгерская нотация, все равно придется писать как уже написано. - Закавыка в том, что все равно у каждого второго проекта будут свои собственные устоявшиеся правила. Вам может показаться что ShapreUptr это что-то совершенно очевидное, но это не так. - Особенно в сочетаниях типа - std::shared_ptr<std::vector<std::unique_ptr<Shape>>>- По-моему это больше похоже на контр-пример. - Пользоваться такими типами очень тяжело и легко приводит к ошибкам, с алиасом или без. Если в коде постоянно нужно использовать и передавать что-то подобное, то вместо тяп-ляп спрятать имя под алиасом, по-хорошему надо задуматься о выделении соответствующей сущности в отдельный тип. - Потому что постоянное жонглирование вложенностью-разыменованием и безымянными полями абстрактных контейнеров это форменная жесть с какой стороны ни посмотри. - Имя, которое невыносимо печатать из-за его длины -- это чаще всего признак проблемы, которую надо решать не алиасом. - Ну и главное, хорошие привычки нужно вырабатывать сразу, на простых ученических задачках. - Как например использование emplace_back() вместо push_back() даже если в данном конкретном случае компилятор оптимизирует лишний move в ноль? =) - Но суть моей придирки к алиасам в том, что вы фактически производите небольшую обфускацию и называете это хорошей привычкой.  - eao19714.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 -- ну такое себе, "сомнительно, но окай" (с)  - Cfyz14.05.2025 12:29- Я насмотрелся на программистов, которые не используют using-и. - Ну а я насмотрелся на тех, которые пихают typedef и using где надо и не надо. - Явное всегда лучше неявного. - Если у вас программисты пишут плохой код потому что они не используют using почем зря -- дело совершенно точно не в using. - Для emplace_back есть очевидный сценарий применения <...> Для добавления же в конец вектора готового объекта предназначен push_back. - Вот тут вынужден согласиться, в данном случае почем зря emplace приплел.  - eao19714.05.2025 12:29- Ну а я насмотрелся на тех, которые пихают typedef и using где надо и не надо. - Примеры можно? Что-то мне сложно представить, как using-ами можно код испортить. - Может из OpenSource что-нибудь? - Если у вас программисты пишут плохой код потому что они не используют using почем зря -- дело совершенно точно не в using. - В том-то и дело, что не у меня. Почитаешь профильные ресурсы, все профи просто высшего разряда. Как придешь какой-нибудь проект консультировать, так просто в шоке -- где все те монстры от программирования, которые себя пяткой в грудь в комментариях бьют. И код корявый получается не потому, что тупые неумехи его пишут, а потому, что просто вовремя не научили, как можно себе жизнь облегчить, а код -- упростить. 
  - Cfyz14.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> в примере выше? - Что, конечно же, вас ничуточку не переубедит. Поэтому здесь мои полномочия видимо все, окончены =(. 
  - eao19714.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-а, сейчас стало бы гораздо проще. - А что пользоваться этим невозможно, так кому какое дело. - Интересно почему этим невозможно пользоваться? - Что, конечно же, вас ничуточку не переубедит - Есть немаленькая вероятность, что я программирую дольше, чем вы живете на свете. И есть еще большая вероятность, что говнокода пришлось разгрести тоже побольше. Так что да, не убеждают. А вот ощущение, что вы сперва сказали ерунду, а потом ее старательно защищаете, только усиливается. - Кстати говоря, профнепригодность относилась к совету не использовать алиасы в простых случаях. А не то, что вы написали выше. 
 
 
 
 
  - skovoroad14.05.2025 12:29- Казалось бы, наоборот? Если я знаю, что FooPtr это некий уместный в данном контексте тип указателя, то я не думаю о нижележащем типе и когнитивная нагрузка падает? - Мне неважно, что он юник или шейред, мне важно, что он указывает на Foo и используется в заданной сигнатуре. Остаётся только нужная информация. Нагрузка падает.  - Cfyz14.05.2025 12:29- Мне неважно, что он юник или шейред - Нюанс в том, что когда действительно неважно что это за тип, то как правило это будет typename T или auto =). - А вот что автор скрыл под Ptr -- unique, shared, QPointer, указатель из boost или вообще голый указатель -- это обычно существенно влияет на семантику владения, совместимость между типами и как с этим можно обращаться вообще помимо -> и *. - Да, внутри функции вам действительно часто все равно, что это за указатель. Ну так в названии переменной или поля тип и не указывается, shape и shape.  - skovoroad14.05.2025 12:29- Семантика владения и прочие детали важны при написании кода (и то в большинстве случаев компилятор не даст ошибиться). - Но код читается кратно больше, чем пишется. И когнитивная нагрузка - это про чтение, а не про написание. Здесь алиасы работают, как хорошее именование переменных и даже как конструирование типов - упрощает код.  - Cfyz14.05.2025 12:29- Но код читается кратно больше, чем пишется. И когнитивная нагрузка - это про чтение, а не про написание. - Именно. И поэтому аргумент мол не хочется выписывать раз за разом полное имя -- плохой. - Здесь алиасы работают, как хорошее именование переменных - Именно. И поэтому сокращение имен или использование нестандартных наименований без существенной на то причины -- усложняет чтение. - Семантика владения и прочие детали важны при написании кода - И при попытке понять что происходит в данном фрагменте кода. Особенно когда неочевидно это оно специально так написано или случайно получилось и возможно тут ошибка. - Но опять-таки, не все алиасы плохие. Дискуссия началась с использования using ShapeUptr = std::unique_ptr<Shape>. Это, положа руку на ногу, просто бесполезная перестановка символов местами, которая выигрывает несколько символов ценой замены явного, всем и каждому понятного типа на локальную историю, которую надо будет найти и запомнить (и сразу же забыть, потому что в каждом случае она разная). Зачем? Просто потому что.  - eao19714.05.2025 12:29- Это, положа руку на ногу, просто бесполезная перестановка символов местами, которая выигрывает несколько символов - Блин, еще раз повторяю: экономия символов здесь не при чем от слова совсем. - Зачем? - Затем, что именованные типы (даже в виде алиасов) служат как дополнительный слой абстракции и скрывают лишние детали. Когда вам потребуется узнать что за ShapeUptr -- вы посмотрите. А пока это не потребовалось, то лучше использовать одно имя вместо std::unique_ptr. - Просто потому что. - Если вы чего-то не понимаете, то это не значит, что в этом нет смысла. Аргументов вам здесь уже привели в ассортименте. Причем разные люди. 
 
 
 
 
  - Dooez14.05.2025 12:29- autoтакже скрывает тип, но повсеместен в современном C++. Хотя может вы из секты людей, которые всегда явно пишет тип переменных?- Хороший алиас позволяет в первую очередь меньше читать. Если он ограничен областью видимости, то когнитивная нагрузка вполне вероятно будет меньше чем при его отсутствии. - Алиасы значительно упрощают рефакторинг и позволяют избежать багов. Обобщенный код без алиасов я даже представлять не хочу. - Если говорить про границы API то конечно лучше иметь сигнатуры со стандартными именами. Но в реализациях функций и классов не вижу проблемы при грамотно выбранных именах.  - Cfyz14.05.2025 12:29- autoтакже скрывает тип, но повсеместен в современном C++. Хотя может вы из секты людей, которые всегда явно пишет тип переменных?- Фундаментальное отличие auto в том, что он скрывает тип в очень небольшом контексте. Грубо говоря вот видим объявление auto i = vec.begin(), и вот в пределах экрана эта переменная используется. - То же самое с локальным using в пределах области видимости. - Проблема с теми алиасами, которые определены где-то в совершенно другом месте и надо вспоминать/искать что это такое. IDE еще могут быстро подсказать, но при чтении/ревью например в web-интерфейсе это очень мешает. - Заметьте, что я не агитирую за полный отказ от using вообще. Только там, где без этого можно легко обойтись. - Алиасы значительно упрощают рефакторинг и позволяют избежать багов. Обобщенный код без алиасов я даже представлять не хочу. - Обобщенный алиас алиасу рознь. Одно дело using ElementType = T; в контейнере. И совсем другое это сокрытие семантики, как это часто происходит с указателями или многоэтажными контейнерами. Бездумное использование алиасов запросто может наоборот стать причиной неочевидной ошибки. - Но в реализациях функций и классов не вижу проблемы при грамотно выбранных именах. - Вот казалось бы, std::unique_ptr<Shape> -- куда уж лучше. Указатель, уникальный, из стандартной библиотеки. Но нет, это слишком просто. 
 
 
  - simplepersonru14.05.2025 12:29- Сделали в проекте такие глобальные юзинги (в условном common/core.h) : - template <class T> using U = std::unique_ptr<T>;- И также SH с шаредптр - Читаемость не теряется и не нужно руками делать на каждый такой класс отдельный юзинг. - Плюс такого подхода, что мы всегда видим оригинальный класс и например поиск по символу нужно делать не для каждого такого юзинга, а только для оригинального класса  - eao19714.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 все равно остается на месте. 
  - simplepersonru14.05.2025 12:29- Однобуквенность. Вопрос привычки, такая практика. Их всего 2 таких алиаса, как самые часто-используемые, понятно что если бы их было 10, это уже совсем другой разговор, но эти даже интуитивно понятные 
- В примере с ShapeUptr угловых скобок было бы на одну пару меньше, они не то чтобы куда-то испарятся все. Но вместе с тем появился и дополнительный символ, его нужно написать. Не сделать forward decl с оригинальным классом, чтобы использовать этот юзинг (т.к. он как правило рядом с определением класса), а в моем примере можно в U<MyClass> пихать такую декларацию при определённых условиях. Ну и то что говорил про поиск по символу 
- Про замену типа указателя. Для меня выглядит как исключительное событие и думаю писал полный тип хитрого указателя по месту, чтобы не вводить в заблуждение пользователя этого символа на предмет что под капотом. Вообще пример странный, не могу представить себе такое. 
 - Приведу шутошную аналогию, это не аргумент и не всерьез: - Давайте в проекте определим и будем использовать - using Int = int;- Мало ли нам понадобится массово подменить целые числа, а имя Int все равно останется на месте  - eao19714.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 и компилятор еще и сам по рукам разработчикам бить начнет, когда они радиус с шириной начнут путать по недосмотру. - Jijiki14.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; } }
 
 
 
 
 
  - cupraer14.05.2025 12:29- Не знал, что неиспользование гитлаба — смертный грех, караемый остракизмом. - Я, например, подавляющее большинство написанного кода открываю в OSS, у нас это неповоротливое говнище не в чести.  - Cfyz14.05.2025 12:29- Автор исходного комментария посетовал (справедливо) мол как можно так писать на C++ в 2025. А еще он сказал что не знает, что такое MR. - Я поиронизировал мол как можно в 2025 не знать что такое pull/merge request, особенно если приходится часто ревьють код.  - eao19714.05.2025 12:29- Я поиронизировал мол как можно в 2025 не знать что такое pull/merge request - Давайте будем точными: речь шла про MR, а не про PR. - И да, непонятно, если я не пользуюсь gitlab-ом от слова совсем, то чем это в 2025-ом отличается от 2015-го?  - Cfyz14.05.2025 12:29- Есть такая штука, называется "здравый смысл". Необязательно все доводить до абсурда =/. - К нынешнему моменту наверное 99% опенсурса хостятся на двух платформах: github и gitlab. Сложно найти разработчика, который с ними не сталкивался. - Я конечно догадался что вы не признали термин merge request, потому и написал мол это как pull request, только gitlab. - И разумеется это мало чем отличается от 2015, вот как будто непонятно откуда в моем комментарии взялся 2025.  - eao19714.05.2025 12:29- К нынешнему моменту наверное 99% опенсурса хостятся на двух платформах: github и gitlab. Сложно найти разработчика, который с ними не сталкивался. - Из которых только около 1% на gitlab-е. 
 
 
 
 
 
 
  - Cfyz14.05.2025 12:29- Претензия не к ООП или архитектуре. - Претензия к использованию устаревших и не идиоматических конструкций. Как будто вы не очень хорошо знаете C++, но тогда возникает вопрос зачем вы пишете пример именно на нем?  - Kahelman Автор14.05.2025 12:29- Я бы был менее категоричен: во-первых: «не очень хорошо знаете современный С++», - Во-вторых: в предыдущей статье я ссылался на книгу Гради Буча, которая вышла некоторое время назад. Примеры там написаны на C++/Java без использования новейших фич языков и даже без темплейтов. Так что я старался держаться одного стиля с оригиналом. 
 
 
 
 - MonkeyWatchingYou14.05.2025 12:29- Чтоб повысить заход на эту статью надо было добавить теги #c, #assembler и #xml. 
 И непонятно зачем и холиварно.
 Шутка.
 - nin-jin14.05.2025 12:29 - ermouth14.05.2025 12:29- Ширина/высота прямоугольника и радиус – по-хорошему uint. Ширина и высота это к слову не dx, dy – которые и правда могли бы быть int. То, что у автора везде int – скорее проблемы автора.  - nin-jin14.05.2025 12:29- А это и не ширина и высота. Поправил нейминг. Ну и тип радиуса тоже, спасибо. 
  - Kahelman Автор14.05.2025 12:29- Как в анекдоте: «я знал что дискуссия будет только по последнему вопросы ..» 
 
 
 - Cfyz14.05.2025 12:29- Следующая итерация уже в пути. Требования изменятся. Канвас расширится. Архитектура проявит себя. - Не хватает: треугольника, квадрата, овала, ромба. :) - Спойлер: в итоге не будет никаких Circle, Rectangle и прочих треугольников, останется только Shape с набором кривых, описывающих контур фигуры.  - Kahelman Автор14.05.2025 12:29- Все гораздо проще. Есть план «развития» продукта. Что вполне вписывается в «тестовую» архитектуру. Задача посмотреть как разные подходы будут себя вести. Опять-таки, задача сохранить „time to market“. Ниже привели пример, который вроде как позволит все безгранично расширять. Но как MVP это в любом случае оверинжиниринг.  - Chamie14.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(); }- Есть к нему какие-нибудь претензии?  - Dhwtj14.05.2025 12:29- Есть, конечно - Рисование пропало внутри create тут и конструктор и рисование - Значит, при добавлении или модификации нового метода будет всё сломано. - Сделай рисование красным. Сделай вычисление площадей  - Chamie14.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(); } - Dhwtj14.05.2025 12:29- circle: (x: number, y: number, r: number): Shape => ({ render: () => `Drawing a Circle R=${r} at (${x},${y})`, getArea: () => r * r * Math.PI, }),- Нарушение принципа единственной ответственности, разные вещи а одном месте и разделить нельзя. То есть если один функционал пришел один человек, а другой другой, то они будут мешать друг другу. Даже если один, то вам придется постоянно переключать контекст внимания  - Chamie14.05.2025 12:29- Так это что же, ООП — это сплошное нарушение принципа единственной ответственности? - Ну, и да, для этого есть второй вариант. 
 
 
 
 
 
  - ermouth14.05.2025 12:29- не будет никаких Circle - Окружность не представима кривыми Безье. Так что может и будут, аппроксимация не всегда приемлема.  - Cfyz14.05.2025 12:29- Ну значит возьмем B-сплайны. Какими кривыми аппроксимировать это детали реализации. В первом приближении можно вообще набором прямых обойтись.  - ermouth14.05.2025 12:29- возьмем B-сплайны - Окружность точно не представима никакими сплайнами, хотя аппроксимируют почти всегда через них. Точно берут там, где важна гомогенность пространства, это типа чтобы когда вы вал в отверстии виртуально поворачиваете, его не заклинивало на узлах соотв производных из-за того, что мы аппроксимацию взяли вместо окружности. 
 
  - Jijiki14.05.2025 12:29- у безье если она опишет круг на сколько я понял когда вникал(пока не глубоко), надо будет определять попадает точка в растеризаторе или не попадает(это я увидел в видео по шрифту), если попадает, то вставляем точку, но тут шейп это ББ скорее всего, соотв круг имеет радиус, потомучто я сомневаюсь что задача написать растеризатор, потомучто проще картинку с теми же точками грузануть, если - { 
 T P1; // Point 1
 T C1; // Control 1- T P2; // Point 2 - T C2; // Control 2 - }х2 соотв проверить можно в блендере, кароче есть туториал там ИК он крепит к ходу по безье и там можно создать круг канал забыл как называется polyfjord (Animate a Character in 15 Minutes in Blender) - ну и в конце надо по гаусу размыть край по сигме 1 или 2 - ну и не знаю если это 100% круг можно и стандартной функцией пройтись с радиусом, а в конце гаусом - Скрытый текст 
 
 
 - Jijiki14.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]); 
 }- Я бы не хотел такой код поддерживать, когда автор давно работу поменял, а мне понять надо что там и где используется.  - Jijiki14.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; }
 
  - Jijiki14.05.2025 12:29- тоесть вам надо написать векторную математику (vec2-3-4, quaternion или rot, mat2-3-4)), а потом относительно её архитектуры накидывать архитектуру отрисовки и растеризации точек треугольников или позиций картинок, спасибо за минус - у меня кстати работает ) 
 
 - Kelbon14.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; }
 - Dhwtj14.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..) не пройдем :) - Все в детали углубились а базовый функционал никто не реализовал.  - Dhwtj14.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)- Пока всё идёт нормально  - Dhwtj14.05.2025 12:29- F# более компактный, читаемый, тоже чисто функциональный подход - И заменил на точки вместо пар чисел - // Определяем типы для углов и точек 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- Я не такой быстрый. Мне все ваши наброски ещё множить в репо чтобы потом разбираться можно было :) наконец-то функциональщики подключились :) 
 
 
 
 
 - kreofil14.05.2025 12:29- Что-то уже было в предыдущем поколении. И споры. И даже с кодом: http://softcraft.ru/paradigm/dhp/ 
 Дежавю, однако... - Kahelman Автор14.05.2025 12:29- Одна из проблем отечественной школы - абсолютно кондовый язык. Как будто через дебри пробираешься. Причем в переводах «классиков» поточное не встречается, т.е. можно на русском нормально писать технические тексты. 
 
 - kreofil14.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; }
 - kreofil14.05.2025 12:29- Если интерес касается разных эволюций кода, то можно посмотреть здесь: - https://github.com/kreofil/evo-situations/tree/main/evolution 
 Старое описание ситуаций можно почитать отсюда:
 http://softcraft.ru/ppp/simplesituations/- Из последнего: http://softcraft.ru/ppp/ 
 - Jijiki14.05.2025 12:29- это графический редактор надо добавлять шейп в растр в нужную точку(тоесть есть полотно формата и есть известный его размер)[w*h*4] - просто так шейпы собирать не надо в массивах, и по save сохранять в картинку, если это отрисовка в через массив шейпов это картинки шейпов с (pos left corner,w/h) для интерфейсов или функционал кисточки тоже с нужным офсетом на полотне 
 
           
 

withkittens
Горшочек, не вари.