Чтобы задать частичную специализацию, мы можем сделать другое определение MyClass:
Здесь переопределяется параметр E на вот эту кракозяблу, которая становится void если выполняется условие, что тип B наследуется от MyBase.
template<typename B>
class MyClass<B, typename std::enable_if<std::is_base_of<MyBase, B>::value>::type>
{};
Здесь переопределяется параметр E на вот эту кракозяблу, которая становится void если выполняется условие, что тип B наследуется от MyBase.
В общем, эта классная идея не выгорела даже на этапе компиляции. Проблема в С++, в концепции инклюдов и forward-declaration'ах. Ему становится плохо от таких специализаций...
Дело в том, что если в .cpp не будет подключен .h, содержащий вашу частичную специализацию, то компилятор о ней и не узнает, и просто возьмет не специализированный шаблонный класс, и сделает его инстанс. То есть
Но еще больше проблем вызывает то, что пользователь может попросту забыть включить хидер со специализацией и получит неправильное поведение. То есть довольно легко отстрелить колено так, что не поймешь откуда прилетело.
Еще сильнее колено отстреливается когда в .h нет нужной специализации, а в .cpp она появляется. Здесь происходит откровенный пиздец, т.к. компилятор думает что класс определен с
Об этом писал в своей статье в предыдущем посте, что такой проблемы в принципе бы не существовало, будь у С++ другая стратегия компиляции, без инклюдов и форвардов.
Дело в том, что если в .cpp не будет подключен .h, содержащий вашу частичную специализацию, то компилятор о ней и не узнает, и просто возьмет не специализированный шаблонный класс, и сделает его инстанс. То есть
Ref<Actor>
заработает как обычный указатель, и не будет уметь сериализоваться/делать нужные штуки. Проблем добавляет цикличность зависимостей - class Actor
уже хочет знать о существовании специализации Ref<Actor>
, и приходится хитро форвардить. А так как в самом class Actor
тоже есть шаблонные методы, например Ref<T> FindActorByType<T>()
, то просто зафорвардить не получится, т.к. реализация функции в том же .h где и форвард, и компилятор ругается на отсутствие реализации Ref<T>
, который определен только forward'ом. Это решаемо, но через уродливые #include ""
посреди .h, что выглядит как минимум странноНо еще больше проблем вызывает то, что пользователь может попросту забыть включить хидер со специализацией и получит неправильное поведение. То есть довольно легко отстрелить колено так, что не поймешь откуда прилетело.
Еще сильнее колено отстреливается когда в .h нет нужной специализации, а в .cpp она появляется. Здесь происходит откровенный пиздец, т.к. компилятор думает что класс определен с
Ref<>
одного размера (8 байт на указатель), а реализацию функций с другим` Ref<>` (8 байт на указатель + 8 байт на вспомогательную инфу). В рантайме это выглядит как запись вне стека/участка памяти и шваркается рандомно. Слава господи address sanitizer сразу подсвечивает такие проблемы. Хотя разобраться в таком креше - это капец, абсолютно не понимаешь почему у тебя программа пытается писать за пределами объекта в конструкторе.Об этом писал в своей статье в предыдущем посте, что такой проблемы в принципе бы не существовало, будь у С++ другая стратегия компиляции, без инклюдов и форвардов.
Уже после проблем с компилятором, смирившись что со специализациями не получается, я стал думать о классах с соответствующим назначением:
И я понял что крупно ошибся. Ведь делать все ссылки на акторы/компоненты с таким функционалом просто не нужно и даже вредно. Например, для списка детей актора мне не нужен весь это функционал сераилизации/восстановления ссылок. Он вреден, ведь у таких ссылок есть накладные ресурсы. То есть по факту такие ссылки нужны в логике компонент/акторов, написанных пользователем, а внутри движка нужны обычные ссылки
В итоге я пришел к более простому и правильному решению - выделить ссылки на акторы/компоненты/ассеты в отдельные классы, отвечающие именно за эту функциональность:
ActorRef<>, ComponentRef<>, AssetRe<>
. И я понял что крупно ошибся. Ведь делать все ссылки на акторы/компоненты с таким функционалом просто не нужно и даже вредно. Например, для списка детей актора мне не нужен весь это функционал сераилизации/восстановления ссылок. Он вреден, ведь у таких ссылок есть накладные ресурсы. То есть по факту такие ссылки нужны в логике компонент/акторов, написанных пользователем, а внутри движка нужны обычные ссылки
В итоге я пришел к более простому и правильному решению - выделить ссылки на акторы/компоненты/ассеты в отдельные классы, отвечающие именно за эту функциональность:
LinkRef<>
, специализирующиеся под Actor/Component, и AssetRef<>
для ссылок на ассетыДалее, были изощрения с самим классом умного указателя. Как я в начале писал, у меня используется внешний счетчик, но в одном блоке памяти с объектом. То есть аллоцируя объект, выделяется чуть больше памяти и перед объектом кладется счетчик. Это улучшает работу с кешем процессора, т.к. счетчик обращаясь к объекту скорее всего будет взаимодействие со счетчиком. При обращении к счетчику мы сразу возьмем весь объект или его кусок в кеш, и его не нужно ждать из памяти, как в случае если счетчик и объект не рядом.
Однако у подхода есть и минус: если остались только слабые ссылки на объект, то после вызова деструктора объекта его память не освобождается, а весь кусок памяти со счетчиком висит пока есть хотя бы одна слабая ссылка. Память освобождается только когда и сильных, и слабых ссылок не остается. В случае с отдельным счетчиком такой проблемы нет, память освобождается, в памяти висит только сам счетчик.
Работает это все через функцию-конструктор, вместо обычного вызова new. То есть создание объекта получается вот так:
Такое оборачивание нужно для того, чтобы перегрузить поведение выделения участка памяти и добавить инициализацию счетчика:
- аллоцируем участок памяти размером
- вызываем placement new для счетчика
- вызываем placement new для объекта
- передать счетчик ссылок в объект
placement new - это конструирование объекта, наподобие обычному new, но без аллокации, а с уже указанным участком памяти
Здесь сразу появляется пачка проблем. Начиная с банального удобства, ведь когда пишешь
Далее проблема использования сильных и слабых ссылок в конструкторе класса. Например, конструируя
Это заставляет искать обходные пути, например писать фабрики для объектов. Для копирования делать функции а-ля
1. использовать фабрики, как описано выше. Просто не удобно, нужно знать о фабриках, получается плохой и не очевидный синтаксис
2. использовать методы пост-инициализации, в которых прокидывать ссылки в детей и т.п. Это нарушает концепцию RAII, которая гласит что объект готов к использованию сразу после конструктора
3. как-то хитро закинуть счетчик ссылок в конструктор, через вторичное глобальное хранилище. То есть объявляем глобальную переменную на тред, в которую кладем счетчик, откуда уже его можно взять в конструкторе. Убивает кеш процессора
4. закидывать счетчик ссылок прямо в конструктор параметром. Придется тащить в параметры конструктора сервисную переменную, а так же во всех наследников этого класса
Однако у подхода есть и минус: если остались только слабые ссылки на объект, то после вызова деструктора объекта его память не освобождается, а весь кусок памяти со счетчиком висит пока есть хотя бы одна слабая ссылка. Память освобождается только когда и сильных, и слабых ссылок не остается. В случае с отдельным счетчиком такой проблемы нет, память освобождается, в памяти висит только сам счетчик.
Работает это все через функцию-конструктор, вместо обычного вызова new. То есть создание объекта получается вот так:
auto myObject = mmake<MyClass>(...);
. Такое оборачивание нужно для того, чтобы перегрузить поведение выделения участка памяти и добавить инициализацию счетчика:
- аллоцируем участок памяти размером
sizeof(RefCounter) + sizeof(MyClass)
- вызываем placement new для счетчика
- вызываем placement new для объекта
- передать счетчик ссылок в объект
placement new - это конструирование объекта, наподобие обычному new, но без аллокации, а с уже указанным участком памяти
Здесь сразу появляется пачка проблем. Начиная с банального удобства, ведь когда пишешь
mmake<MyClass>
в IDE, она не знает что тебе нужно показать список аргументов конструктора, для него это просто какая-то шаблонная функция с вариативным кол-вом аргументов. Соответственно, нужно просто помнить список аргументовДалее проблема использования сильных и слабых ссылок в конструкторе класса. Например, конструируя
Actor
, внутри создается так же и ActorTransform
, который хочет держать слабую ссылку на сам Actor
. Или в конструкторе копирования, нужно продублировать всех детей, которые так же хотят иметь слабую ссылку на родителя. В целом есть еще кейсы, когда в конструкторе объект уже хочет взять сильную или слабую ссылку на себя же. Но так сделать не получится, потому что счетчика еще нет у объектаЭто заставляет искать обходные пути, например писать фабрики для объектов. Для копирования делать функции а-ля
Clone()
, с раздельным конструированием и прокидыванием ссылок внутри. Я попробовал несколько альтернативных подходов1. использовать фабрики, как описано выше. Просто не удобно, нужно знать о фабриках, получается плохой и не очевидный синтаксис
2. использовать методы пост-инициализации, в которых прокидывать ссылки в детей и т.п. Это нарушает концепцию RAII, которая гласит что объект готов к использованию сразу после конструктора
3. как-то хитро закинуть счетчик ссылок в конструктор, через вторичное глобальное хранилище. То есть объявляем глобальную переменную на тред, в которую кладем счетчик, откуда уже его можно взять в конструкторе. Убивает кеш процессора
4. закидывать счетчик ссылок прямо в конструктор параметром. Придется тащить в параметры конструктора сервисную переменную, а так же во всех наследников этого класса
К себе я взял 2 и 4 пункты. 1й может реализовать сам юзер. 3й вредный для производительности. Со 2м оказалось не удобно, т.к. конструктор разбивался фактически на две функции. Писать алгоритмы в таком ключе не просто, а тем более переписывать уже готовые конструкторы, где есть расчет что ссылку можно взять сразу (с сырыми указателями такой проблемы ведь не было). А вот с 4м получилось все более-менее хорошо: алгоритмы просты, т.к. можно сразу работать со ссылками на себя, но приходится везде протаскивать
А вот с тем, как закинуть это в конструктор - отдельная магия. Для этого функция
Так же сама функция
Собственно вот так выглядит комбайн по созданию умных указателей. Внутри он делает все что нужно, на выходе выдавая готовый
RefCounter* refCounter
в параметре конструктора, в т.ч. по всех наследниках. Протаскивание довольно неприятно, ведь это по дефолту уходит в наследников компонент и акторов, но все-таки терпимо и делается быстро.А вот с тем, как закинуть это в конструктор - отдельная магия. Для этого функция
mmake<T>
умеет понимать, принимает ли конструктор T()
первым параметром RefCounter* refCounter
. Если да, то созданный ранее счетчик передается первым параметром в конструктор. Далее идут параметры, переданные в mmake<>(...)
. Там же в mmake<>()
делается проверка на наличие метода PostRefConstruct()
в конструируемом типе, и если есть - вызывается.Так же сама функция
mmake<>()
собственно не функция, а хитрый макрос, который умеет собирать место аллокации в сорцах для последующего анализа аллокаций. То есть создавая объект через mmake<>()
, мы получаем запись о том что в таком-то .cpp в такой-то строке был создан объект размером Х байт. Далее можно просто смотреть откуда в сорцах аллоцируется больше всего. Работает это через прослойку с маленьким объектом на стеке, который запоминает путь к .cpp и строку через макросы __FILE и LINE__.Собственно вот так выглядит комбайн по созданию умных указателей. Внутри он делает все что нужно, на выходе выдавая готовый
Ref<>
с вызовом всего что определено пользователем.Отдельный пункт про наследование. Так уж выходит, что базовый класс для объектов со счетчиком -
Например, есть
и есть
и ты пытаешься сделать
Делать разные счетчики, очевидно не правильно. Можно попробовать применить виртуальное наследование, но тогда все становится хуже, причем весьма неочевидным способом. Дело в том что компилятор перестанет вообще вызывает конструктор
В общем, можно пойти двумя другими путями: использовать интерфейс объекта со счетчиком, убрав из него явно ссылку на счетчик, заменив его виртуальной функцией, которая его возвращает. Тогда с множественным наследованием все окей, но приходится перегружать эту функцию в классе наследнике. Другой путь, немного расточительный по памяти, иметь на каждый базовый класс ссылку на счетчик, и всем раздать одну и ту же ссылку при конструировании. Мы итак уже прокидываем его через конструктор, теперь просто нужно не забыть его прокинуть везде. Немного помогает шаблонно-макросная магия: достаточно внутри класса перечислить всех наследников.
RefCounterable
- начинает пересекаться при множественном наследовании. Например, есть
class ISceneDrawable: public RefCounterable
и есть
class CursorEventsListener: public RefCounterable
и ты пытаешься сделать
class MyClass: public ISceneDrawable, public CursorEventsListener
. Делать разные счетчики, очевидно не правильно. Можно попробовать применить виртуальное наследование, но тогда все становится хуже, причем весьма неочевидным способом. Дело в том что компилятор перестанет вообще вызывает конструктор
RefCounterable(refCounter)
у наследников, даже если это явно прописано, а будет вызывать просто RefCounterable()
. Такая вот редиска при множественном виртуальном наследовании.. Чтобы вызывался RefCounterable(refCounter)
, нужно его вызывать из всех классов. Что не удобно, не красиво и легко забыть, получив очередную пулю в колено.В общем, можно пойти двумя другими путями: использовать интерфейс объекта со счетчиком, убрав из него явно ссылку на счетчик, заменив его виртуальной функцией, которая его возвращает. Тогда с множественным наследованием все окей, но приходится перегружать эту функцию в классе наследнике. Другой путь, немного расточительный по памяти, иметь на каждый базовый класс ссылку на счетчик, и всем раздать одну и ту же ссылку при конструировании. Мы итак уже прокидываем его через конструктор, теперь просто нужно не забыть его прокинуть везде. Немного помогает шаблонно-макросная магия: достаточно внутри класса перечислить всех наследников.
Что ж, имея этот джентельменский набор удобств, остальной перевод указателей превращается практически в рутину. Это все выглядит довольно просто, но пока экспериментируешь и перебираешь подходы, путь оказывается длинным и тернистым. Однако познавательным. Все больше убеждаюсь что написание базовых примитивов самому, будь то контейнеры или умные указатели (или игровой движок), приходит гораздо более глубокое понимание уже готовых инструментов.
Все еще много багов и недоработок, но все поддается контролю и основной путь пройден. Однако, уже наблюдается эффект от перехода на умные указатели, ушли многие баги из-за нуллпоинтеров и использования удаленных объектов. Что, в принципе, логично
Теперь интересно будет вкрутить тот самый GC в умные указатели, с которого и проросла идея своих смарт-поинтеров. Это будет в виде ужасных макосов, вкоряченных в Ref<>, а так же потребуется как-то разметить "рутовые" ссылки, от которых начнется построение дерева памяти. Но эту штуку я уже давным-давно делал и даже писал статью на хабр.
Все еще много багов и недоработок, но все поддается контролю и основной путь пройден. Однако, уже наблюдается эффект от перехода на умные указатели, ушли многие баги из-за нуллпоинтеров и использования удаленных объектов. Что, в принципе, логично
Теперь интересно будет вкрутить тот самый GC в умные указатели, с которого и проросла идея своих смарт-поинтеров. Это будет в виде ужасных макосов, вкоряченных в Ref<>, а так же потребуется как-то разметить "рутовые" ссылки, от которых начнется построение дерева памяти. Но эту штуку я уже давным-давно делал и даже писал статью на хабр.
Хабр
Garbage Collector & C++
Ручное управление памятью с С++ — одновременно один из самых больших плюсов и минусов в языке. Действительно, эта парадигма позволяет создавать очень производительные программы, однако она же рождает...
Еще довольно быстро прикрутил к проекту профилировщик tracy. Он активно используется на работе в Playrix, и весьма удобен для геймдева. Обычно профайлеры показывают статистику за весь промежуток работы приложения, что для игр не удобно. В играх гораздо лучше рассматривать время кадра, статистику между кадрами. Отделять разные этапы загрузки игры. Tracy позволяет все это увидеть, разметить все как тебе нужно. А еще он подсоединяется по сети, и можно профайлить прямо с реального девайса. Это весьма удобно
Интеграция очень простая. Подключаешь сабмодулем, добавляешь пару сорцов или подключаешь cmake, делаешь минимальную разметку и готово. У меня ушло пару часов чтобы подключить tracy к проекту.
Профилировщик кадра - это та штука, которой мне не хватало на протяжении всей разработки движка. До этого выкручивался профайлером MSVS и своими простыми профайлерами, которые просто суммировали время работы функций. Но теперь можно детально изучать время кадра и загрузку. В целом, даже сейчас, цифры получаются приятные, учитывая объем сцены в редакторе. Ведь сам редактор сделано через движковый UI, что само по себе довольно неплохой тест движка. Много функционала, графики, но работает довольно шустро
Для интереса - дамп tracy. Его можно открыть клиентом tracy с версии 0.1.0
Интеграция очень простая. Подключаешь сабмодулем, добавляешь пару сорцов или подключаешь cmake, делаешь минимальную разметку и готово. У меня ушло пару часов чтобы подключить tracy к проекту.
Профилировщик кадра - это та штука, которой мне не хватало на протяжении всей разработки движка. До этого выкручивался профайлером MSVS и своими простыми профайлерами, которые просто суммировали время работы функций. Но теперь можно детально изучать время кадра и загрузку. В целом, даже сейчас, цифры получаются приятные, учитывая объем сцены в редакторе. Ведь сам редактор сделано через движковый UI, что само по себе довольно неплохой тест движка. Много функционала, графики, но работает довольно шустро
Для интереса - дамп tracy. Его можно открыть клиентом tracy с версии 0.1.0
GitHub
GitHub - wolfpld/tracy: Frame profiler
Frame profiler. Contribute to wolfpld/tracy development by creating an account on GitHub.
Немного о дальнейших планах. Сейчас моей целью будет по максимуму стабилизировать движок и платформы. Для этого хочу поднять поддержку ios, android, linux. А так же добавить CI на гитхаб. Причесать кодстайл, поправить ошибки и просто старые места. По ходу дела поправить биндинг в скрипты, сделать его более оптимизированным. По возможности прикрутить spine анимации.
В итоге хочется получить рабочую бета-версию, чтобы уже на ней попробовать делать игры. Так же пробовать внедрить это в какие-нибудь студии. Опыт показал что решение может быть интересным и полезным для других, но нужно быстро и резко внедрять движок, чтобы сразу получать профит. С багами и нестабильным кодом это не получается
В итоге хочется получить рабочую бета-версию, чтобы уже на ней попробовать делать игры. Так же пробовать внедрить это в какие-нибудь студии. Опыт показал что решение может быть интересным и полезным для других, но нужно быстро и резко внедрять движок, чтобы сразу получать профит. С багами и нестабильным кодом это не получается
На выходных прикрутил элементарный CI, который хочется развить до полноценной проверки всех интересующих платформ. Пока что завел MSVS билд под windows, тк на текущий момент только он живой.
Попробовал пару сервисов appveyor и github actions. Оба весьма похожи, но гитхаб нативно интегрирован в гитхаб, поэтому в итоге выбрал его. Теперь в репозитории o2 красуется бейджик о статусе сборки. Теперь хочу восстановить linux платформу и так же загнать ее в CI.
Попробовал пару сервисов appveyor и github actions. Оба весьма похожи, но гитхаб нативно интегрирован в гитхаб, поэтому в итоге выбрал его. Теперь в репозитории o2 красуется бейджик о статусе сборки. Теперь хочу восстановить linux платформу и так же загнать ее в CI.
Немного об удобстве CI через github actions/appveyor. У них похожие концепции - сборка настраивается через конфигурационный файл.
Я всегда был сторонником графического интерфейса, а не текстового. Поэтому не люблю всякие терминалы, CMAKE и подобные вещи. В github action то же самое, там все настройки в текстовом файле с определенным синтаксисом. Сходу навязывается некий синтаксис, то есть правила, которые необходимо соблюдать. Нарушишь эти правила - беда. Это плохо признак UX, ведь это прямое нарушение принципа защиты от ошибки. Все мы люди, можем опечататься, что-то забыть, что поломает систему. Гораздо лучше интерфейсы, где возможность ошибки прост исключена.
Так же текстовый интерфейсы по умолчанию не интуитивны. В GUI можно увидеть тултипы, выпадающие списки, имена полей, то есть можно методом тыка разобраться в функциональности. В текстовом варианте у тебя максимум есть команда —help, которая вываливает тебе тонну текста практически без форматирования.
С github actions у меня сразу такая проблема и возникла, при выборе типа агента на котором запускать сборку. По умолчанию там Linux, мне нужно было выбрать windows.... и я просто не знал что там написать. Пришлось лезть в документацию, специально искать нужную информацию, то есть делать лишние действия. Если бы это был GUI, то у меня был бы выпадающий список с возможными вариантами агентов, и я бы просто выбрал за 5 секунд вместо гугления 5 минут. Но в итоге скрипт мне сгенерировал Chat GPT :)
Мне кажется так происходит из-за проф деформации программистов. Так уж вышло что мы 95% времени работаем с текстом. А так же со сложными вещами. Поэтому идем по накатанной: запихиваем все в текст и не стараемся упростить. Делать GUI как правило лениво, это гораздо дольше чем распарсить аргументы командной строки или текстовый файл.
Но на мой взгляд сейчас со всякими GPT это уже не проблема, достаточно грубо описать функциональность и получить почти работающий GUI код. Даже с генерацией python-gui хорошо справляется ChatGPT, если стравить ему текст из команды —help
Я всегда был сторонником графического интерфейса, а не текстового. Поэтому не люблю всякие терминалы, CMAKE и подобные вещи. В github action то же самое, там все настройки в текстовом файле с определенным синтаксисом. Сходу навязывается некий синтаксис, то есть правила, которые необходимо соблюдать. Нарушишь эти правила - беда. Это плохо признак UX, ведь это прямое нарушение принципа защиты от ошибки. Все мы люди, можем опечататься, что-то забыть, что поломает систему. Гораздо лучше интерфейсы, где возможность ошибки прост исключена.
Так же текстовый интерфейсы по умолчанию не интуитивны. В GUI можно увидеть тултипы, выпадающие списки, имена полей, то есть можно методом тыка разобраться в функциональности. В текстовом варианте у тебя максимум есть команда —help, которая вываливает тебе тонну текста практически без форматирования.
С github actions у меня сразу такая проблема и возникла, при выборе типа агента на котором запускать сборку. По умолчанию там Linux, мне нужно было выбрать windows.... и я просто не знал что там написать. Пришлось лезть в документацию, специально искать нужную информацию, то есть делать лишние действия. Если бы это был GUI, то у меня был бы выпадающий список с возможными вариантами агентов, и я бы просто выбрал за 5 секунд вместо гугления 5 минут. Но в итоге скрипт мне сгенерировал Chat GPT :)
Мне кажется так происходит из-за проф деформации программистов. Так уж вышло что мы 95% времени работаем с текстом. А так же со сложными вещами. Поэтому идем по накатанной: запихиваем все в текст и не стараемся упростить. Делать GUI как правило лениво, это гораздо дольше чем распарсить аргументы командной строки или текстовый файл.
Но на мой взгляд сейчас со всякими GPT это уже не проблема, достаточно грубо описать функциональность и получить почти работающий GUI код. Даже с генерацией python-gui хорошо справляется ChatGPT, если стравить ему текст из команды —help
Апдейт! Завел linux, теперь можно и там запускать игру или редактор. Особо ничего нового со стороны кода не было сделано, т.к. linux был поддержан еще прошлым летом для ребят из гемблинга.
Но пришлось поправить некоторые ошибки компиляции после перехода на умные указатели. Одна из них потребовалась из-за разницы работы компиляторов. При создании ссылки, у типа проверяется наличие метода
Решилось все небольшим рефакторингом. Метод конструирования ссылки был friend'ом классов, которые имели приватный
Сейчас еще остаются проблемы на linux платформе: есть креш на закрытии редактора, не поддерживается сохранение размеров окна между запусками, не меняется тип курсора и другие небольшие недоработки. Все это буду делать планомерно, цель завести Linux полноценной платформой, где запускается редактор со всем нужным функционалом.
Но пришлось поправить некоторые ошибки компиляции после перехода на умные указатели. Одна из них потребовалась из-за разницы работы компиляторов. При создании ссылки, у типа проверяется наличие метода
PostRefConstruct
. Если такой есть, он вызывается. Проблема была в том, что этот метода приватный, а type trait, Определяющий наличие типа - в глобальном неймспейсе. Компилятору MSVS все равно, и он успешно компилировал код, говоря что метод есть. На gcc все строже, и т.к. метод приватный, то взять указатель на функцию невозможно, а значит type trait возвращал false при проверке наличия метода. Из-за этого он не вызывался и где-то там далее поведение программы портилось (а точнее в системе ассетов).Решилось все небольшим рефакторингом. Метод конструирования ссылки был friend'ом классов, которые имели приватный
PostRefConstruct
. По этому же принципу вся инфраструктура по созданию ссылки, в т.ч. type trait, были вынесены в отдельный класс, который уже и дружился с типами с PostRefConstruct
.Сейчас еще остаются проблемы на linux платформе: есть креш на закрытии редактора, не поддерживается сохранение размеров окна между запусками, не меняется тип курсора и другие небольшие недоработки. Все это буду делать планомерно, цель завести Linux полноценной платформой, где запускается редактор со всем нужным функционалом.
О, и конечно же, завел CI на github чтобы проверять собираемость linux. Есть что-то в этих бейджиках на странице проекта 😃
image_2024-08-18_12-17-55.png
14.9 KB
когда перемудрил с кодировкой...