Pull to refresh

Comments 22

С удовольствием прочитал статью, идея интересная. Но для меня странным выглядит конструкция:

    ship = spaceship("Ramambahara");
    object obj = asteroid(335577);
    ship = obj; //вот после это совсем как-то не ожидаешь получить невалидный объект
    if (ship.is_null()) //что-то символичное в этом есть...
    {
        std::cout << "нет больше корабля, его уничтожил астероид...\n";
        return;
    }


Когда работаю на прямую с указателями на интерфейсы, то необходимость проверки результата приведение вверх по иерархии (или предварительной проверки возможности приведения) очевидна сама по себе. Если, конечно задумываться, что возвращает dynamic_cast. А вот в случае с объектами это может ускользнуть.
А в начале делать присвыоение к временному объекту, потом делать проверку на его валидность и только после этого делать финальное копирование — довольно накладно. Хотя использование move семантики уменьшит накладные расходы.
Вы правы, всё что нужно — это добавить конструктор перемещения object(object&&) и оператор перемещения operator = (object&&). В целом можно вообще разделить объекты по ссылке и по значению как из второй статьи цикла Академии с оптимизацией placement new для небольших классов и скаляров. Также можно внутри класса в принципе запретить null-значения, то есть ругаться на object без данных и на dynamic_cast, который вернул null. В целом идею можно развивать до полноценной библиотеки или встраивать в существующую, профит весьма привлекателен.
Чуть-чуть я не успел с дополнением к моему же коментарию… Как библиотеку это то же можно оформить, а потом еще взять сборщик мусора для C++ (а они уже написаны) и в итоге получим некоторое подобие C#. Но будут ли такие программы быстры, ведь только факт использования C++ не гарантирует быстродействия.

Я не против таких идей, они раскрывают богатство языка, но не стоит их класть в основу разработки на C++. Я читал выпуски Академии, но считаю — не нужно совершать «побег из темницы типов». Бывают случаи когда без этого код получается уродливым, тогда да, но это пусть это будет маленький кусочек в большом проекте. А если таким становится весь проект, то нужно менять язык или архитектуру приложения.
Собственно что хотел я сказать первым коментарием — не надо стараться сделать C++ похожим на Java, C# или тем более на Python. C++ имеет свою идеологию, свои правила, свои плюсы и минусы.

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

Иногда такое можно использовать, но базировать на таких методах разработку на C++ не стоит.
Всё на самом деле гораздо интереснее. Сейчас C++ очень часто встраивается в высокоуровневые библиотеки бизнес-логики и писать биндинги под такие обобщённые объекты на порядок проще. Соответственно преобразования в те же Python и C# получаются крайне простыми и код будет почти одинаковым, что важно. Это верно и в обратную сторону, в игровой индустрии например C++ состовляет основу движка и поверх идёт скриптовая обвязка, оптимизировать данный подход несложно, а разработчики будут легко работать как внутри движка, так и в расширениях. Сейчас скорость разработки на вес золота.
Кроме всего прочего очень часто библиотеки общего пользования очень часто изобилуют Pimpl'ами. Данный подход позволяет оптимизировать расходы на разработку «классов реализации» выстроив иерархию классов данных, что опять же положительно скажется на скорости разработки.
Не стоит также забывать на скорость вхождения в работу новых людей. Код получается простым и понятным, в нём проще разобраться, новички быстро втягиваются и пишут полноценный код. Опять же увеличивая скорость разработки.
Я не спорю про скорость разработки. Я к тому, что за это приходится платить скоростью работы приложения (смотря что за приложение, конечно). Известные библиотеки хорошо, но если эта билиотека позволяет писать на одном языке так как буд-то пишешь на другом, то может стоит просто взять другой язык? И тогда искать сотрудников становится еще проще.

Языки C#, Python, подкупают своей простотой, но за это мы платим скоростью работы приложения, точнее сказать может быть медленее, и все как-то к этому привыкли, хотя понится были упреки в их адрес, что они медленные. Но эта «простота» увеличивает порог вхождения — для правильной разработки, нужно больше знать об устройстве языка, какие механизмы он использует, как работает, что кроется за простым "=".

Это же относится и к вашему решение, я не пытаюсь преуменьшить его интересность и важность.

Pimpl'ы позволяют сохранить работоспособность при использовании разных версий библиотек, когда интерфейс сохраняется, а внутреннее устройство поменялось. И ваше решение их не заменяет.

В общем каждому инструменту свое время и место. И хорошо когда есть обширный выбор.

P.S.
Возможно, я просто, не могу оценить перпективы использование подобных вещей в связке с другими языками (из-за отсутсвия такого опыта) и рассматриваю только применительно к чистому C++.
Скорость разработки с использованием таких классов прямо пропорциональна скорости разработки приложения. Скорость разработки интерфейсных классов при этом не отличается от разработки из с применением паттерна Pimpl.
В некоторых случаях у нас нет выбора и часть функционала надо писать на C++, при этом предоставляя удобный интерфейс для работы другим разработчикам.
Pimpl'ы повзоляют легко менять реализацию. В приведённом коде так же легко можно менять поля и реализацию классов object::data с наследниками. Здесь преимуществ не меньше, а больше. Этот подход гибче и дополнительные классы более обоснованы.
Связка с другими языками это хорошо, но данный подход будет отлично работать и сам по себе в C++ для C++, предоставляя удобный способ работы с иерархией классов.
В качестве альтернативы стандартному RTTI можно использовать LLVM-style RTTI (HowToSetUpLLVMStyleRTTI.html). Так как внутри компилятора приведение типов по иерархии используется интенсивно, он разрабатывался так, чтобы с одной стороны быть достаточно гибким, а с другой стороны иметь меньше накладных расходов, чем стандартный RTTI.
В Qt тоже используется схожий подход. например, в QGraphicsItem и иже с ним.
Чтобы было ближе к Qt имеет смысл сделать copy-on-write для данных. То есть заменить на небольшую обвязку наз std::shared_ptr с перегрузкой operator -> с const. Вообще действительно похоже.
Да RTTI-то тут при чем… Я только про него @eatsig сказал.
А так да, можно аналогии и с Вашей статьей провести.
Опять дочитал до слова virtual и запнулся:

virtual ~object(); // для корректной генерации unique_ptr

Поясните, пожалуйста, что вы имеете в виду?
То, что это единственная причина, по которой здесь нужен виртуальный деструктор, в такой модели в наследниках нет никаких данных, да и сами деструкторы в них не подразумеваются.
Погуглите pimpl unique_ptr: 1, 2, 3
Спасибо, я в курсе. Для пимпла на базовый класс опасно и затратно использовать unique_ptr с виртуальным деструктором, тогда уж shared_ptr + защищенный невиртуальный декструктор. Во всех примерах, что вы привели, unique_ptr в пимпле не используется для хранения указателя на базовый класс. Вследствие несистемности подхода получаем, например, утечку в object::data.
В object::data конечно же нужен виртуальный деструктор. Вечером всё поправлю. Извините за неточность. Вообще shared_ptr тоже не вариант, больше подойдет комбинация подходов из первых двух статей: by-value или copy-on-write — оптимально и без дополнительных затрат.
И вся эта бодяга для того, чтобы написать это??

object obj = asteroid(335577);
asteroid = obj;

И для этого создается каждый раз лишний объект на куче? И пишется двойная иерархия классов с дублированными методами? И это называется «быстро пишется и быстро работает»? И главное — зачем??

Да и само название уже звучит смешно — инкапсуляция интерфейсов. Примерно как «деланье публичными приватных данных».
Объект на куче прекрасно оптимизируется, во-первых by-value из материала второй статьи через placement new для небольших объектов, во-вторых через copy-on-write для больших объектов. Двойная логика работы с классами в больших проектах всё равно пишется, если используется Pimpl, здесь подход позволяет осмыслить классы данных, спрятанные в реализации, через аналогичное дерево иерархии. Зачем: мы пишем код без указателей и ссылок, работая именно с экземплярами классов, а не с какими-то малопонятными ссылками на то, что создала фабрика и выдала нам в виде неудобной синтаксической конструкции в лучшем случае или в виде raw указателя в худшем.
Если вы инкапсулируете привычные интерфейсы типа ISomething* в удобные классы работы с этими интерфейсами Something, то это хорошо. Если вам платят за то, что вы просто пишете код, который хоть как-то работает, не расширяем и трудно читаем — это тоже неплохо (но не для новичка разбирающего ваш код, конечно же).
При чем тут copy-on-write, если вы создаете 100К разных объектов, например?

Далее…

Язык C++ всячески поощряет указатели и ссылки на базовые классы, которые множатся и усложняют код, заворачиваются во всевозможные «умные» указатели

Не поощряют, но дают такую возможность, если есть необходимость.

и порождают километровые строки при любом обращении к подобной конструкции!
Согласитесь, вряд ли удобно использовать такое:
std::unordered_map<std::string, std::vector< std::shared_ptr<base_class>>>

А слабо написать:
using Asteroid = std::unordered_map<std::string, std::vector< std::shared_ptr<base_class>>>;
или
typedef std::unordered_map<std::string, std::vector< std::shared_ptr<base_class>>> Asteroid;
и дальше использовать только тип Asteroid? Хотя в любом случае никто не определяет внешний API через такие типы.

Особенно если для каждого элемента вектора нужна операция класса-наследника, то есть метод не входит в вышеупомянутый base_class

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

Вывод: есть случаи, когда ваш вариант оправдан, но далеко не всегда.
Есть такой же typedef для std::string. Мы с Вами оба видим замечательные стеки с кучей ненужной информации во время ошибок компиляции, линковки или логирования через typeid: std::basic_string<… std::allocator <… > > и т.д. Особенно если мы специализируем шаблон от std::string, там вообще песня получается. Если перегрузка от std::string, или конструкции, содержащей подобный typedef, тоже получаем километровую неинформативную простыню. Вам это точно надо? Вы хотите множить подобный подход? Одно дело через using отсекать лишние упоминания namespace'ов — пространства имён как раз благо, а совсем другое — создавать видимость, что всё хорошо, когда всё крайне запущено, запутано и переусложнено.
API обязан быть простым, понятным и в нём не должно быть ничего лишнего. Sine qua non.
Разработчик, использующий Ваш API не должен через раз делать dynamic_cast, static_cast или, не дай бог, const_cast, только потому, что разработчик API не предусмотрел очевидные use cases. Всё должно быть логично и максимально минималистично в использовании.
Sign up to leave a comment.