Для вас это всего лишь история, для меня -- собственный опыт.
но период до шаблонов вообще можно не считать потому что это не с++, а фигня какая-то, которой пользовались 3 калеки.
Чуть больше, по некоторым оценкам количество программистов на C++ в 1991-ом году оценивалось где-то в 400K.
Ну и эта самая "фигня какая-то" была сильно круче тогдашних конкурентов (коих и не сказать, чтобы было много на конец 1980-х и начало 1990-х).
Забавно, что для некоторых C++ в то время не был отдельным языком программирования, а воспринимался просто как следующая версия Си, в который добавили пару-тройку новых ключевых слов. Тем более, что одним из "selling points" C++ всегда было то, что можно было взять уже имеющийся код на Си и с минимальными переделками начать использовать его как код на C++ (собственно перевод кодовой базы GCC на C++ как раз лишний тому пример). Даже без классов и перегрузки операторов. Просто ссылки вместо указателей.
Так что если бы C++ не выглядел в начале своей жизни именно как нашлепка над Си, то вряд ли он обрел бы такую популярность. Мог бы запросто повторить судьбу Eiffel-я, а то и Modula-3 (при том, что Eiffel не умер, живет себе и здравствует, просто мало кому нужен).
Изначально их не было. Они появились в C++ только в 1990-ом году, когда язык уже стал популярным за счет своей совместимости с Си.
Более того, шаблонов в C++ так же изначально не было. И выкручиваться с обобщенным программированием на C++ тогда приходилось за счет Си-шного препроцессора с define-ами.
Да, и т.е. спустя сколько там 40 лет комитет таки запили модули и ничего не отвалилось.
Об этом рано говорить, модули пока что используют не только лишь все.
Мало того он и сам из хотел сделать, но типа и так пойдет.
Он и auto изначально хотел использовать для автоматического вывода типов. Но "совместимость" с Си не позволила.
Си здесь вообще не причем.
Ну да, ну да. Взлетел бы C++ если бы он бы не помесью Симула и Си, а помесью Симула и Модула-2 (к примеру) -- большой вопрос. По факту же C++ смог несколько десятилетий жить за счет экосистемы Си (начиная от линкеров и форматов объектных файлов, заканчивая бесплатной интеграцией с Си-шным кодом). Как-то странно слышать, что "Си здесь вообще ни при чем".
Но давайте признаем, что виноват на самом деле страус, изобретая с++ он почему то решил что модули это для слабоков
Страуструп был вынужден использовать имевшийся тогда Си, в котором никаких модулей не было. А почему был выбран Си тот же Страуструп многократно и подробно рассказывал, емнип, в "Дизайн и эволюция языка C++".
Следование принципу DRY. У вас, по сути, close и деструктор должны приводить к одинаковым результатам. Поэтому логичным выглядит не повторять логику в разных местах, а выражать одно через другое.
Кстати говоря, странно, почему вы оператор перемещения через move_assign делаете, а close -- нет. Можно же close сделать так:
И получить те же самые бенефиты, на которых вы зациклены: про добавлении нового поля в класс не нужно делать лишние операции.
Чтобы делать флуширование, необходим доступ к buffer и buffer_pos объекта OFile. Т.е. в ваш handle_holder помимо самого handle, необходимо добавить указатель на объект OFile, std::function или что-то вроде того.
Это потому, что вы увели разговор в сторону. Изначально речь шла о том, что я не вижу смысле в UniqueHandle, который не может сам закрыть хранящийся в нем дескриптор. И показал вам свою реализацию похожего класса.
Вы же сейчас пытаетесь перевести разговор на другой уровень абстракции, когда над дескриптором появляется еще какая-то логика (вроде промежуточных буферов). Очевидно, что handle_holder к этой логике никакого отношения не имеет и не следует в него закладывать еще что-то.
Но для этого более высокого уровня абстракции можно сделать свой класс, хранящий и handle_holder и еще что-то, и знающий как это что-то обработать при закрытии ресурса.
Вы так говорите, как будто это что-то плохое. :)(:
Для заменяемых типов (transparently replaceable) — не нужен. (Т.к. в данном случае имеет место реконструкция объекта того же самого типа, т.е. тип исходного объекта и вновь созданного полностью совпадают.)
У вас move_assign -- это шаблон и вы не знаете, есть ли в типе T константные поля или поля-ссылки. В статье, на которую вы сослались, соответствующая ситуация описана в разделе "Использование std::launder", где рассматривается структура с const int n внутри.
Проблема. Т.к. одного умения делать close, увы, не всегда достаточно. Конкретный пример: деструктор в классе OFile вызывает метод flush(), который записывает все оставшиеся данные в буфере в файл.
Не вижу причин, по которым ваш ~OFile не может быть реализован вот так:
~OFile() { close(); }
Следовательно, в условном Traits::destroy вы можете делать флуширование, если вам это нужно.
во-вторых, что более важно: при добавлении новых переменных-класса в OFile обновлять код реализации operator=(OFile &&) не потребуется.
Да уже понятно что у вас пунктик по этому поводу. Только вот если использовать идиому "make temporary then swap", то модифицировать придется реализацию swap, что все равно полезно.
А вот когда у объекта деструктор вызывается явно просто чтобы сделать присваивание/перемещение, то это добавит приключений тем программистам, которые при разработке добавляют в деструкторы своих классов отладочные печати.
Так что мой вопрос остаётся в силе: было бы интересно увидеть, какую альтернативу моему решению предлагаете вы?
Вам я ничего не предлагаю. Реализация, ссылку на которую я давал, у меня давно используется.
Но, за неимением вашего кода, давайте покажу на примере кода от Microsoft.
Мне доводилось заглядывать в код C++ REST SDK, этого хватило, чтобы скептически относиться к "коду от Microsoft". Так что ссылка на такой себе авторитет.
Сам я предпочитаю реализовывать copy operator и move operator через идиому "make temporary then swap".
Но, что даже более важно, благодаря UniqueHandle можно добавлять в FileHandle новые переменные-класса только в одном месте — в самом классе, т.е. править код move assignment operator и move constructor при этом не нужно, т.к. новые переменные-класса будут учитываться автоматически в сгенерированном компилятором коде move assignment operator и move constructor.
Только вот если вашу реализацию использовать в рамках C++17, то там нужен std::launder, т.к. вызовом деструктора вы прекращаете лайфтайм старого объекта, а возвращенное оператором placement new значение выбрасываете.
Если вы считаете, что UniqueHandle не нужен
Вы невнимательно прочитали: я говорил, что UniqueHandle, который не может сделать close, не нужен. А если он может делать close, то очистка ресурсов в move operator не проблема.
желательно не просто на словах, а в виде конкретного кода
Спасибо, что нашли время ответить. Но мотивация к появлению move_assign не смотря на обилие текста от меня ускользнула.
Если функцию move_assign() "реализовать" как *dest = std::move(other);
Так ведь можно вообще не иметь move_assign и делать просто *dest = std::move(other) в местах, где вы применяете move_assign.
И нет, "Потому, что UniqueHandle не умеет корректно закрывать handle." -- не оправдание. Нафиг нужен UniqueHandle, который не умеет закрывать хранящийся в нем дескриптор.
Возможно, если не поленюсь, напишу proposal к стандарту C++
Может лучше пожалеть людей из комитета, которым придется читать подобный бред?
Если тип объекта не поменялся (включая cv-квалификации), то std::launder не нужен.
Я так понимаю, что это начиная с C++20 std::launder не нужен. А в рамках C++17 вроде бы еще нужен, даже если тип остается тем же самым (здесь в конце раздела "std::launder and pointer values").
Вы принципиально не наследуете свои исключения от std::exception (или какого-то наследника std::exception)?
Почему вы не используете дерево наследования для своих исключений? Вроде такого:
class FfhException {};
class FileOpenError : public FfhError {};
class WrongFileNameStr : public FfhError {};
class FileIsAlreadyOpened : public FfhError {};
class AttemptToReadAClosedFile : public FfhError {};
Без такой иерархии пользователи могут забабахаться ловить исключения из вашей библиотеки. Особенно если со временем вы туда еще какие-то исключения будете добавлять.
Можно спросить, а почему такая мудреная реализация move_assign?:
template <class Ty> void move_assign(Ty *dest, Ty &&other)
{
if (dest != &other) {
dest->~Ty();
new(dest)Ty(std::move(other));
}
}
Почему нельзя было просто сделать *dest = std::move(other)?
Кроме того, если мне не изменяет склероз, то после того, как вы сделали placement new для dest, то для дальнейшего использования dest вам следует применить к нему std::launder.
а демонстрация того, что в C++ нет никакого способа обеспечения иммутабельности ссылок
Ссылку в C++ вы просто так изменить не сможете. Отсюда и задачка с i, j и ri. Т.е. назвать ссылки в C++ мутабельными -- это от глубокого знания предмета, не иначе.
И речь про ссылки. Не про объекты, ссылки на которые взяты. Именно про ссылки.
У вас может быть ссылка на const- объект. Или ссылка на не-const объект. Но вот поменять значение самой ссылки вы просто так не сможете.
А никакого другого смысла, кроме защиты от ошибок, механизм иммутабельности не несёт вообще.
Мощно, внушаить. Еще что-то такого же масштаба задвинуть сможете?
Да я вообще не спорю, а показываю несостоятельность некоторых ваших заявлений. Чтобы было лучше понятно, что в C++ вы разбираетесь как "свинья в апельсинах", пардон май френч.
Вам не всё ли будет равно, упадёт ваша программа “валидным и легальным способом” или не очень валидным и даже совсем нелегальным?
Для меня есть большая разница упадет ли программа или нет. Использование трюков с UB прямой путь к падению, поэтому за такие трюки нужно отрывать руки.
Так-то и иммутабельность низачем не нужна, если сразу правильно программы писать.
Не уводите разговор в сторону. Вы полезли судить о C++, вас в очередной раз макнули в то, что вы неправы в своих утверждениях про C++. И сама по себе иммутабельность здесь не причем. Причем ваша псевдо ученость.
Конечно UB. Но вы спросили, как сделать – я вам показал.
Дяденька, а вы вменяемы вообще? Показали как выстрелить в ногу и считаете это нормальным? Ну ахринеть.
Вы свои проекты тоже с такими ключами компилите?
Случается.
Я не знаю, какая была задача у вас
Так я повторю:
int i = 0;
int & ri = i;
int j = 0;
В одной области видимости есть живые i и j. Нужно сделать так, чтобы ссылка ri, которая ссылалась на i, начала ссылаться на j. Валидным и легальным способом, само собой.
но я вам показал именно то, что хотел – что неизменность ссылок в C++ не гарантируется
Вы показали (в очередной раз) свое незнание предмета разговора.
(а именно, в C++ есть указатели на иммутабельные объекты, иммутабельные указатели на объекты, иммутабельные указатели на иммутабельные объекты, ссылки на иммутабельные объекты, но нет иммутабельных ссылок)
Вы хотите сказать, что ссылки в C++ мутабельны?
Тогда вот вам:
int i = 0;
int & ri = i;
int j = 0;
покажите как сделать ri ссылкой на j.
Сама концепция уничтожения объекта в практическом отношении востребована в языке C++ только из-за особенностей используемого архаичного механизма ручного управления памятью.
Ну надо же. Ручное управление памятью уже архаично. Настолько, что из ряда предметных областей его и в отдаленной перспективе не изживут.
Ну да, касательно разработчиков Rust остается только сказать "а мужики-то и не знают"... facepalm.jpg
доступ к константам безопасен из нескольких потоков
Наличие mutable членов структур/классов уже делает картину мира сложнее. Не говоря уже про то, что в C++ нет транзитивной иммутабельности (к счастью или к сожалению), поэтому даже вызов константного метода у константного объекта может вести к изменению каких-то данных:
class mutator {
public:
void change_something() {...}
...
};
class viewer {
mutator * m_mutator;
public:
viewer(mutator * mutator) : m_mutator{mutator} {}
void f() const { m_mutator->change_something(); }
};
mutator m;
const viewer v1{&m};
const viewer v2{&m};
v1.f(); // Значение m могло измениться.
v2.f(); // Значение m могло измениться еще раз.
Так что не все так однозначно, к сожалению. Но лучше уж иметь такой const, чем не иметь никакого.
Некоторое время назад сделал std::shared_ptr<const some_data> для случая, когда экземпляры some_data разделялись сразу между несколькими потребителями. Грубо говоря, один тред выкачивает данные из источника и формирует std::shared_ptr<const some_data>, после чего этот shared_ptr отсылается всем заинтересованным потребителям (каждый на своем треде). За счет того, что данные константные не нужно париться о том, что кто-то из потребителей начнет их менять.
Для вас это всего лишь история, для меня -- собственный опыт.
Чуть больше, по некоторым оценкам количество программистов на C++ в 1991-ом году оценивалось где-то в 400K.
Ну и эта самая "фигня какая-то" была сильно круче тогдашних конкурентов (коих и не сказать, чтобы было много на конец 1980-х и начало 1990-х).
Забавно, что для некоторых C++ в то время не был отдельным языком программирования, а воспринимался просто как следующая версия Си, в который добавили пару-тройку новых ключевых слов. Тем более, что одним из "selling points" C++ всегда было то, что можно было взять уже имеющийся код на Си и с минимальными переделками начать использовать его как код на C++ (собственно перевод кодовой базы GCC на C++ как раз лишний тому пример). Даже без классов и перегрузки операторов. Просто ссылки вместо указателей.
Так что если бы C++ не выглядел в начале своей жизни именно как нашлепка над Си, то вряд ли он обрел бы такую популярность. Мог бы запросто повторить судьбу Eiffel-я, а то и Modula-3 (при том, что Eiffel не умер, живет себе и здравствует, просто мало кому нужен).
Изначально их не было. Они появились в C++ только в 1990-ом году, когда язык уже стал популярным за счет своей совместимости с Си.
Более того, шаблонов в C++ так же изначально не было. И выкручиваться с обобщенным программированием на C++ тогда приходилось за счет Си-шного препроцессора с define-ами.
Об этом рано говорить, модули пока что используют не только лишь все.
Он и auto изначально хотел использовать для автоматического вывода типов. Но "совместимость" с Си не позволила.
Ну да, ну да. Взлетел бы C++ если бы он бы не помесью Симула и Си, а помесью Симула и Модула-2 (к примеру) -- большой вопрос. По факту же C++ смог несколько десятилетий жить за счет экосистемы Си (начиная от линкеров и форматов объектных файлов, заканчивая бесплатной интеграцией с Си-шным кодом). Как-то странно слышать, что "Си здесь вообще ни при чем".
Страуструп был вынужден использовать имевшийся тогда Си, в котором никаких модулей не было. А почему был выбран Си тот же Страуструп многократно и подробно рассказывал, емнип, в "Дизайн и эволюция языка C++".
Следование принципу DRY. У вас, по сути, close и деструктор должны приводить к одинаковым результатам. Поэтому логичным выглядит не повторять логику в разных местах, а выражать одно через другое.
Кстати говоря, странно, почему вы оператор перемещения через move_assign делаете, а close -- нет. Можно же close сделать так:
И получить те же самые бенефиты, на которых вы зациклены: про добавлении нового поля в класс не нужно делать лишние операции.
Это потому, что вы увели разговор в сторону. Изначально речь шла о том, что я не вижу смысле в UniqueHandle, который не может сам закрыть хранящийся в нем дескриптор. И показал вам свою реализацию похожего класса.
Вы же сейчас пытаетесь перевести разговор на другой уровень абстракции, когда над дескриптором появляется еще какая-то логика (вроде промежуточных буферов). Очевидно, что handle_holder к этой логике никакого отношения не имеет и не следует в него закладывать еще что-то.
Но для этого более высокого уровня абстракции можно сделать свой класс, хранящий и handle_holder и еще что-то, и знающий как это что-то обработать при закрытии ресурса.
Оно выглядит как-то излишне экстремально.
У вас move_assign -- это шаблон и вы не знаете, есть ли в типе T константные поля или поля-ссылки. В статье, на которую вы сослались, соответствующая ситуация описана в разделе "Использование std::launder", где рассматривается структура с
const int n
внутри.Не вижу причин, по которым ваш ~OFile не может быть реализован вот так:
Следовательно, в условном
Traits::destroy
вы можете делать флуширование, если вам это нужно.Да уже понятно что у вас пунктик по этому поводу. Только вот если использовать идиому "make temporary then swap", то модифицировать придется реализацию swap, что все равно полезно.
А вот когда у объекта деструктор вызывается явно просто чтобы сделать присваивание/перемещение, то это добавит приключений тем программистам, которые при разработке добавляют в деструкторы своих классов отладочные печати.
Вам я ничего не предлагаю. Реализация, ссылку на которую я давал, у меня давно используется.
Мне доводилось заглядывать в код C++ REST SDK, этого хватило, чтобы скептически относиться к "коду от Microsoft". Так что ссылка на такой себе авторитет.
Сам я предпочитаю реализовывать copy operator и move operator через идиому "make temporary then swap".
Только вот если вашу реализацию использовать в рамках C++17, то там нужен std::launder, т.к. вызовом деструктора вы прекращаете лайфтайм старого объекта, а возвращенное оператором placement new значение выбрасываете.
Вы невнимательно прочитали: я говорил, что UniqueHandle, который не может сделать close, не нужен. А если он может делать close, то очистка ресурсов в move operator не проблема.
Где-то здесь есть реализация
handle_holder
.Спасибо, что нашли время ответить. Но мотивация к появлению move_assign не смотря на обилие текста от меня ускользнула.
Так ведь можно вообще не иметь move_assign и делать просто
*dest = std::move(other)
в местах, где вы применяете move_assign.И нет, "Потому, что UniqueHandle не умеет корректно закрывать handle." -- не оправдание. Нафиг нужен UniqueHandle, который не умеет закрывать хранящийся в нем дескриптор.
Может лучше пожалеть людей из комитета, которым придется читать подобный бред?
Я так понимаю, что это начиная с C++20 std::launder не нужен. А в рамках C++17 вроде бы еще нужен, даже если тип остается тем же самым (здесь в конце раздела "std::launder and pointer values").
И еще вопрос(ы) по исключениям:
Вы принципиально не наследуете свои исключения от
std::exception
(или какого-то наследникаstd::exception
)?Почему вы не используете дерево наследования для своих исключений? Вроде такого:
Без такой иерархии пользователи могут забабахаться ловить исключения из вашей библиотеки. Особенно если со временем вы туда еще какие-то исключения будете добавлять.
Можно спросить, а почему такая мудреная реализация move_assign?:
Почему нельзя было просто сделать
*dest = std::move(other)
?Кроме того, если мне не изменяет склероз, то после того, как вы сделали placement new для dest, то для дальнейшего использования dest вам следует применить к нему std::launder.
Вы привели пример невалидного кода.
На этом разговор уже закончен. Для вменяемых программистов. Вы же вольны теоретизировать дальше.
Ссылку в C++ вы просто так изменить не сможете. Отсюда и задачка с
i
,j
иri
. Т.е. назвать ссылки в C++ мутабельными -- это от глубокого знания предмета, не иначе.И речь про ссылки. Не про объекты, ссылки на которые взяты. Именно про ссылки.
У вас может быть ссылка на const- объект. Или ссылка на не-const объект. Но вот поменять значение самой ссылки вы просто так не сможете.
Мощно, внушаить. Еще что-то такого же масштаба задвинуть сможете?
Да я вообще не спорю, а показываю несостоятельность некоторых ваших заявлений. Чтобы было лучше понятно, что в C++ вы разбираетесь как "свинья в апельсинах", пардон май френч.
Для меня есть большая разница упадет ли программа или нет. Использование трюков с UB прямой путь к падению, поэтому за такие трюки нужно отрывать руки.
Не уводите разговор в сторону. Вы полезли судить о C++, вас в очередной раз макнули в то, что вы неправы в своих утверждениях про C++. И сама по себе иммутабельность здесь не причем. Причем ваша псевдо ученость.
Дяденька, а вы вменяемы вообще? Показали как выстрелить в ногу и считаете это нормальным? Ну ахринеть.
Случается.
Так я повторю:
В одной области видимости есть живые
i
иj
. Нужно сделать так, чтобы ссылкаri
, которая ссылалась наi
, начала ссылаться наj
. Валидным и легальным способом, само собой.Вы показали (в очередной раз) свое незнание предмета разговора.
Да блин, ну ёжкинижеж... Не, ну как так-то?
У вас же там UB.
https://wandbox.org/permlink/IazzAnT1hLsEcGP2
Не говоря уже про то, что задача была вообще другой.
Завязывали бы вы с рассуждениями про C++, а?
Ну ведь степень вашей невменяемости уже даже не удивление, я прям таки отторопь вызывает.
Вы хотите сказать, что ссылки в C++ мутабельны?
Тогда вот вам:
покажите как сделать
ri
ссылкой наj
.Ну надо же. Ручное управление памятью уже архаично. Настолько, что из ряда предметных областей его и в отдаленной перспективе не изживут.
Ну да, касательно разработчиков Rust остается только сказать "а мужики-то и не знают"... facepalm.jpg
Наличие mutable членов структур/классов уже делает картину мира сложнее. Не говоря уже про то, что в C++ нет транзитивной иммутабельности (к счастью или к сожалению), поэтому даже вызов константного метода у константного объекта может вести к изменению каких-то данных:
Так что не все так однозначно, к сожалению.
Но лучше уж иметь такой const, чем не иметь никакого.
Некоторое время назад сделал
std::shared_ptr<const some_data>
для случая, когда экземплярыsome_data
разделялись сразу между несколькими потребителями. Грубо говоря, один тред выкачивает данные из источника и формируетstd::shared_ptr<const some_data>
, после чего этот shared_ptr отсылается всем заинтересованным потребителям (каждый на своем треде). За счет того, что данные константные не нужно париться о том, что кто-то из потребителей начнет их менять.Что лямбда в C++ спокойно переживает тот контекст, в котором она была создана.
Что, черт возьми, ты здесь несешь?Сами-то хоть поняли, что сказали?Лямбда и есть объект. У этого объекта есть тип. Поэтому можно создать std::vector для экземпляров такого типа. И заполнить этот вектор.
Здесь нет никакого наследования.
Не-а. Но это же C++ знать нужно ну хоть чуть-чуть.
Это реализация лямбд в C++ позволяет мне использовать их вот таким образом.