Показаны сообщения с ярлыком Велосипедостроение. Показать все сообщения
Показаны сообщения с ярлыком Велосипедостроение. Показать все сообщения

суббота, 29 октября 2022 г.

[prog.c++] Дикая идея о том, как можно было бы еще улучшить fmtlib и std::format

Навеяно срачем на RSDN вот в этой теме.

По сути, там были указаны реальные проблемы, которые есть при использовании fmtlib и, соответственно, std::format.

Одна из этих проблем в том, что для описания форматера для моего типа T мне нужно делать специализацию для fmt::formatter, а это нельзя сделать в моем пространстве имен. Поэтому, если я в своем пространстве имен только что определил тип T, то мне нельзя здесь же определить и фоматтер для него, нужно сперва закрыть свое пространство имен, а потом открыть его вновь.

Еще одна проблема показана вот в этом комментарии. Позволю себе процитировать:

Сейчас также все не сахар. Форматер в виде специализации класса не вызывается и всё, пока ты правильно не подберёшь синтаксис перегрузки. С шаблонными классами это бывает не очень просто. Вот пример у меня в коде:

namespace htlib::v2
{
template<typename type_t>
struct is_point {

    static std::false_type test(...);
    template<htlib::v2::uint_t dimensions, typename other_t>
    static std::true_type test(const htlib::v2::pointxd_t<dimensions, other_t>&);

    static constexpr bool value = decltype(test(std::declval<type_t>()))::value;
};
template<typename type_t>
inline constexpr bool is_point_v = is_point<type_t>::value;

} // namespace htlib::v2

template<typename char_t, typename point_t>
class std::formatter<point_t, char_t, std::enable_if_t<htlib::v2::is_point_v<point_t>>>
{
//...
}

Вот такие мета-функции приходится писать что бы написать форматер для всех наследников базового шаблонного класса.

Хотя в случае с функциями была простая перегрузка, типа:

template<htlib::v2::uint_t dimensions, typename other_t>
void formatter(const htlib::v2::pointxd_t<dimensions, other_t>&);

В обсуждении прозвучала идея о том, что лучше бы было, если бы за парсинг и форматирование значений отвечали бы не классы, а свободные функции. Свободные функции могли бы размещаться в пространстве имен пользователя, соответственно, они бы подхватывались посредством ADL.

Очень хорошо выгоды от свободных функций сформулированы здесь:

если бы std::format для кастомизации использовал бы не шаблон класса со специализациями, а неквалифицированный вызов функции с каким-то предопределенным именем, это могло бы дать ряд преимуществ:

  • Пользователь мог бы определять кастомные форматтеры в том же пространстве имен, что и пользовательские типы. Это удобнее писать и удобнее читать, потому что не нужно рвать пространства имен или выносить определения в какие-то отдельные места;
  • При определении кастомных форматтеров пользователь мог бы использовать дополнительные параметры по умолчанию — как в списке шаблонных параметров, так и в списке формальных параметров функции, что дает возможность использования SFINAE и вообще дает большую гибкость в тех случаях, когда кастомизацию нужно сделать не для одного конкретного типа, а для какого-нибудь сеймейства типов, объединенных каким-то признаком;
  • Как частный случай — форматтер, определенный для базового класса автоматом будет работать и для всех производных, для которых не предоставлена своя собственая версия форматтера;
  • Используя квалифицированные вызовы, пользователь мог бы внутри кастомго форматтера повторно использовать форматтеры из других пространств имен, что дает возможность декорирования форматтеров.

Хотя лично мне перспектива писать свободные функции parse и format для своих типов не очень нравится, имхо, класс formatter с методами parse и format для таких целей удобнее, имхо. Да и совместимость с уже написанным кодом терять не хочется, так что подход со свободными функциями должен как-то сочетаться с уже написанными formatter-ами.

Посему возникла дурацкая идея о том, как можно подружить оба эти подхода.

Суть в том, чтобы оставить класс formatter как он есть. Но не создавать его напрямую, а получать посредством свободной функции-фабрики. Как раз эту самую функцию-фабрику можно будет определить в собственном пространстве имен рядом со своими типами.

Под катом слепленный на коленке за 10 минут пример того, как это может быть.

Насколько это все реально внедрить в fmtlib не знаю. Заглянул в исходники, но там не то, чтобы все сложно. Но это таки большой проект, в котором я ни бум-бум. Сходу не разобраться.

А копать глубоко нет возможности. Я тут, к сожалению, слегка зашился и чувствую, что едва хватает сил на выполнение обязательств по текущему проекту. Даже за PR для RESTinio не могу взяться :(

Поэтому зафиксирую идею здесь, в блоге. Если у кого-то из читателей будет желание довести это все до мейнтейнеров fmtlib, то хорошо. Если нет, значит кто-то другой придумает что-то получше и сможет довести до внедрения и реализации.

Напоследок скажу, что на RSDN-е были таки обозначены актуальные проблемы, с которыми люди сталкиваются в fmtlib и std::format. И было бы хорошо тем или иным способом решить эти проблемы. Наверняка способы найдутся.

среда, 21 сентября 2022 г.

[prog.c++] Отказался от использования маркера can_throw в arataga

В проекте arataga использовался прием, который на момент активной работы над arataga, существенно повышал мне коэффициент спокойного сна. Подробнее об этом трюке я писал два года назад на Habr-е: can_throw или не can_throw?

В двух словах: в функции/методы, в которых можно было спокойно выбрасывать исключения, передавался специальный маркер типа can_throw_t. Этот маркер указывал, что функция/метод вызывается из контекста, в котором исключения перехватываются и обрабатываются. Соответственно, если такой маркер в функции/методе есть, значит я могу сделать throw или вызвать какую-то бросающую исключение операцию. А если такого маркера нет, то мне нужно обрамить свои действия блоком try-catch.

К появлению can_throw привело то, что в arataga пришлось писать много callback-ов, которые вызывались при выполнении IO-операций через Asio и парсинге HTTP-протокола посредством http_parser. В таких контекстах исключения нельзя было выпускать наружу и требовалось понимать, пишу ли я сейчас код для callback-а или для чего-то другого.

Надо сказать, что прием can_throw мной всегда оценивался как не вполне однозначный.

Да, на этапе активной разработки arataga он сыграл свою положительную роль. И я до сих пор думаю, что если бы не вспомнил про идею с can_throw (это не моя придумка, когда-то что-то подобное обсуждалось на одном из форумов), то реализация первой версии arataga наверняка затянулась бы и мне пришлось бы потратить больше усилий на тестирование.

Но уже тогда, два года назад, было очевидно и то, что маркеры can_throw изрядно замусоривают код. Что с этим делать я не знал и не имел времени (скорее желания), чтобы приостановиться, провести анализ получившегося кода и решить как быть с can_throw дальше.

Потом же развитие aragata было заморожено вообще. Сейчас этот проект важен для меня как демонстрация того, как выглядит реальный код для продакшена на SObjectizer.

Кроме того, arataga представляет из себя отличный полигон для проверки нововведений в SObjectizer. Т.е. если есть идея какой-то новой фичи для SObjectizer-а и эта фича упростит потроха arataga (или снизит накладные расходы), значит фича стоит того, чтобы ее воплотить в жизнь.

Проект arataga интересен еще и тем, что это один из двух проектов за последние несколько лет, в которых я серьезно задумывался над использованием stackfull-короутин. В arataga в виде короутин можно было бы реализовать обработчики отдельных подключений (т.е. модель coroutine per connection). И, если бы в SObjectizer была поддержка stackfull-короутин "искаропки", то может быть именно с модели coroutine per connection я реализацию arataga и начал бы.

Но в SObjectizer поддержки stackfull-короутин нет. Все еще нет.

Давеча у меня появилось некоторое время чтобы подумать о том, что же в SObjectizer можно добавить. Вариантов для рассмотрения не так, чтобы много, и stackfull-короутины в short-list-е.

Тут-то проект arataga в очередной раз и вспомнился. Если добавлять короутины в SObjectizer, то в arataga их можно будет и сразу же протестировать "в деле".

Дабы освежить в памяти детали работы arataga погрузился в код...

И слегка прифигел от количество can_throw в реализации connection-handler-ов.

Когда смотришь свежим взглядом на то, что сам же делал два года назад и уже основательно все позабыл, то твой собственный код уже выглядит как чужой. Что дает возможность оценивать его объективно и непредвзято.

Обилие can_throw обескуражило настолько, что стало понято, что с этим нужно что-то делать. Только после этого можно было задуматься о попытке переписать arataga на короутинах.

Анализ кода показал, что сейчас в arataga осталось совсем не так много мест, в которых нужно заботиться о выбросе исключения наружу. И эти все места уже старательно обернуты в try-catch. Так что can_throw реально стал выглядеть избыточным рудиментом.

Поэтому в итоге can_throw из кода arataga был полностью изъят.

Да, это привносит некоторый риск того, что при доработках arataga впоследствии можно будет забыть о тех правилах, которые были выработаны, которые диктуют использование правильных оберток при написании callback-ов. Но этот риск сейчас выглядит как разумный, думается, что такие штуки должны быть выявлены и исправлены на этапе тестирования.

Так что можно сказать, что интересный эксперимент, проведенный чуть больше двух лет назад, завершился со следующим итогом: на длительном временном отрезке can_throw себя не оправдал.

В самом начале разработки arataga, когда еще не было тех самых оберток над callback-ами и не было полного понимания того, в каких местах и какие callback-и потребуются, can_throw сыграл свою положительную роль. Но затем, по мере накопления опыта и структуризации использованных подходов, can_throw стал больше мешать, чем помогать. И пришло время от can_throw избавиться. Мавр сделал свое дело, мавр может уходить...


История с can_throw -- это еще одна иллюстрация того, что когда ты занимаешься проектом достаточно долгое время, то тебе доводится увидеть как обесцениваются со временем твои старые идеи, казавшиеся когда-то удачными.

PS. Я заикнулся о том, что рассматриваю возможность добавления stackfull-короутин в SObjectizer. Но тут все очень не просто. Погружение в arataga наводит на мысль о том, что stackfull-короутины здесь могут и не помочь. Так что еще рано говорить о том, что решение о добавлении stackfull-короутин в SObjectizer принято. Тут еще нужно думать и думать, да и вообще это уже совсем другая история.

четверг, 8 апреля 2021 г.

[prog.c++.bicycle] Отдельный тип для строковых литералов своими руками

Если я правильно понимаю, то в C и C++ строковые литералы хранятся в статической памяти. Поэтому указатель на строковый литерал остается валидным, фактически, до самого конца работы программы. Т.е. если мы пишем что-то вроде:

class long_living_object {
  const char * value_;
public:
  long_living_object(const char * v) : value_{v} {}
  ...
};

long_living_object make_object_ok() {
  return { "Hello, World!" };
}

то у нас, в принципе, все нормально. В возвращаемом функцией make_object_ok объекте будет находиться валидный указатель.

К сожалению, в C++ нет никакого способа отличить указатель на строковый литерал от указателя на какой-то временный массив символов. Т.е., ничто в языке не запрещает нам сделать так:

long_living_object make_object_bug() {
  char greeting[] = "Hello, World!";
  return { greeting };
}

Чтобы использовать объекты long_living_object было безопасно, нужно скопировать содержимое передаваемой строки вовнутрь. Что требует дополнительных ресурсов, особенно когда длина строки нам заранее неизвестна и мы вынуждены использовать динамическую память.

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

вторник, 10 марта 2020 г.

[prog.c++.bicycle] I'm working on the first draft of experimental type-safe router for RESTinio

The express-like router is available in RESTinio for several years. I think it is a very easy to use tool, that is familiar to many developers. But it has some congenital defects I wanted to repair for a long time.

The congenital defects of express-like router

The propensity to errors

The first defect is that express-like router is error-prone and isn't friendly for repeated cases. Let's look at one of the examples from RESTinio, express_router:

пятница, 6 марта 2020 г.

[prog.c++] Реинкарнация procxx под именем procyy

К сожалению, библиотека procxx выглядит окончательно заброшенной своим автором. Поэтому я решил не отсылать PR в procxx, т.к. его вряд ли приняли бы, а сделал форк под другим именем:

https://github.com/eao197/procyy

Этот форк лежит, в общем-то в том же виде, в котором и procxx. Никаких тестов, продвинутых примеров, проектных файлов для CMake или чего-нибудь еще. Как взял, так и оставил :)

Планов по какому-то дальнейшему развитию нет. Если в дальнейшем потребуется какая-то дополнительная функциональность, то буду добавлять по мере надобности. Если у кого-то будут проблемы с procyy, открывайте issue, по возможности постараюсь разобраться и помочь.

Нет и планов добавить procyy в conan, vcpkg, hunter или какой-нибудь другой менеджер зависимостей. Лично мне это не нужно, а если кому-то нужно, тот пусть и берет на себя все эти заботы. Простите.

PS. Кидаться в меня помидорами по поводу качества кода нет смысла, программист из меня так себе. Получилось как получилось. Конструктивные соображения по поводу улучшения кода продуктивнее оформлять в виде issues.

среда, 9 октября 2019 г.

[prog.c++] Первые результаты второго подхода к удобному парсеру HTTP-полей в RESTinio

Продолжение истории, начатой некоторое время назад. Первая попытка сделать некий удобный в использовании набор инструментов для парсинга HTTP-полей в RESTinio завершилась результатом, который вызывал противоречивые ощущения и меня лично не удовлетворил. В том числе и потому, что сложные HTTP-поля, вроде поля Accept, парсить было бы совсем не просто.

Поэтому когда представилось время вернуться к доработке RESTinio, то была предпринята следующая попытка. И вот сегодня были получены первые результаты. Кому интересно милости прошу под кат:

понедельник, 1 октября 2018 г.

[prog.thoughts] Будет ли SObjectizer-6? И, если, то каким и когда?

Этот пост появился под влиянием комментария к одной из предыдущих заметок. Если кому-то интересно, что лично я думаю о перспективах возникновении принципиально новой версии SObjectizer-а, то милости прошу под кат. Для остальных вряд ли размышления на эту тему будут интересны, сорри.

среда, 15 августа 2018 г.

[prog.c++] Про попытку изобразить strong typedef из подручных средств

У нас завершается очередная итерация работ над демо-проектом Shrimp. На днях выйдет статья на Хабре с некоторыми подробностями (если кто-то пропустил, то вот первая статья про Shrimp, а вот вторая). Сегодня же я попробую рассказать об одной штуке в коде Shrimp-а, которая у меня самого вызывает неоднозначные впечатления. Если кому-то интересны потуги натянуть сову на глобус извращения старого сиплюсплюсника, то милости прошу под кат.

пятница, 18 ноября 2016 г.

[prog.c++.bicycle] Пытаюсь наваять совсем простенькую функцию n_times

Когда-то уже заходила речь (кажется в G+), что хорошо было бы иметь в C++ функцию n_times для того, чтобы не нужно было циклы выписывать руками. Т.е. вместо того, чтобы писать:

const auto iterations = std::stoul(argv[1]);
for( unsigned long i = 0; i != iterations; ++i )
   do_some_action();

А можно было бы изобразить что-то вроде:

n_times( std::stoul(argv[1]), []{ do_some_action(); } );

Ну и в итоге дозрел до того, чтобы добавить n_times в нашу маленькую библиотечку cpp_util. Самый первый, черновой вариант, выглядит вот так:

template< typename COUNT_TYPE, typename LAMBDA >
void
n_times( COUNT_TYPE n, LAMBDA && body )
   {
      loops_details::ensure_non_negative( n );
      for( ; n; --n )
         body();
   }

В нем вспомогательная функция ensure_non_negative в случае, если COUNT_TYPE является знаковым типом, проверит, чтобы n имел неотрицательное значение. В случае же отрицательного значения будет порождено исключение std::invalid_argument.

И вот тут у меня есть сомнения. Ведь такая проверка может вести к дополнительным накладным расходам. В каких-то случаях компилятор ее выбросит. Но в каких-то оставит. А это может не понравится некоторой части потенциальных пользователей. Посему есть мысль пойти по такому варианту:

using namespace cpp_util_3;

// Пользователь не против получить исключение, если ошибся
// со значением аргумента.
n_times< checked_loop >( std::stol(argv[1]), []{ do_some_action(); } );

// Пользователь зуб дает, что все будет чики-пуки.
n_times< unchecked_loop >( std::stol(argv[1]), []{ do_some_action(); } );

Или по такому:

using namespace cpp_util_3;

// Пользователь не против получить исключение, если ошибся
// со значением аргумента.
n_times( untrusted_n(std::stol(argv[1])), []{ do_some_action(); } );

// Пользователь зуб дает, что все будет чики-пуки.
n_times( trusted_n(std::stol(argv[1])), []{ do_some_action(); } );

Интересно мнение читателей: вам бы какой вариант был бы более удобен? Вообще без проверок? С проверками всегда? С отключаемыми проверками?

При этом если n -- это беззнаковое значение, то оно не проверяется вообще. Тут в дело вступает совсем простая шаблонная магия, которая устраняет избыточные проверки прям в compile-time.

PS. Кроме n_times еще хочется сделать up_to (или for_every_i):

using namespace cpp_util_3;

for_every_i( 0, 10, [](auto i) { std::cout << i << std::endl; } );

Тут так же можно подумать на счет checked_loop/unchecked_loop...

пятница, 4 марта 2016 г.

[prog] Mxx_ru 1.6.7

Вышла версия 1.6.7 инструмента Mxx_ru с начальной поддержкой т.н. externals. Теперь в Mxx_ru есть средства для выкачивания внешних зависимостей из Git/Hg/Svn и установки частей этих зависимостей в нужные подкаталоги. Пока внешние зависимости берутся только из репозиториев Git/Hg/Svn. Поддержка закачки тарболлов/архивов будет добавлена в следующей версии. Надеюсь, получится выкатить ее достаточно быстро.

Установить Mxx_ru можно командой gem install Mxx_ru

Обновить Mxx_ru можно командой gem update Mxx_ru

Так же Mxx_ru можно загрузить с SourceForge (gem-файл).

PDF-ку с документацией пока не обновлял. Сделаю это уже после выпуска версии с поддержкой тарболлов. Посему единственная имеющаяся в наличии документация собрана в этом посте под катом.

Итак, что такое MxxRu::externals и как с ними бороться?

четверг, 22 октября 2015 г.

[prog.c++11] Опыт добавления трассировки в SO-5.5.9. Часть II. Variadic templates для упрощения рутины и уменьшения дублирования

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

среда, 21 октября 2015 г.

[prog.c++11] Опыт добавления трассировки в SO-5.5.9. Часть I. Шаблоны и наследование вместо дублирования

Добавил трассировку механизма доставки сообщений до агентов в версию 5.5.9. По ходу дела пришлось решить одну задачку, которая может быть интересна сама по себе, безотносительно SObjectizer. А именно: как создать две слегка отличающися версии одного кода, но без его дублирования и без потери его эффективности.

вторник, 20 октября 2015 г.

[prog.c++] Велосипедостроение: способ разделить интерфейс класса на публичную и приватную части

При написании библиотек иногда сталкиваешься с такой ситуацией: есть некий публичный класс X с набором публичных методов (т.е. этот класс видит и использует пользователь библиотеки). Есть набор других классов в этой библиотеке, не важно публичных или нет, которые так или иначе используют класс X. Пока они работают только с публичными методами класса X, то все хорошо. Но что делать, если им требуется получить доступ к закрытой части X?

понедельник, 8 июня 2015 г.

[prog.c++11] Промежуточный итог экспериментов с конвейерами и реактивным программированием поверх SO-5

Эксперименты с реактивными программированием и выстраиванием обработчиков сообщений в конвейеры начались из-за обсуждения релиза SO-5.5.5 на LOR-е. То, что получилось на данный момент -- это чистой воды черновой proof-of-concept, который развивается как один из примеров в дистрибутиве SO-5. Каких-либо планов по включению pipelines в состав штатных средств SObjectizer пока нет. Более того...

четверг, 9 апреля 2015 г.

[prog.opensource] Реализация MD5 на чистом C++11. Just For Fun :)

При реализации примера md5_bruteforce обнаружил, что практически все актуальные реализации MD5 находятся в больших или не очень больших библиотеках (вроде OpenSSL, PolarSSL, Crypto++, Botan, POCO и т.д.) И если отдельно лежащую, без лишних зависимостей, версию MD5 на чистом С еще можно найти, то вот с C++ными вариантами дело было плохо. Поэтому, взяв за основу код из POCO, сделал свое.

Получился один hpp-файл. В зависимостях только стандартная библиотека C++11. Проверял под MSVS2013, GCC 4.9.2 (Win, Linux), clang 3.4.1 (FreeBSD), clang 3.6.0 (Linux). (Upd. На длинных последовательностях (больше 4GiB), тоже проверял, работает, по крайней мере в 64-битном режиме).

Лицензия -- трехпунктная BSD + лицензия POCO + лицензия от RSA Data Security. Вроде как все OpenSource-ое, допускающее использование в любых проектах, в том числе и закрытых.

Сама реализация лежит здесь. Там же полный "проект", в котором все проверялось. Все под Svn, поскольку мне так удобнее. Если у кого-то будет время и желание взять и разместить где-то еще -- буду только рад.

Сам я вряд ли буду заниматься дальнейшим развитием этого кода, писать документацию, снабжать дополнительными примерами и т.д. Если кому-то это все нужно, спокойно берите мой код за основу и развивайте в том направлении, которое вам нужно/интересно. Если же требуются какие-то мелкие правки/дополнения/улучшения, можно написать мне, постараюсь сделать.

Под катом полный исходник реализации + примеры того, как ей можно пользоваться.

вторник, 7 сентября 2010 г.

[life.wow] Если эллиптический тренажер поставить на колесики…

…то получится сей забавный девайс:

Данный велосипед производит и продает компания ElliptiGO.

Сам не велосипедист, но интересно, а в горку на нем как? Проще, чем на обычном или нет?

вторник, 27 октября 2009 г.

[comp.prog.cpp] Очередной маленький C++ велосипедик: mutable_copy_on_demand

Сделал недавно простой шаблонный класс для упрощения решения маленькой задачи: есть константный объект, нужно выполнить над ним несколько шагов обработки, на каждом шаге возможна модификация части объекта. После всех шагов объект должен быть отослан другому агенту. Получалось что-то вроде:

void process_object( const some_object_t & original )
  {
    // Делаем копию, которая может модифицироваться.
    some_object_t copy( original );
    // На последующую обработку уходит уже копия объекта.
    do_first_step_then_others( copy );
  }

void do_first_step_then_other( some_object_t & obj )
  {
    // Обработка заключается в замене одного из полей,
    // если его текущее значение не удовлетворяет
    // некоторым условиям.
    if( need_change_field_1( obj ) )
      obj.set_field_1( value_for_field_1() );

    do_second_step_then_others( obj );
  }
...
void do_N_step_then_send( some_object_t & obj )
  {
    if( need_change_field_N( obj ) )
      obj.set_field_N( value_for_field_N() );

    send( obj );
  }

При всей своей простоте это решение мне не нравилось тем, что копия объекта создается всегда. Даже если в 90% случаев объект вообще не модифицировался. А исходный объект был не очень маленьким – с десятком строковых полей + еще несколькими полями со сложной структурой.

Т.е. задача была в том, чтобы создавать копию объекта только при первой модификации. Но, если модификация произошла, чтобы именно копия объекта рассматривалась как актуальное значение. Для этого я написал очень простой класс (его шаблонный вариант приведен здесь). С его использованием преобразование объекта стало выглядеть приблизительно так:

void process_object( const some_object_t & original )
  {
    // Делаем "ленивую" копию, которая может модифицироваться.
    mutable_copy_on_demand_t< some_object_t > copy( original );
    // На последующую обработку уходит уже копия объекта.
    do_first_step_then_others( copy );
  }

void do_first_step_then_others(
  mutable_copy_on_demand_t< some_object_t > & obj )
  {
    // Обработка заключается в замене одного из полей,
    // если его текущее значение не удовлетворяет
    // некоторым условиям.
    if( need_change_field_1( obj.immutable() ) )
      obj.changeable().set_field_1( value_for_field_1() );

    do_second_step_then_others( obj );
  }
...
void do_N_step_then_send(
  mutable_copy_on_demand_t< some_object_t > & obj )
  {
    if( need_change_field_N( obj.immutable() ) )
      obj.changeable().set_field_N( value_for_field_N() );

    send( obj.immutable() );
  }

Объект mutable_copy_on_demand_t хранит ссылку на исходный объект и указатель на копию. Изначально копии нет и метод immutable возвращает ссылку на исходную копию. При первом обращении к методу changeable создается копия, и метод immutable начинает возвращать ссылку на нее.

Вот такой частный случай Copy-On-Write, упрощенный за счет того, что ссылку на исходный объект не нужно контролировать – она в данном сценарии гарантированно остается неизменной.