Кажется, вы игнорируете принципиальное отличие pmr от не-pmr. Оно вовсе не в наличии нескольких имплементациий аллокаторов в стандартной библиотеке. Оно про скалируемость разработки ценой рантайм полиморфизма, которая с обычными аллокаторами просто ужасна
Но герой вашей статьи вместо упрощения усложняет свою жизнь 🙂
std::pmr::memory_resource* resource = std::pmr::get_default_resource() последним параметром?
Зачем вам вообще иначе pmr? Используйте обычный std::allocator
То что у вас должно было быть в примере - AA тип со всеми вытекающими, включая прием аллокатора в clone, и вы просто передаёте свой аллокатор в std::pmr::deque, который уже сам все передаст дальше
Никаких placement-new/вызовов деструктор в, у вас для этого методы polymorphic_allocator есть (не путать с memory_resource). И никаких менеджеров-синглтонов. В крайнем случая адаптер, который будет управлять аллокатором
Посыл полезный, но в данном случае почти все проблемы решались бы передачей аллокатора извне по цепочке, попутно делая ваши типы allocator-aware при необходимости. Это и есть центральная идея pmr, и было бы полезно увидеть демонстрацию "как надо"
Корректность такого кода довольно легко было бы проверить 1) статически, проверяя на соответствие требованиям AA (в стандарте готового шаблона нет, но пишется он один раз для всех типов) 2) в юнит тесте, делая инъекцию аллокатора с подсчётом аллокаций, и выставляя аллокатором по-умолчанию другой аллокатор, бросающий исключение при аллокации
С полиморфическими аллокаторами столько разных тонкостей, например - деградация move-assignment, AA, а вы выбрали написать про костыли для лайфтаймов, ортогональные с самими аллокаторами
Для понимания большинства ньюансов достаточно изучить P2127, P2035, P2126, при желании игнорируя легаси и специфику BDE
Соглашусь, у меня формулировка подкачала. Но все-таки, я отвечал на комментарий где предлагается именно обращаться по указателю.
Что касается просто каста - в directx когда-то был официальный способ сеттить float через интерфейс, принимающий int - ровно reintepret_cast'ом из float в int.
С алайасингом и у меня коллиззии в памяти, поэтому я без толики иронии и предложил со мной поспорить :)
Обычно угадывать - такая себе идея, а главное зачем, если вышеупомянутые memcpy/std::copy/std::bit_cast с 99.9% вероятностью сделают то же самое (без оверхеда), но на 100% надежно?
Но тут речь даже о другом, цитируя комментарий выше:
Вы можете кастовать (char/unsigned char/std::byte)* в любой тип, и обращение по указателю далее будет валидным, если там существует объект.
Правило то такое действительно есть, только работает оно строго в другую сторону
Все примеры одинаковое UB - кастить можно только к char* (или unsigned char/std::byte), а не наоборот. Обратно - только memcpy/std::copy/std::bit_cast.
Да, следующий абзац я прочитал. Вопрос больше к формулировке в контексте истории такого подхода под виндой)
Какое-то время назад было же очень модно (а где-то "необходимо") использовать всякие Nt*/Ke*/Rtl*, которых не было в документации. Ладно в юзермоде, код просто ломался, но на такие драйвера приходилась львиная доля BSOD. Казалось бы - без особых костылей, но логика железная - нет в документации, значит можно менять/удалять по внутренней необходимости винды. Они ж только для внутреннего использования, правда ведь? :)
Если вы про "владение" объектом, у RAII своя специфика, но на этом уникальность заканчивается. Все остальное тоже лишь частично специфично для плюсов. Концепции примитивов синхронизации или размер кэшлайна, например, вообще к языку отношения не имеют. Но в плюсах подобные знания, по моим наблюдениям, требуются гораздо чаще, чем в других "популярных" языках.
Время проверяет только возможность писать что-то на языке. Оно не проверяет оптимальность этого решения.
Речь не об оптимальности, а о целесообразности.
Я нигде не говорил, что надо на расте что-то имеющееся переписывать. Речь только о новых проектах, не более.
Это говорил автор статьи. Раз вы топите за Rust в этой дискуссии, я решил что вы и с текстом статьи согласны :)
С которой работаете. Разного уровня. Но не надо утрировать и спускаться туда, что едва может пригодится. В данном случае контекстом была функция и "владение" объектом. Если вы будете работать с многопоточностью, вам придется погружаться в матчасть concurrency и модели памяти. Высоконагруженные системы или рендеринг - во многих случаях аллокаторы и снова модели памяти.
зачем тогда C++
В общем-то это ответ и на остальные вопросы, и естественно, это лишь мое видение: С++ это зрелый, проверенный временем (и гигантским количеством кода) язык, для которого существует не только гора библиотек, но и огромное количество методологий, инструментов, гайдлайнов, и прочих смежных практик. С рынком все обстоит сложнее, и я тут экспертное мнение выразить не могу. Если по вашему это все тоже легаси, а легаси это плохо - а у вас есть уверенность, что Rust тот же во всем лучше? Перепишут на него весь софт, драйвера, да и ОС заодно. Но где-то в середине процесса поймут, что он все же не так хорош, есть идеи получше, и нужен новый язык. Вообразите, что получится? ИМХО, пусть удачные языки развиваются и набирают обороты, а время покажет нужное направление. 10 лет - чудовищно молодой возраст для языка, чтобы бросится переписывать на него все, что можно.
Когда вы ссылаетесь на UB, странно использовать семантический смысл. string_view это связка указателя и целочисленной переменной. С точки зрения стандарта это принципиально разные вещи.
Повисшие ссылки являются в C++ неопределённым поведением
Опять же, в нашем случае - указатели. Но так или иначе, неопределенное поведение возникает не в вакууме, а при доступе к dangling pointer. Все это четко прописано в стандарте в разделе 6.7.3.
потому компилятор может преобразовать программу, исходя из невозможности этого неопределённого поведения
Из пояснения выше - в моменте возврата dangling pointer - не может. Санитайзер отлично справится с задачей.
А еще, справедливости ради, такое должно ловится даже не санитайзерами, а SCA. В GCC инструмент явно не доделан. А даже если по итогу он не сможет ловить вложенные кейсы - он и не претендует на сколь-либо серьезный статус SCA.
Предвкушая ваш вопрос о "сами то используете": в коммерческой разработке? Конечно. Во многих проектах без SCA заказчик не станет с вами работать, а в по-настоящему серьезных проектах, код не имеет ни шанса пройти сертификацию.
>Компилятор вообще ничего не обязан делать, но чем больше ошибок компилятор предотвращает, тем лучше.
Ну как же, компилятор обязан делать то, что явно указано в стандарте. Но раз вы возражаете, разверну свою банальную идею про отсутствие обязательства предотвращения таких ошибок: если нет тривиального способа обнаружить ошибку, обязанность, возложенная на компилятор потенциально существенно усложнит имплементацию. По этой причине большая часть диагностик имплементируется на уровне "так можно но нельзя", и примерно по той же причине стандарт С11 - модульный.
>Это влияет только для проектов уровня хелловорлда. Для проектов побольше даже самые мозговитые люди делают ошибки — ну, просто потому, что все детали удержать в памяти человека невозможно.
Тут я немного некорректно выразился. Мозг нужен везде, и разница в компетенции даже для разработчиков на питоне может серьезно повлиять на количесто и критичность ошибок.
Я имел в виду то, что есть языки, которые не требуют глубокого понимания матчасти. Как правило, все сводится к степени высокоуровневости языка. С++ становится все более высокоуровневым с новыми стандартами, вероятность отстрелить себе ногу действительно сильно уменьшается (те же умные указатели или аллокаторы как яркий пример), но вместе с этим язык продолжает развиваться и в "плюсовую" сторону - memory models, execution policies, и тот же string_view можно отнести сюда. У кого-то здесь есть более удачная идея для дизайна string_view? Поделитесь! Очень интересно послушать (не сарказм)
Если человек не способен распознать, где нельзя возвращать string_view, ему следует его избегать, а возможно избегать C++ вообще. И это не плохо, ведь есть же Rust!
P.S. в более или менее крупных проектах на C++, существуют safety guidelines (не путать с coding style). Их задача во многом состоит именно в том, чтобы защитить разработчика от C++. И они действительно строгим образом трейсятся с помощью различных SCA инструментов. Так что в конечном итоге все подобные тезисы в том или ином виде сводятся к "я разыменовал nullptr и у меня segfault. давайте сделаем язык с безусловной превентивной проверкой на nullptr для всех указателей".
Мы действительно можем получить некоторый range строки, который, разумеется, будет non-owning. И мы действительно можем использовать этот функционал локально или применяя ООП.
А теперь вернемся к вашему примеру и вашим функциям.
Возврат вообще чего-либо non-owning из семантически чистой функции - грубая ошибка дизайна. По этой же причине невалидны обе ваши функции, и по этой же причине уместно сравнение с возвратом локальной переменной по ссылке.
Должен ли компилятор наказывать за такое? Было бы неплохо, но, надеюсь, не нужно пояснять, почему он не обязан этого делать.
Иными словами, еще раз: язык тут не причем. Хотя, в каком-то смысле, вину C++ может увидеть в том, что у многих сложилось впечатление, что он с течением лет превращается в ту доминирующую группу языков, где можно писать код не используя мозг.
Кажется, вы игнорируете принципиальное отличие pmr от не-pmr. Оно вовсе не в наличии нескольких имплементациий аллокаторов в стандартной библиотеке. Оно про скалируемость разработки ценой рантайм полиморфизма, которая с обычными аллокаторами просто ужасна
Но герой вашей статьи вместо упрощения усложняет свою жизнь 🙂
std::pmr::memory_resource* resource = std::pmr::get_default_resource() последним параметром?
Зачем вам вообще иначе pmr? Используйте обычный std::allocator
То что у вас должно было быть в примере - AA тип со всеми вытекающими, включая прием аллокатора в clone, и вы просто передаёте свой аллокатор в std::pmr::deque, который уже сам все передаст дальше
Никаких placement-new/вызовов деструктор в, у вас для этого методы polymorphic_allocator есть (не путать с memory_resource). И никаких менеджеров-синглтонов. В крайнем случая адаптер, который будет управлять аллокатором
А можно поподробнее, в чем тут конкретно неудобность? На примере third-party
Посыл полезный, но в данном случае почти все проблемы решались бы передачей аллокатора извне по цепочке, попутно делая ваши типы allocator-aware при необходимости. Это и есть центральная идея pmr, и было бы полезно увидеть демонстрацию "как надо"
Корректность такого кода довольно легко было бы проверить 1) статически, проверяя на соответствие требованиям AA (в стандарте готового шаблона нет, но пишется он один раз для всех типов) 2) в юнит тесте, делая инъекцию аллокатора с подсчётом аллокаций, и выставляя аллокатором по-умолчанию другой аллокатор, бросающий исключение при аллокации
С полиморфическими аллокаторами столько разных тонкостей, например - деградация move-assignment, AA, а вы выбрали написать про костыли для лайфтаймов, ортогональные с самими аллокаторами
Для понимания большинства ньюансов достаточно изучить P2127, P2035, P2126, при желании игнорируя легаси и специфику BDE
Right click->Break when value changes?
А окно установки дата-бряка вполне понимает &var
del перечитал
А, да? А покажете указатель на нулевой адрес?
Соглашусь, у меня формулировка подкачала. Но все-таки, я отвечал на комментарий где предлагается именно обращаться по указателю.
Что касается просто каста - в directx когда-то был официальный способ сеттить float через интерфейс, принимающий int - ровно reintepret_cast'ом из float в int.
С алайасингом и у меня коллиззии в памяти, поэтому я без толики иронии и предложил со мной поспорить :)
Обычно угадывать - такая себе идея, а главное зачем, если вышеупомянутые memcpy/std::copy/std::bit_cast с 99.9% вероятностью сделают то же самое (без оверхеда), но на 100% надежно?
Но тут речь даже о другом, цитируя комментарий выше:
Правило то такое действительно есть, только работает оно строго в другую сторону
Все примеры одинаковое UB - кастить можно только к char* (или unsigned char/std::byte), а не наоборот. Обратно - только memcpy/std::copy/std::bit_cast.
Референс - https://eel.is/c++draft/basic.lval#11
Но не возражаю, если опровергнете.
UPD: перечитав примеры, согласен, что вторая часть вполне себе валидная, но пассаж про каст из char очень неоднозначный
Да, следующий абзац я прочитал. Вопрос больше к формулировке в контексте истории такого подхода под виндой)
Какое-то время назад было же очень модно (а где-то "необходимо") использовать всякие Nt*/Ke*/Rtl*, которых не было в документации. Ладно в юзермоде, код просто ломался, но на такие драйвера приходилась львиная доля BSOD. Казалось бы - без особых костылей, но логика железная - нет в документации, значит можно менять/удалять по внутренней необходимости винды. Они ж только для внутреннего использования, правда ведь? :)
Как же вы недооцениваете чудовищность этого костыля
Если вы про "владение" объектом, у RAII своя специфика, но на этом уникальность заканчивается. Все остальное тоже лишь частично специфично для плюсов. Концепции примитивов синхронизации или размер кэшлайна, например, вообще к языку отношения не имеют.
Но в плюсах подобные знания, по моим наблюдениям, требуются гораздо чаще, чем в других "популярных" языках.
Речь не об оптимальности, а о целесообразности.
Это говорил автор статьи. Раз вы топите за Rust в этой дискуссии, я решил что вы и с текстом статьи согласны :)
Дизайн возникает от юзкейсов. Если вам может потребоваться сделать
return id(&local_variable);
- да, это ошибка дизайна.В примере AnthonyMikh именно такой пример и приведен
Из функции, которая семантически является чистой.
С которой работаете. Разного уровня. Но не надо утрировать и спускаться туда, что едва может пригодится. В данном случае контекстом была функция и "владение" объектом.
Если вы будете работать с многопоточностью, вам придется погружаться в матчасть concurrency и модели памяти.
Высоконагруженные системы или рендеринг - во многих случаях аллокаторы и снова модели памяти.
В общем-то это ответ и на остальные вопросы, и естественно, это лишь мое видение:
С++ это зрелый, проверенный временем (и гигантским количеством кода) язык, для которого существует не только гора библиотек, но и огромное количество методологий, инструментов, гайдлайнов, и прочих смежных практик. С рынком все обстоит сложнее, и я тут экспертное мнение выразить не могу.
Если по вашему это все тоже легаси, а легаси это плохо - а у вас есть уверенность, что Rust тот же во всем лучше? Перепишут на него весь софт, драйвера, да и ОС заодно. Но где-то в середине процесса поймут, что он все же не так хорош, есть идеи получше, и нужен новый язык.
Вообразите, что получится?
ИМХО, пусть удачные языки развиваются и набирают обороты, а время покажет нужное направление. 10 лет - чудовищно молодой возраст для языка, чтобы бросится переписывать на него все, что можно.
Когда вы ссылаетесь на UB, странно использовать семантический смысл. string_view это связка указателя и целочисленной переменной. С точки зрения стандарта это принципиально разные вещи.
Опять же, в нашем случае - указатели. Но так или иначе, неопределенное поведение возникает не в вакууме, а при доступе к dangling pointer. Все это четко прописано в стандарте в разделе 6.7.3.
Из пояснения выше - в моменте возврата dangling pointer - не может. Санитайзер отлично справится с задачей.
А еще, справедливости ради, такое должно ловится даже не санитайзерами, а SCA. В GCC инструмент явно не доделан. А даже если по итогу он не сможет ловить вложенные кейсы - он и не претендует на сколь-либо серьезный статус SCA.
Предвкушая ваш вопрос о "сами то используете": в коммерческой разработке? Конечно. Во многих проектах без SCA заказчик не станет с вами работать, а в по-настоящему серьезных проектах, код не имеет ни шанса пройти сертификацию.
>Компилятор вообще ничего не обязан делать, но чем больше ошибок компилятор предотвращает, тем лучше.
Ну как же, компилятор обязан делать то, что явно указано в стандарте. Но раз вы возражаете, разверну свою банальную идею про отсутствие обязательства предотвращения таких ошибок: если нет тривиального способа обнаружить ошибку, обязанность, возложенная на компилятор потенциально существенно усложнит имплементацию. По этой причине большая часть диагностик имплементируется на уровне "так можно но нельзя", и примерно по той же причине стандарт С11 - модульный.
>Это влияет только для проектов уровня хелловорлда. Для проектов побольше даже самые мозговитые люди делают ошибки — ну, просто потому, что все детали удержать в памяти человека невозможно.
Тут я немного некорректно выразился. Мозг нужен везде, и разница в компетенции даже для разработчиков на питоне может серьезно повлиять на количесто и критичность ошибок.
Я имел в виду то, что есть языки, которые не требуют глубокого понимания матчасти. Как правило, все сводится к степени высокоуровневости языка.
С++ становится все более высокоуровневым с новыми стандартами, вероятность отстрелить себе ногу действительно сильно уменьшается (те же умные указатели или аллокаторы как яркий пример), но вместе с этим язык продолжает развиваться и в "плюсовую" сторону - memory models, execution policies, и тот же string_view можно отнести сюда. У кого-то здесь есть более удачная идея для дизайна string_view? Поделитесь! Очень интересно послушать (не сарказм)
Если человек не способен распознать, где нельзя возвращать string_view, ему следует его избегать, а возможно избегать C++ вообще. И это не плохо, ведь есть же Rust!
P.S. в более или менее крупных проектах на C++, существуют safety guidelines (не путать с coding style). Их задача во многом состоит именно в том, чтобы защитить разработчика от C++. И они действительно строгим образом трейсятся с помощью различных SCA инструментов.
Так что в конечном итоге все подобные тезисы в том или ином виде сводятся к "я разыменовал nullptr и у меня segfault. давайте сделаем язык с безусловной превентивной проверкой на nullptr для всех указателей".
>Вроде бы string_view как раз сделали для того, чтобы не копировать строки лишний раз.
И снова ошибаетесь, почитайте proposal для string_view, буквально первый раздел. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3921.html
Мы действительно можем получить некоторый range строки, который, разумеется, будет non-owning. И мы действительно можем использовать этот функционал локально или применяя ООП.
А теперь вернемся к вашему примеру и вашим функциям.
Возврат вообще чего-либо non-owning из семантически чистой функции - грубая ошибка дизайна. По этой же причине невалидны обе ваши функции, и по этой же причине уместно сравнение с возвратом локальной переменной по ссылке.
Должен ли компилятор наказывать за такое? Было бы неплохо, но, надеюсь, не нужно пояснять, почему он не обязан этого делать.
Иными словами, еще раз: язык тут не причем. Хотя, в каком-то смысле, вину C++ может увидеть в том, что у многих сложилось впечатление, что он с течением лет превращается в ту доминирующую группу языков, где можно писать код не используя мозг.