Pull to refresh

Comments 123

Спасибо за статью, радикальный подход!
Проясните, пожалуйста, следующий момент:
test().setX(1000); // не поменяет исходный объект

Что такое setX на неизменяемом объекте? Какой объект поменяется? Я ожидал увидеть здесь ошибку компиляции, так как не константные методы не должны работать на immutable.
создаётся копия объекта, для копии вызывается setX, копия выходит из контекста и удаляется.
А зачем? Может стоит возвращать константную ссылку, чтобы подобного не происходило? Или это замысел такой?
Могу предположить, что это чтобы легче было использовать парадигмы ФП в с++. Зачем? Спросите автора, я не знаю ;)
Где нужна иммутабельность? Например, в паралелльном программировании — иммутабельность уменьшает количество побочных эффектов. ФП в С++ — boost, в Qt активно используется в фьючерсах, там есть известная концепция: ReduceMap и тд.
Перед тем, как заявлять иммутабельность, стоило бы посмотреть, в какой код преобразуется ваш шаблон, и какие машинные коды из него получатся. \Fa в студии и -S в мингве.

И не забывайте, 99% не заморачиваются const_cast. В лучшем случае сразу reinterpret_sast, но чаще просто (T*).
помните про const_cast

помните про reinterpret_cast, который вертел ваши объекты как ему хочется

Это же темная комната с детскими грабельками.
Смысл в сохранении естественной семантики работы с объектом. setX может что-то вернуть и это что-то может использоваться далее. В С++ пока нету перегрузки оператора. (пока нету, может быть в середине марта будет решение), который позволит реализовывать умные ссылки, поможет делать защитные заместители и декораторы.
Я честно говоря не понял в чем преимущества

Immutable<int> a(1);

перед банальным (и отлично работающим)

const int a(1);

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


const просто запрещает подпрограмме изменять объект — но не дает никаких гарантий. Immutable дает гарантию подпрограмме что снаружи объект также никто не изменит.

Чукча не дуракКомпилятор оптимизирующий, статические константные величины просто будут подставляться в качестве значения. В худшем случае, поместит в секцию .data. А от прямой инъекции в твою память тебя практически ничто не спасёт.

При чем тут инъекция в память?

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

Зачем менять значение, которое декларировано как неизменяемое?

так а если незачем, то «зачем платить больше» и копировать то, что не собираешься изменять?

А кто говорил о копировании? В том-то и смысл, что там где обычную структуру надо копировать — на Immutable-версию можно передавать ссылку.

А теперь посмотрите внимательно на приведённый в публикации код, а именно на operator(), который возвращает копию объекта.


Зачем передавать ссылку на Immutable, а затем при каждом обращении к нему делать копию объекта, если копию можно сделать только один раз, передавая аргументы в функцию по значению? Just KISS.


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

Да там вообще много глупостей. Я отвечал на вопрос "зачем", а не доказывал идеальность конкретной реализации.

еще раз: есть такая нехорошая вешь как const_cast. Посмотрите «Язык программирования D» и перечитайте введение

Незачем. Но если язык позволяет выстрелить себе в ногу, это стоит иметь в виду.

Во-первых, любая попытка изменения константы этапа компиляции — UB. Тут и говорить не о чем.
Во-вторых, есть серьёзные сомнения, что компилятор сможет правильно переварить всю эту мешанину на серьёзных классах. Что получится в итоге, ванговать не имеет смысла
В-третьих, я не вижу 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) меньше побочных эффектов, т.к. работаете с копией.

2) А можно мне не факт компиляции, а сравнение сгенерированного ассемблерного кода для вашей версии и классического подхода?
5) Спасаясь от мутабельности вы нарываетесь на иммутабельность. А я тем временем всё равно не знаю, чем ваша мешанина лучше, чем
int a = 5;
const int& b = a;

http://ideone.com/sOcMvB
Всё, дошло.
5) Ответ самому себе, Immutable сохраняет своё состояние, но, при этом, плодит уймы своих изменённых копий, которые, конечно, могли бы быть выоптимизированны компилятором, но даже если это и произойдёт, на времени построения и перепостроения это скажется фатальным образом.
В целом подход имеет право быть, но я бы не заморачивался константностью внутри класса, и просто бы перегрузил все операторы для Immutable поведения, оставив возможность работать с хранимым значением напрямую.

к 2) оверхед не очень большой, но… давайте обсудим с цифрами. Предоставьте, пожалуйста код, на Ваше усмотрение (для большей объективности), чтобы для него получить выход компилятора.

Зачем его представлять, если вы предлагаете постоянно копировать объекты?
откуда взяться этим побочным эффектам? У вас есть 6 базовых способа передать значение в функцию (=, &, const&, *, const*, &&), их все же не просто так придумали, а для того, чтобы функция работала с данными простым, понятным и максимально эффективным способом
«const просто запрещает подпрограмме изменять объект — но не дает никаких гарантий. Immutable дает гарантию подпрограмме что снаружи объект также никто не изменит.»

Вы не могли бы пояснить свою мысль? Кто может снаружи изменить объект? Другой тред?
Смотрите, вы передаете в другую функцию: внутри можно изменить с помощью const_cast, игры с адресами и т.д. При работе с Immutable вы работаете с копией, а не с оригиналом. Прочитайте главу 8.1 «Язык программирования D», автор А. Александреску.
Иммутабельные данные в параллельном программировании в разы снижают количество ошибок, за счет меньшего числа побочных эффектов.
Не лучше ли просто писать нормальные программы где не используют подобные хаки? Тогда компилятор вежливо предупредит Вас что Вы пытаетесь передать иммутабельный объект в функцию которая может его изменить, а Вы всегда можете поставить в этом месте создание копии, если подобное решение допустимо. Да, это порождает необходимость расстановки кучи const-ов везде где это нужно. Но зато в итоге и получается намного более надежный и понятный код. Ваше решение пытается создавать копии автоматически, но мне честно говоря кажется что это плохая идея. Копий будет создано больше чем нужно, некоторые ошибки в норме выявимые еще на этапе компиляции очень странно проявят себя в рантайме, да и в целом довольно громоздко.
При работе с 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. В Вашем примере есть не константная переменная a и функция foo которая обещает не менять первый из аргументов получаемый по ссылке. Если Вы хотите сделать «иммутабельный объект», то пишете

const int a = 5;
foo(a, a); // Действительно сюрприз, причем от компилятора!


Модификатор const у функции лишь говорит что иммутабельный объект может использоваться в качестве первого аргумента функции. Он не защищает (и не должен защищать) от внешнего по отношению к функции изменению объекта (в данном примере таковым является вызов функции, а не ее тело).
Если Вам нужен иммутабельный объект, то Вы объявляете этот объект иммутабельным. Const int как в моём примере — и всё, дальше Вы получили все гарантии.
Объявление же аргумента функции 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 в C++ умные люди придумали. Если же в приложении бардак и требуется зачем-то постоянно превращать не-immutable состояние в immutable, то таки да, придется плодить копии. Но собственно, immutable<> шаблон в этом ничего не меняет.

Вы все еще подходите со стороны вызывающего кода. Да, там достаточно 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<>, а дальше использовать по ссылке и никаких лишних копирований.

Лишние копирования у вас будут при каждом вызове оператора (). И еще в конструкторе.

Уважаемый ixjxk, я прекрасно понимаю Вашу идею «дополнительного уровня защиты». Вы заставляете вызывающий код создавать immutable-копию каждый раз когда происходит переход от «обычного» кода к коду работающему с immutable-данными, но если весь код работающий дальше использует Ваш шаблон, то дополнительного копирования после этого первого не происходит. Там есть определенные косяки с сеттерами (по хорошему весь класс следовало бы банально сделать эквивалентом const X&) но в целом он работает. Это все понятно и меня честно говоря уже начали раздражать люди которые повторяют одни и те же вещи «человеку не понявшему идеи». Да все я прекрасно понял, спасибо. Для меня эта тема весьма актуальна поскольку, повторю, у меня есть приложение которое обрабатывает гигабайты данных в реальном времени в двадцать потоков которые их шарят между собой. И Вы знаете, за 3 года разработки в команде из 20 человек случаев когда const-защиты не хватило бы не было ни одного. Все они отлавливались компилятором, очень удобно было, причем код кое-где пришлось переписать существенно. Вот с mutable-данными проблемы были и мы, кстати, придумали как довольно неплохо защитить от случайных ошибок и их (правда, уже не на уровне компиляции, а в реал-тайме).

Так вот, возвращаясь к нашим баранам: на основании своего опыта я пытаюсь сказать одну простую, в общем-то, вещь. Функции (да и объекты) не существуют сами по себе, «в вакууме». Они являются частями приложения. И у этого приложения должна быть структура. Это включает в себя внятное понимание того какие данные есть в приложении, как их организовать наиболее удобным образом в объекты и как происходит обработка этих объектов. И уже под эту структуру пишется собственно код. Вопросы времени жизни объекта и того шарится ли объект между разными потоками или нет естественным образом являются частью этой структуры и как правило естественным образом решаются в ее рамках. И тогда описанные выше проблемы которые Вы пытаетесь решать, на уровне функций уже просто не возникают. И оказывается возможным не копировать объекты вообще (у Вас они копируются минимум один раз).

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
В момент превращения объекта в immutablе Вам где-то нужно взять гарантию что все вышележащие уровни, другие потоки и т.п. не имеют mutable-ссылок на этот объект и не будут их использовать. Но если у Вам есть такая гарантия (например от архитектуры) то и шаблон уже не нужен, достаточно объект передавать дальше как const &.
Это не пример практического сценария, это искусственно сконструированный пример проблемы. Хотите я Вам подобный искусственный пример для неаккуратной работы с умными указателями накидаю? При этом при правильном использовании умные указатели работают великолепно и дополнения самописными велосипедами не требуют.

Нет, это упрощенный пример реальной проблемы, с которой я столкнулся 10 лет назад.

Рискну предположить что Ваша проблема несложно и надежно решалась без нужды в шаблонах типа immutable<>. Потому и спрашиваю что хочу разобраться, так ли это, или есть нетривиальный сценарий который стоит предусмотреть и перекрыть.

Да не решается она без прописывания иммутабельности в определении функции! Тут разве что restrict поможет, но его в стандарте нет.

Да, не решается. Но речь вот о чём: я (да и многие другие) ещё не встречал ситуацию, где настолько была бы необходима иммутабельность. Видимо, на C++ люди пишут аккуратно, и им достаточно обычного const.


С иммутабельностью (readonly поля) же я намучался в C#, когда компилятор неявно делал копирование структуры против моей воли, даже не выдавая предупреждений.

Иммутабельность очень удобна для параллельной обработки, особенно в модели акторов, там это действительно сильно помогает. Но const там за глаза хватает если им грамотно пользоваться

Не встречал такого поведения компилятора в 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-типами, не имеющих ограничений.

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

Да, в C# нет разделения методов на const и не-const. А в C++ эта проблема решается одним модификатором const.

Я приводил пример решения, он тривиален: если бы объект был изначально объявлен const, то проблему в Вашем примере отловил бы еще компилятор. И для иммутабельных объектов в приложении обеспечить это в подавляющем большинстве случаев легко.

Или Вы просто хотите использовать иммутабельные объекты не как иммутабельные объекты сами по себе, а как костыль для обхода грубых логических ошибок где один и тот же объект используется для чтения и записи в функции которая не поддерживает in-place обработку?
Вы логическую ошибку так просто банально маскируете.
Это костыль как он есть, assert(input != output) в такой функции на порядок полезнее.

Поясните, почему вы считаете что immutable просто маскирует ошибку.


А assert — это рантайм-проверка. Хотелось бы чтобы компилятор следил.

Я бы посоветовал пытаться из С++ сделать D. Хотите использовать возможности на D, так и пишите на нём. Программистам C++ и так тошно от нагромождения шаблонов.

Потому что Ваша функция исходит из предположения что input != output. Это ее контракт. В момент когда Вы начинаете пользоваться функцией вне ее контракта Вы допускаете ошибку. Возьмите к примеру классификацию функций thread-safe, reentrant и не-reentrant. Если Вы пытаетесь использовать не-реентрабельную функцию из нескольких потоков, то что это будет? Ошибка. Вы нарушили контракт функции и огребете из-за этого проблемы. То же самое и здесь. Но вместо того чтобы проверить что этот контракт соблюдается и исправить соответствующие вызовы, Вы предлагаете каждый раз создавать копию input чтобы даже при нарушении контракта функция продолжала работать. А поскольку делать вручную это лень то Вы считаете хорошим решением обернуть это в хитрый шаблон который сделает эту копию автоматически. В итоге вызов нарушающий контракт не исправлен, а програма плодит без нужды копии объектов, хотя и работает. В моем понимании это «костыль».

Где, ну где вы вычитали что я предлагаю делать копию?! С копией как было бы очень просто — достаточно было бы убрать ссылочность у параметра.

Эм, а что Вы, простите, предлагаете еще делать с функцией из Вашего примера?

Передавать первым параметром ссылку на Immutable… где Immutable — нормальная реализация шаблона, а не как у автора.

Так это и есть создание копии. От того что она будет создана возможно не в месте непосредственного вызова а несколько раньше (там где создавался Immutable) ничего принципиально не меняется.

Можно же обойтись перемещением значения внутрь создаваемого immutable. Перемещение не такое затратное как копия.

Если у Вас есть функция f(in, out) и Вы вызываете f(a,a) то все сломается, в какие бы темплейты это не было завернуто. Работать оно может только в случае вызова f(copy_a, a), независимо от того как именно будет порождена copy_a. Причем если у Вас есть возможность сделать move в новый объект который будет дальше передаваться вместо исходного, то вместо

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 во внешнем коде — но это никак не контролируется. Контракт функции позволяет вызвать ее, передав ей любую переменную. Хоть константную — хоть нет.


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

Я Вам очень подробно расписал что делать с функцией не меняя условий его вызова.Это либо assert и ловим багу в коде который вызывает функцию, либо правим багу возникающую в функции для частного случая in==out. Ни один из этих вариантов не требует модификации ничего кроме кода функции, но вы предпочли их просто проигнорировать. Это общий случай и он гарантирует что все будет работать как надо. Есть еще частный, который вы приводите с move как обоснование того почему ваш код «может и не порождать лишней копии». Этот код требует определенных гарантий во внешнем коде и при наличии этих гарантий тривиально заменяется на использование иммутабельного объекта без нужды в шаблонах.

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

Какую опасную оптимизацию требует мой код?

Ручную расстановку move для потенциально расшаренных объектов.
Но в общем я понял что общаться в Вами бесполезно, Вы просто наугад выбираете одну из моих фраз и пишете по ней какие-то общие слова игнорируя контекст. Вы не видите ничего плохого в том чтобы жить с кодом который (одно из двух) либо вызывает функцию на запись в неожиданный объект, либо не делает копии там где это нужно, а я вижу, и не считаю что вариант «раз где-то без копирования все может сломаться, то будем по умолчанию делать копию везде, но с помощью этого костыля и ручной расстановки 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<immutable<T>*> не очень приятно смотреть. Хотя да, действительно, указатель-то передётся по значению обычно.

Вот именно, указатель — по значению.


Кстати, не вижу проблем с двойной защитой. Почему-то же никого не смущают конструкции вида char const * const * const. Так почему вложенные immutable — это плохо? Если из-за размера, то можно и сократить...

Вы забыли про оператор разыменования *

похоже на 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, первый пункт из стандарта (тоже на память)
шаблонный конструктор НЕ замещает стандартный копирующий конструктор.

И поэтому копирующий конструктор дополнительно помещается как удалённый, потому что возможность копирования иммутабельных данных нам не интересна


immutable(immutable const &) = delete;

restrict не поможет… Это подсказка компилятору для генерации более эффективного когда. Если программист нарушает, то это неопределенное поведение. Максимум, что может сделать компилятор — это выдать предупреждение, и то в тривиальном случае.


N1570 ISO/IEC 9899:201x

Проблема иммутабельных данных (да и вообще ФП) в с++ очень проста: базовые концепции ФП, его краеугольные камни, очень красиво выглядят на бумаге, но их «честная» реализация накладывает ощутимый оверхед, причем на любые операции. С++ — не тот язык, на котором решают задачи, где такое допустимо.
В С++17 вместо return Immutable(a.value() + b); можно записать return Immutable(a.value() + b);

Я похоже невнимальный, но в двух выделенных участках я не вижу различий.

Не могли бы вы рассказать, зачем здесь 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)
        , 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);
    }

Только все-таки *static_cast<Base const*>(this).

Если причесать, то можно так:
return *this;

Sign up to leave a comment.

Articles