Привет, Хабр! Об иммутабельных данных немало говориться, но о реализации на С++ найти что-то сложно. И, потому, решил данный восполнить пробел в дебютной статье. Тем более, что в языке D есть, а в С++ – нет. Будет много кода и много букв. О стиле – служебные классы и метафункции используют имена в стиле STL и boost, пользовательские классы в стиле Qt, с которой я в основном и работаю.  Что из себя представляют иммутабельные данные? Иммутабельные данные – это наш старый знакомый const, только более строгий. В идеале иммутабельность означает контекстно-независиую неизменяемость ни при каких условиях. По сути иммутабельные данные должны: Иммутабельные данные пришли из функционального программирования и нашли место в параллельном програмировании, т. к. гарантируют отсутсвие побочных эффектов. Как можно реализовать иммутабельные данные в С++?
В С++ у нас есть (сильно упрощенно): Функции и void не имеет смысл делать иммутабельными. Ссылки тоже не будем делать иммутабельными, для этого есть const reference_wrapper. Что касается остальных вышеперечисленных типов, то для них можно сделать обертки (а точнее нестандартный защитный заместитель). Что будет в итоге? Цель сделать как-бы модификатор типа, сохранив естественную семантику для работы с объектами данного типа.  Общий интерфейс прост – всю работу выполняет базовый класс, который выводится из характеристик (traits): Запрещая оператор присваивания, мы запрещаем перемещающий оператор присваивания, но не запрещаем перемещающий конструктор. immutable_impl что-то вроде switch, но по типам (не стал делать такой – слишком усложняет код, да и в простом случае он не особо нужен – ИМХО). В качестве ограничений явно запретив все операции присваивания (макросы помогают): А теперь давайте рассотрим как реализованы отдельные компоненты. Под значениями (далее value) понимаются объекты фундаментальных типов, экземпляры классов (структур, объединений), перечислений. Для value у на есть класс, который определяет является ли тип классом, структурой или объединением: Если да, то для реализации используется используется CRTP: К сожалению, в С++ пока нет перегрузки оператора .. Хотя, это ожидается в С++ 17 (http://open-std.org/JTC1/SC22/WG21/docs/papers/2016/p0252r0.pdf, http://open-std.org/JTC1/SC22/WG21/docs/papers/2016/p0252r0.pdf, http://www.open-std.org/JTC1/SC22/wg21/docs/papers/2015/p0060r0.html), но вопрос еще открыт, ибо коммитет нашел нестыковки.
Тогда бы можно было просто написать: Но решение по этому вопросу ожидается в марте, поэтому для этих целей пока используем оператор (): Обратите внимание, на конструктор:~~ там инициализируется как immutable_value, так и базовый класс. Это позволяет осмысленно манипулировать с immutable_value через operator (). Например: Если же тип является встроенным, то реализация будет один-в-один, за исключением базового класса (можно было бы изъвернуться, чтобы соответствовать DRY, но как-то не хотелось усложнять, тем более, что immutable_value делался после остальных...): Пока вроде бы просто и неинтересно, но теперь примемся за массивы. Надо сделать что-то вроде std::array сохранив естественную семантику работы с массивом, в том числе для работы с STL (что может ослабить иммутабельность). Особенность релизации заключается в том, что при обращении по индексу к многомерному возвращается массив меньшей размерности, тоже иммутабельный. Тип массива рекурсивно инстанцируется: см. operator[], а конкретные типы для итераторов и т.д выводятся с помощью array_traits. Для определения типа меньшей размерности используется класс характеристик: который для многомерных массивов для при индексировании возвращает иммутабельный массив меньшей размерности. Операторы сравнения очень просты: Для работы с иммутабельным массивом используется иммутабельный итератор array_iterator: Для многомерных массивов все тоже самое: Попробуем слегка обезопасить указатели. В этом разделе рассмотрим обычные указатели (raw pointers), а далее (сильно далее) рассмотрим smart pointers. Для smart pointers будет использоваться SFINAE. По реализации immutable::pointer скажу сразу, что pointer не удаляет данные, не считает ссылки, а только обеспечивает неизменяемость объекта. (Если переданный указатель изменен или удален из-вне, то это нарушение контракта, которое средствами языка не отследить (стандартными средствами)). В конце-концов, защититься от умышленного вредительства или игры с адресами невозможно. Указатель должен быть корректно инициализирован. immutable::pointer может работать с указателями на указатели любой степени ссылочности (скажем так). Например: Кроме вышеперечисленного, immutable::pointer не поддерживает работы со строками в стиле С: Данный код будет работать не так как ожидается, т.к. immutable::pointer при инкременте возвращает новый immutable::pointer с другим адресом, а в условном выражении будет проверяться результат инкремента, т.е. значение второго символа строки. Вернемся к реализации. Класс pointer предоставляет общий интерфейс и, в зависимости от того что из себя представляет Tp (указатель на указатель или прото указатель) использует конкретную реализации pointer_impl. Суть следующая: был тип T  , а для его хранения/представления используется (шаблонно-рекурсивно) реализация pointer_impl<T , true>, что можно изобразить так: Итого, получается: const T const const *const.  Для простого указателя (который не указывает на другой указатель) реализация следующая: Для вложенных указателей (указатели на указатели): Для следующих видов указателей особого смысла не стоит делать специализации: Как определить что перед нами smart pointer? Smart pointers реализуют операторы * и ->. Чтобы определить их наличие воспользуемся SFINAE (реализацию SFINAE рассмотрим позже): Скажу сразу, что через operator ->, увы, используя косвенное обращение, можно нарушить иммутабельность, особенно если в классе есть mutable данные. Кроме того константность возвращаемого значения может быть снята, как компилятором (при выводе типа), так и пользователем. Реализация – здесь все просто: Что это такое и с чем его едят лишний раз объяснять не надо. С помощью SFINAE можно определить наличие в классе методов, типов-членов и т.д, даже наличие перегруженных функций (если задать в выражении testexpr вызов нужной функции с необходимыми параметрами). arg может быть пустым и не участвовать в testexpr. Здесь используется SFINAE с типами и SFINAE с выражениями: И еще: перегрузку можно разрешить (найти нужную перегруженную функцию) если сигнатуры совпадают, но отличаются квалификатором const [ volatile ] или volatile совместно с SFINAE в три фазы: 1) SFINAE — если есть, то ОК
2) SFINAE + QNonConstOverload, если не получилось, то
3) SFINAE + QConstOverload В исходниках Qt можно найти интересную и полезную вещь: Попробуем что получилось:  Давате вспомним про операторы! Например, добавим поддержку оператора сложения:
Сначала реализуем оператор сложения вида Immutable<Type> + Type: В С++17 вместо можно записать  Т.к. оператор + коммутативен, то Type + Immutable<Type> можно реализовать в виде:  И снова, через первую форму реализуем Immutable<Type> + Immutable<Type>: Теперь можем работать: Аналогично можно определить остальные операции. Вот только не надо перегружать операторы получения адреса, &&, ||! Унарные +, -, !, ~ могут пригодиться… Эти операции наследуются: (), [], ->, ->, (унарный).  Операторы сравнения должны возвращать значения булевского типа: