Comments 123
Проясните, пожалуйста, следующий момент:
test().setX(1000); // не поменяет исходный объект
Что такое setX на неизменяемом объекте? Какой объект поменяется? Я ожидал увидеть здесь ошибку компиляции, так как не константные методы не должны работать на immutable.
Immutable<int> a(1);
перед банальным (и отлично работающим)
const int a(1);
Насколько я понял, разница проявляется при передаче объекта по ссылке в подпрограмму.
const просто запрещает подпрограмме изменять объект — но не дает никаких гарантий. Immutable дает гарантию подпрограмме что снаружи объект также никто не изменит.
При чем тут инъекция в память?
Если программист захочет изменить неизменяемое значение, он это сделает, C++ его в этом не ограничивает. Единственный способ не допустить этого — передавать данные в функции по значению.
Зачем менять значение, которое декларировано как неизменяемое?
А кто говорил о копировании? В том-то и смысл, что там где обычную структуру надо копировать — на Immutable
-версию можно передавать ссылку.
А теперь посмотрите внимательно на приведённый в публикации код, а именно на operator()
, который возвращает копию объекта.
Зачем передавать ссылку на Immutable, а затем при каждом обращении к нему делать копию объекта, если копию можно сделать только один раз, передавая аргументы в функцию по значению? Just KISS.
Кстати, просто передавая константный объект по ссылке, у нас есть возможность вызывать методы без копирования объекта.
Незачем. Но если язык позволяет выстрелить себе в ногу, это стоит иметь в виду.
Во-вторых, есть серьёзные сомнения, что компилятор сможет правильно переварить всю эту мешанину на серьёзных классах. Что получится в итоге, ванговать не имеет смысла
В-третьих, я не вижу T* operator&() = delete;, так что ничто не помешает навернуть константность через (void*) (Кроме стандарта, но разве он написан не для того, чтобы мы его нарушали?)
В-четвёртых, даже если выполнить предыдущий пункт, всё ещё можно прогибать на синонимичный шаблон без const через reinterpret_cast.
В-пятых, для меня до сих пор не ясно, каким местом это безопаснее и нагляднее, чем const& или const*const. Ну да, выглядит дишнее. Ну да, типа нагляднее. И? Мы пришли на плюсах программировать или делать из них очередное manageбожество?! Быть может, не стоит городить то, что всё равно не сработает?
1) иммутабельные данные могут обрабатываться в разных потоках без проблем, за счет меньшего числа побочных эффектов.
2) может. Такое устроит (это из будущей статьи):
QVector<Immutable<int>> imm = {
Immutable<int>(1),
Immutable<int>(2),
Immutable<int>(3),
Immutable<int>(4),
Immutable<int>(5),
};
Stream<decltype(imm)> t(imm);
qDebug() << t.map(static_cast<QString(*)(int, int)>(&QString::number), 2)
.filter(std::bind(
std::logical_not<>(),
std::bind(QString::isEmpty, std::placeholders::_1))
)
.map(&QString::length)
.filter(std::greater<>(), 1)
.filter(std::bind(
std::logical_and<>(),
std::bind(std::greater_equal<>(), std::placeholders::_1, 1),
std::bind(std::less_equal<>(), std::placeholders::_1, 100)
)
)
.reduce(std::multiplies<>(), 1);
3) в конце статьи упомянуто, что такое не надо делать;
4) манипулируя с адресом можно сделать все что угодно. Даже в Java через механизм рефлексии можно напакостить;
5) меньше побочных эффектов, т.к. работаете с копией.
5) Спасаясь от мутабельности вы нарываетесь на иммутабельность. А я тем временем всё равно не знаю, чем ваша мешанина лучше, чем
int a = 5;
const int& b = a;
http://ideone.com/sOcMvB
Всё, дошло.
В целом подход имеет право быть, но я бы не заморачивался константностью внутри класса, и просто бы перегрузил все операторы для Immutable поведения, оставив возможность работать с хранимым значением напрямую.
Вы не могли бы пояснить свою мысль? Кто может снаружи изменить объект? Другой тред?
Иммутабельные данные в параллельном программировании в разы снижают количество ошибок, за счет меньшего числа побочных эффектов.
При работе с Immutable вы работаете с копией, а не с оригиналом
Так чем это лучше простой передачи по значению?
Главное отличие immutable от const: преобразование &T -> const &T можно сделать неявно, а &T -> immutable &T — нет. Собственно, автор и попытался сделать враппер для эмуляции функционала D.
Обратный вызов. Или сама функция:
void foo(const int& a, int& b) {
b = 42;
std::cout << a << std::endl;
}
//...
int a = 5;
foo(a, a); // Сюрприз!
Не понял к чему это: вы меняете неконстантную ссылку
Внутри foo
ссылка a
— константная. Но это не помешало ей внезапно измениться в процессе выполнения foo
.
const int a = 5;
foo(a, a); // Действительно сюрприз, причем от компилятора!
Модификатор const у функции лишь говорит что иммутабельный объект может использоваться в качестве первого аргумента функции. Он не защищает (и не должен защищать) от внешнего по отношению к функции изменению объекта (в данном примере таковым является вызов функции, а не ее тело).
А я что говорю?
Объявление же аргумента функции const не делает лежащий за ним объект иммутабельным.
А что делать если функция должна принять иммутабельный параметр по ссылке-то?
void foo(const int& a, int& b) {
const immutable_a(a); // черт его знает что нам передали
b = 42;
std::cout << immutable_a << std::endl;
}
//...
int a = 5;
foo(a, a); // Работает!
А если требуется избежать копирования?..
Вы все еще подходите со стороны вызывающего кода. Да, там достаточно const.
Но для вызываемого кода константная ссылка не гарантирует иммутабельность.
Объект предоставляет вызывающий код, ему и обеспечивать его иммутабельность.
Собственно шаблон автора ничего в этом подходе не меняет. От слова «совсем». Экземпляр immutable<> будет создавать вызывающий код и при этом переход от «обычного» объекта к immutable потребует создания копии
Мои, самые вдумчивые читатели, верным путем идете, товарищи, вот пища для ума, основанная на Ваших примерах:
void foo(const Immutable<int> &a, Immutable<int> &b)
{
a = 42;
b = 42;
a = b;
b = a;
const_cast<Immutable<int>&>(a) = 100;
a = Immutable<int>(500);
b = Immutable<int>(500);
a = std::move(Immutable<int>(500));
b = std::move(Immutable<int>(500));
}
Каждое присваивание даст ошибку компиляции. Шаблон Immutable<> дает еще один уровень защиты.
Можно один раз объект обернуть в Immutable<>, а дальше использовать по ссылке и никаких лишних копирований.
Лишние копирования у вас будут при каждом вызове оператора ()
. И еще в конструкторе.
Так вот, возвращаясь к нашим баранам: на основании своего опыта я пытаюсь сказать одну простую, в общем-то, вещь. Функции (да и объекты) не существуют сами по себе, «в вакууме». Они являются частями приложения. И у этого приложения должна быть структура. Это включает в себя внятное понимание того какие данные есть в приложении, как их организовать наиболее удобным образом в объекты и как происходит обработка этих объектов. И уже под эту структуру пишется собственно код. Вопросы времени жизни объекта и того шарится ли объект между разными потоками или нет естественным образом являются частью этой структуры и как правило естественным образом решаются в ее рамках. И тогда описанные выше проблемы которые Вы пытаетесь решать, на уровне функций уже просто не возникают. И оказывается возможным не копировать объекты вообще (у Вас они копируются минимум один раз).
C++ в этом отношении довольно специфичный язык. Он очень сильно заточен на то чтобы работать с приложениями наделенными подобной структурой. Если же подобной структуры в приложении нет то… в плюсах есть сто тысяч и один способ выстрелить себе в ногу и те кто пытаются халтурить с плюсовым кодом быстро познают эту истину. А затем начинается попытка «исправить плохой C++ шаблонами» дописав туда функциональность которая один-два подобных способа перекрывает. Мне это представляется плохой идеей, блуждая вслепую Вы наступите не на одни грабли так на другие. Для написания подобного «бесструктурного» кода лучше подходит C#, а не C++ и холивар на тему GC тому ярким свидетельством: «умные указатели» в плюсах как и все остальное сильно увязано на наличие у приложения структуры.
Предчувствую очередной минус, но для 0serg за его труд и аргументированность отвечу.
1) Цель статьи рассказать про концепт ФП — иммутабельные данные. Про Map, Reduce, Filter, карринг и т.п. много написано, но про иммутабельные данные, которые в ФП являются одним из краеуголных камней, сложно что-то найти. Про операции над списками много статей, а реализации некоторых дают немалый оверхед.
2) ФП в С++ смотрится странно, а некоторые его концепты вызывают недоумение. Но и они находят свое примение.
3) D не мой основной язык, просто в стиле "Практический подход к решению проблем программирования C++" (Автор: Мэтью Уилсон) предложил как можно реализовать immutable.
4) Почему-то все комментарии прочитать про D заминусованы, хотя из D в С++ уже пришли шаблоны с переменным числом аргументов, static_assert. В С++ 17 придет constexpr if. Да, то из D, в адаптированной для С++ форме.
В книге "Язык программирования D" от Александреску очень хорошо написано про иммутабельные данные (глава 8).
5) Теперь по сути. Все стали критиковать недостатки value. А достоинства шаблона Immutable<> как-то ускользнули. К value я вернусь попозже.
Например, использование к указателям.
void foo(Immutable<int*> &a)
{
int *c = new int(100500);
Immutable<int*> b(c);
*a = 0; // error
a = b; //error
//...
}
Оверхед от Immutable<> меньше чем от smart pointers. А для компиляторов, которые устраняют лишнее копирование (стандартное требование в С++17), его либо нет, либо очень мал. Потому что, шаблонная обертка выкидывается Gimplifier (это под капотом gcc) в gcc.
6) Почему оставлены модифицирующие функции для value?
- Функция может что-то возвращать что-то интересное для нас (заминусовали);
- Сохранение семантики (заминусовали);
- Некоторый контекст (какая-нибудь библиотека) может потребовать наличия данной функции;
- Некоторые функции в какой-то библиотеке (которую нельзя менять) не меняют объект (явно), но не объявлены константными (разгильдяйство, старый код и т.д.)
- В общем случае эти функции сложно запретить (не будем же нежелательные функции убирать в private и там писать using Class::function). Более подробно расскажу в примере с заместителем:
- нужно сделать класс (защитный заместитель), или еще лучше что-то вроде шаблона optional (C++ 17, см. предложения Страуструпа), который должен быть похож на замещаемый класс. Вот здесь в дело и идет CRTP плюс перегрузка оператора. (за его отсутствие у меня используется ()).
Если нужны более объемные примеры, то поищу (у меня сейчас делается еще одна статья, плюс готовится цикл статей по написанию собственного компилятора в доступном изложении).
Мне кажется, что минусуют, потому что получилось очень и очень криво. Нужно провести большую работу над ошибками.
Вот лично мне вот этот кусок не нравится:
int *c = new int(100500);
Immutable<int*> b(c);
Объясняю: после объявления immutable<int*> b(c)
у нас остался доступ к значению через c
. Здесь никакой иммутабельностью и не пахнет. Можно было бы просто написать const int *b = c;
Единственный допустимый вариант:
Immutable<int*> b(100500);
6) Почему оставлены модифицирующие функции для value?
Явное лучше неявного. Если функция возвращает что-то интересное, но может изменить значение, программист должен явно написать что-то типа b.copy().some_method();
.
Также мне не нравится здесь CRTP. Понимаю, что он сделан для замены оператора точка, но без него можно прекрасно обойтись. Есть оператор ->
, который удобно использовать, когда оборачивается указатель (не меняется семантика). Если же оборачивается значение, тогда придётся менять .
на ->
. Но смысла в оборачивании значения нет, т.к. переменная будет копироваться при вызове функции — значит, можно просто передать аргумент по значению без всяких извратов.
Не важно сколько интересного возвращает функция, но если она меняет данные — ее нельзя вызывать на неизменяемых данных. Это очевидно.
Аналогично с библиотеками. Если библиотека требует наличия функции, которая меняет данные — значит, она не поддерживает неизменяемые данные.
Легаси и разгильдяйство прекрасно лечится при помощи const_cast
. Да, const_cast
есть зло — но это зло хотя бы будет локализовано в проблемных местах, а не будет заложено в архитектуру, как это сделали вы.
Запретить такие функции — проще простого. Надо просто добавить const!
Оверхед от Immutable<> меньше чем от smart pointers.
unique_ptr как правило не имеет оверхеда вообще (покрывая при этом более 90% потребностей)
shared_ptr мало где нужен и при правильном использовании имеет хотя и не-нулевой, но пренебрежимо малый по сравнению с временем на копирование сколь-либо крупного объекта оверхед
А для компиляторов, которые устраняют лишнее копирование (стандартное требование в С++17), его либо нет, либо очень мал
В общем случае Вы не можете сделать потенциально mutable объект immutable не создав его копию (т.к. исходный объект может кто-то в любой момент изменить). Поэтому оверхед на создание копии которая будет храниться в immutable<> будет всегда. С остальным кодом порождающим копии — как повезет.
Цель статьи рассказать про концепт ФП — иммутабельные данные.
Так я не против иммутабельных данных, я их очень широко использую и люблю. Вот отличный иммутабельный int:
const int i = 5;
Я пытаюсь понять есть ли практические сценарии где может быть востребовано автоматическое порождение immutable-данных шаблоном типа Вашего вместо явного порождения immutable-объекта в коде, но к сожалению никто из минусующих не удосужился хотя бы одного подобного примера привести.
В общем случае Вы не можете сделать потенциально mutable объект immutable не создав его копию (т.к. исходный объект может кто-то в любой момент изменить)
ничто не мешает написать make_immutable
Нет, это упрощенный пример реальной проблемы, с которой я столкнулся 10 лет назад.
Да не решается она без прописывания иммутабельности в определении функции! Тут разве что restrict поможет, но его в стандарте нет.
Да, не решается. Но речь вот о чём: я (да и многие другие) ещё не встречал ситуацию, где настолько была бы необходима иммутабельность. Видимо, на C++ люди пишут аккуратно, и им достаточно обычного const.
С иммутабельностью (readonly поля) же я намучался в C#, когда компилятор неявно делал копирование структуры против моей воли, даже не выдавая предупреждений.
Не встречал такого поведения компилятора в C#. Можно пример? Кстати, readonly поля — это еще не иммутабельность.
Можно.
class SomeClass<T>
where T : SomeInterface
{
readonly T field;
}
Если T — value type, то при вызове методов интерфейса у field
каждый раз будет неявно создаваться копия объекта, а не вываливаться ошибка, что так делать нельзя. Если убрать readonly, то всё будет корректно.
Но это же логично. Какого же еще поведения вы ожидали вызывая методы у readonly-структуры?..
Ожидал увидеть ошибку компиляции, что нельзя так делать.
Почему readonly-объект в C# можно менять, выззывая метода, а структуру нет? Нелогично.
Вы упускаете тот факт, что компилятор не знает является ли тип T ссылочным или значимым...
Да, не знает, потому T — generic без class/struct constraint. Но можно было бы просто сделать ошибкой использование readonly в сочетании с generic-типами, не имеющих ограничений.
Или Вы просто хотите использовать иммутабельные объекты не как иммутабельные объекты сами по себе, а как костыль для обхода грубых логических ошибок где один и тот же объект используется для чтения и записи в функции которая не поддерживает in-place обработку?
Нет, я их хочу использовать для предотвращения подобных ошибок!
Это костыль как он есть, assert(input != output) в такой функции на порядок полезнее.
Поясните, почему вы считаете что immutable просто маскирует ошибку.
А assert — это рантайм-проверка. Хотелось бы чтобы компилятор следил.
Я бы посоветовал пытаться из С++ сделать D. Хотите использовать возможности на D, так и пишите на нём. Программистам C++ и так тошно от нагромождения шаблонов.
Где, ну где вы вычитали что я предлагаю делать копию?! С копией как было бы очень просто — достаточно было бы убрать ссылочность у параметра.
Передавать первым параметром ссылку на Immutable
… где Immutable — нормальная реализация шаблона, а не как у автора.
Можно же обойтись перемещением значения внутрь создаваемого immutable. Перемещение не такое затратное как копия.
immutable<T> b = move(a)
гораздо практичнее «создать иммутабельный объект»
const T& b = a;
или в крайнем случае
const T b = move(a);
… и он будет работать во всех тех же ситуациях где работал бы исходный вариант с move. Минус темплейты, плюс невозможность скомпилировать пример с f(a,a).
Вы опять рассуждаете про внешний код! Забудьте про него, вы пишите только функцию. Как вы будете обеспечивать невозможность компиляции f(a, a)
?
Можно же обойтись перемещением значения внутрь создаваемого immutable
Это перемещение может сделать только внешний код
Как вы будете обеспечивать невозможность компиляции f(a, a)?
Да никак это на этапе компиляции в описанных исходных условиях невозможно сделать. Как собственно и в варианте с Immutable который скомпилируется и породит копию которая в большинстве случаев будет не нужна.
Как поступить правильно если у нас есть подобная функция? Я это уже написал. Либо мы считаем что in и out должны быть разными (это является частью контракта) и пишем assert(in != out) и тестированием проверяем что мы его не триггерим. Либо мы допускаем что in и out могут быть одинаковыми и закладываться на обратное мы не можем и переписываем f(a,b) так чтобы она правильно работала и в случае a==b. Один из возможных примеров подобного переписывания:
void foo(const X &a, X &b)
{
std::unique_ptr<X> local_a
if (&a == &b)
local_a.reset(new X(a));
const X& a_to_use = (&a == &b) ? *local_a : a;
// work with a_to_use
}
Причем в отличие от темплейта этот пример породит копию только там где это действительно нужно. В чем плюс от шаблона-то?
Это перемещение может сделать только внешний код
И его сделает внешний код. В чем проблема-то?
Вы опять рассуждаете про внешний код! Забудьте про него, вы пишите только функцию
Может Вы перестанете ходить кругами? А то у Вас когда Вам хочется внешний код может сделать move, но когда я пишу Вам что в этом случае операцию move можно заменить на приведение к const то у Вас сразу оказывается что внешний код трогать нельзя. Определитесь уже.
Вы понимаете что такое "контракт функции"?
Вы предлагаете писать const во внешнем коде — но это никак не контролируется. Контракт функции позволяет вызвать ее, передав ей любую переменную. Хоть константную — хоть нет.
Я предлагаю ввести тип данных, который будет гарантировать неизменность. Во внешнем коде может использоваться копирование — а может использоваться перемещение, на выбор. Но независимо от того что выберет программист, который пишет внешний код — внутри функции параметр меняться не будет.
Давайте для простоты рассмотрим общий случай. Мой код не порождает копий, не требует шаблонов, не требует изменения сигнатуры функции, гарантированно работает всегда. Ваш требует шаблонов, порождает ненужные копии и требует ручной (и опасной в вашем понимании) оптимизации для того чтобы их избежать. Вроде бы очевидно какой из этих вариантов заведомо лучше, разве нет?
Какую опасную оптимизацию требует мой код?
Но в общем я понял что общаться в Вами бесполезно, Вы просто наугад выбираете одну из моих фраз и пишете по ней какие-то общие слова игнорируя контекст. Вы не видите ничего плохого в том чтобы жить с кодом который (одно из двух) либо вызывает функцию на запись в неожиданный объект, либо не делает копии там где это нужно, а я вижу, и не считаю что вариант «раз где-то без копирования все может сломаться, то будем по умолчанию делать копию везде, но с помощью этого костыля и ручной расстановки move по коду часть этих копий уберем» является оправданным решением этой проблемы. Давайте закончим на этом.
Приведите, пожалуйста пример нормальной реализации шаблона Immutable.
Хотя бы для значений… \
И не обязательно по букве и духу ФП. Хотя желательно, чтобы с "завернутым" объектом можно было работать как с оригинальным.
Заранее благодарен.
template<typename T> class immutable {
private:
T const value;
public:
immutable(immutable const &) = delete;
void operator = (immutable const &) = delete;
template<typename... Args> immutable(Args&&... args)
: value(std::forward<Args>(args)...) { }
T const & operator () () const {
return value;
}
T const * operator -> () const {
return &value;
}
}
Если я ничего не напутал — этого должно быть достаточно.
Особые специализации не нужны — всегда можно добавить круглые скобки и получить ссылку на базовый объект.
Рекурсивная иммутабельность тоже не нужна — всегда можно расставить вложенные immutable вручную.
Вот это мне уже нравится. А особая специализация нужна для T*
: чтобы была возможность в конструкторе не указывать new
и вызывать методы не как ()->
, а сразу ->
:
template<typename T> class immutable<T*> {
private:
std::unique_ptr<T> const value;
public:
immutable(immutable const &) = delete;
void operator = (immutable const &) = delete;
template<typename... Args> immutable(Args&&... args)
: value(new T(std::forward<Args>(args)...)) { }
T const * operator () () const {
return value.get();
}
T const * operator -> () const {
return value.get();
}
}
У вас в вашем иммутабельном указателе таки появилась рекурсивная иммутабельность, что является усложнением.
Для простых указателей вообще нет смысла использовать иммутабельную обертку, проще указатель копировать.
У вас в вашем иммутабельном указателе таки появилась рекурсивная иммутабельность, что является усложнением.
А разве это не является целью? Или под иммутабельностью понимается исключительно защита поля (указателя), а не защита содержимого, на который он указывает?
Если только первое, тогда иммутабельность при работе с указателями абсолютно бессмыслена.
Защитить содержимое довольно просто: immutable<T>*
А защитить и содержимое, и поле? На вариант immutable<immutable<T>*>
не очень приятно смотреть. Хотя да, действительно, указатель-то передётся по значению обычно.
Вы забыли про оператор разыменования *
похоже на std::reference_wrapper…
у Вас появилось копирование, против которого уже негативно высказывались
Где копирование?
спасибо за минус
К https://habrahabr.ru/post/322208/#comment_10085776
Копирование появилось в шаблонном конструкторе: инициализирующее значение, копируется или перемещается в value.
Копирование появилось в шаблонном конструкторе: инициализирующее значение копируется или перемещается в value.
Неверно. Всё, что делается в конструкторе — это perfect forwarding аргументов в вызов соотвествующего конструктора value
.
std::forward приводит свой аргумент к rvalue только тогда, когда этот аргумент связан с rvalue. В этом случае value буден сконструировано через вызов конструктора перемещения.
В противном случае value будет сконструировано через конструктор копии.
Если Вы мне не верите, то предлагаю убедиться: С. Майерс "Современный и эффективный С++", главы 5 и 8
А что неправильно-то?
immutable<Foo> f(x); // копируем
immutable<Foo> f(std::move(x)); // перемещаем
immutable<Foo> f(getFoo()); // тоже перемещаем
immutable<Foo> f(1, 2, 3, 4, 5); // создаем на месте
Один шаблонный конструктор покрывает сразу все ситуации...
Вот пример:
#include <QDebug>
class Test {
public:
Test()
{
qDebug() << __PRETTY_FUNCTION__;
}
Test(const Test &)
{
qDebug() << __PRETTY_FUNCTION__;
}
Test(Test &&)
{
qDebug() << __PRETTY_FUNCTION__;
}
private:
};
template<typename T> class immutable
{
private:
T const value;
public:
immutable(immutable const &) = delete;
void operator = (immutable const &) = delete;
template<typename... Args> immutable(Args&&... args)
: value(std::forward<Args>(args)...)
{
qDebug() << __PRETTY_FUNCTION__;
}
T const & operator () () const {
return value;
}
T const * operator -> () const {
return &value;
}
};
int main(int argc, char *argv[])
{
Test t1;
Test t2;
immutable<Test> a(t1);
immutable<Test> b(std::move(t2));
return 0;
}
Результат:
Test::Test()
Test::Test()
Test::Test(const Test&)
immutable::immutable(Args&& ...) [with Args = {Test&}; T = Test]
Test::Test(Test&&)
immutable::immutable(Args&& ...) [with Args = {Test}; T = Test]
Ну да, все ожидаемо. a(t1)
копирует, а через std::move
— не копирует. А вам как надо?
С вопросом где копирование мы разобрались.
Я про то, что так или иначе копию делать придется, а за лишние копирование, среди комментов было высказано немало неласковых слов.
Во-первых, есть огромная разница между однократным копированием и копированием при каждом обращении.
Во-вторых, есть варианты полностью избежать копирования: перемещение и создание объекта на месте через конструктор.
Зачем делать копию, когда можно сконструировать объект на месте?
class Test {
public:
Test(int a, int b)
...
immutable<Test> c(1, 2)
И еще: в С++ есть тонкость:
- шаблонный конструктор НЕ замещает стандартный копирующий конструктор. При точном соответствии типов будет вызван обычный копирующий конструктор.
- шаблонный конструктор никогда не используется для генерации обычного конструктора копий, при его отсутствии, он будет сгенерирован неявно (если только явно не запрещен).
Если память не изменяет, Страуструп, "Язык программирования С++" 4 издание, глава 13, первый пункт из стандарта (тоже на память)
restrict не поможет… Это подсказка компилятору для генерации более эффективного когда. Если программист нарушает, то это неопределенное поведение. Максимум, что может сделать компилятор — это выдать предупреждение, и то в тривиальном случае.
N1570 ISO/IEC 9899:201x
В С++17 вместо return Immutable(a.value() + b); можно записать return Immutable(a.value() + b);
Я похоже невнимальный, но в двух выделенных участках я не вижу различий.
template <typename Base>
class immutable_value<Base, true> : private Base
{
public:
using value_type = Base;
constexpr explicit immutable_value(const Base &value)
: Base(value)
, m_value(value)
{
}
// ...
constexpr Base value() const
{
return m_value;
}
Мне кажется, что так было бы и проще, и на одну копию меньше:
template <typename Base>
class immutable_value<Base, true> : private Base
{
public:
using value_type = Base;
constexpr explicit immutable_value(const Base &value)
: Base(value)
{
}
// ...
constexpr Base value() const
{
return static_cast<Base>(*this);
}
Иммутабельные данные в C++