Сделали оптимизации - нашлись другие недовольные и т.д.
Как раз такое недовольство понятно: взяли и поломали то, что работало до того как. То же, что дали, не покрывает все проблемы. Т.е. взялись делать "хорошо" и недодумали. О чем и речь.
Нет, не безопасно.
Небезопасно с точки зрения буквы стандарта. Но если компилятор в рамках C++17 начнет ломать код, который требовал бы start_lifetime_as из C++23, то это уже вредительство, т.к. возможностей C++17 для покрытия этих ситуаций не хватает. Ну так и нечего ломать то, что работало до.
Т.е. должно работать негласное правило -- да, есть такой UB, но компилятор в рамках стандарта X не должен его эксплуатировать, т.к. устранить этот UB у программиста возможности нет.
Если такого правила нет, значит и комитет, и писатели компиляторов загоняют еще один гвоздь в крышку гроба C++.
Ну сейчас правила еще более просты: если уже тем или иным образом время жизни объекта внутри некоего массива байт уже началось - std::launder. Если еще не началось - std::start_lifetime_as.
Я выше уже привел пример. Советом был изменить архитектуру. Ну а если менять архитектуру, то почему бы не пойти еще дальше и не послать в пешее эротическое сам C++?
Чисто ради этого старого кода.
Да я как бы в курсе. Только вот что делать, если в проекте у меня вместо malloc-а своя функция для аллокации, а вместо read или memcpy -- своя функция побайтового копирования из COM-порта. Получается, что все звери равны, но некоторые равнее.
Как ни сделай, все равно кто-то недовольный да найдется
Так ведь это же нововведения в C++17 и C++20 довели до текущего состояния. Сперва в язык добавили std::launder и сделали вне закона практику, которой был уже не один десяток лет. Но выяснилось, что std::launder не решает всех проблем, т.е. есть еще и создание типов через malloc (унаследованная из Си практика), и десериализация POD-типов в байтовый буфер.
Поэтому в C++20 попробовали это исправить. Ввели неявное начало лайфтайма. Но выяснилось, что это так же не все покрывает.
Поэтому в C++23 ввели еще и std::start_lifetime_as.
А в таких условиях писать код "уже сейчас" становится вообще еще тем квестом. Скажем, если я живу в рамках 17-го стандарта и у меня есть только std::launder, то мне безопасно вставлять std::launder везде. Если же мой код затем перекомпилируют в рамках C++23, то вдруг окажется, что где-то нужен не std::launder, а std::start_lifetime_as.
При этом если я захочу разобраться а как же должно быть правильно в рамках C++23, то остаются неясными предпосылки для std::launder и std::start_lifetime_as. Советы менять архитектуру идут по известному адресу, т.к. все, блин умные, пока реальный код писать или приводить в рабочее состояние не нужно.
ЗЫ. Собственно, чего хотелось бы иметь:
чтобы std::launder оставался только для случаев, когда пересоздается объект. Т.е. был объект типа A и на него были указатели, затем на том месте, где был объект A был создан новый объект A (или какой-то отнаследованный от него B), старые указатели "протухли", нужно их отмыть через std::launder. Все. Больше ни для чего std::launder не нужен;
чтобы std::start_lifetime_as использовался для случая, когда у нас есть std::byte* или char*, и мы хотим сказать компилятору, что по этому указателю реально живет объект A.
Все. Без всяких неявных умолчаний, что мол memcpy или malloc начинает время жизни.
На самом деле это проблема стандартизаторов, которые довели ситуацию до маразма. Выше уже дали ссылку на пропозал, который этот маразм пытается полечить.
Но вообще, конечно, лучше продумать архитектурку так
Ну дык понятно, что лучше быть здоровым и богатым, чем... ;)
Это может быть тупо невозможно, т.к. каждая функция может работать в своем треде и вообще ничего не знать про другую.
Ну, если он не в курсе, то он и всякие неоднозначные оптимизации применять не будет.
Так даже в таком тривиальном случае:
// Где-то в другом TU.
void create(std::span<std::byte> mem) { new(mem.data()) Demo{...}; }
// В моем TU.
void use(std::span<std::byte> mem) {
Demo * d = std::launder(reinterpret_cast<Demo*>(mem.data());
...
}
компилятору нечего оптимизировать, т.к. в моем TU внутри use никаких других объектов Demo или указателей на них нет и не было. Так что здесь, имхо, start_lifetime_as была бы даже логичнее.
Если по указателю находится объект, чей лайфтайм с точки зрения компилятора уже начался, то нужно использовать std::launder
Допустим, у нас есть буфер, в который мы загрузили значение из файла/пайпа. После чего передали указатель на этот буфер в две разные функции. Первая в C++23 должна использовать start_lifetime_as. Тем самым она начинает время жизни объекта.
Что должна делать вторая? Вызывать launder, т.к. первая уже лайфтайм для объекта начала? Или можно еще раз вызвать start_lifetime_as.
Плюс тому, launder/start_lifetime_as ограничены скоупом, в котором мы хотим использовать указатель на что-то. Компилятор понятия не имеет что происходит в других TU, в которых значение по указателю формируется. ХЗ был ли там уже начат лайфтайм или нет.
Если для более раннего стандарта, то там UB.
Так для того же C++17 другого способа и нет. И если компилятор в рамках 17-го стандарта начнет такой UB эксплуатировать, то это будет еще одним свидетельством, что эволюция языка и компиляторостроения пошла куда-то не туда.
std::start_lifetime_as нужен для создания объекта с состоянием уже хранящимся в байтах, где он будет размещен.
Насколько я знаю, start_lifetime_as не создает объект, а говорит компилятору о том, что появился новый объект о котором компилятор ранее не знал (т.е. он не был создан через new, как локальная переменная на стеке или каким-то другим известным компилятором способом). Т.е. программист говорит компилятору: вот в этой области памяти сейчас есть объект, пожалуйста, поверь, что это так.
В этой связи мне не понятно, чем область памяти, заполненная, например, чтением байт из файла или из пайпа, принципиально отличается от области памяти, в которой объект был ранее сконструирован через placement new.
У вас как раз launder нужен после того, как вы создали объекты через placement new, но типизированные указатели на них нигде не сохранили.
Но вот другой сценарий: https://wandbox.org/permlink/wTISrxXoKMlw0PUv (к сожалению, не удалось протестировать с start_lifetime_as, похоже, эту фичу пока еще в компилятор не завезли).
Здесь make_and_use_object для конструирования объекта вызывает разные функторы. И вот после возврата из функтора что должно применяться -- launder или start_lifetime_as? Ведь каждый из функторов заполняет переданный буфер по-разному?
И для launder здесь, вроде как, места нет, т.к. не было ранее объектов Data, на которые у меня могли бы быть указатели, и эти объекты бы пересоздавались.
В общем, я к тому, что после введения в язык start_lifetime_as стало не очень понятно в каком случае требуется launder, а в каком start_lifetime_as.
Да, есть такое дело. Но ведь в C++23 завозят std::start_lifetime_as и, поскольку в статье речь идет о C++26, может быть std::start_lifetime_as уместнее, чем std::launder?
PS. На самом деле я сам не знаю, что правильно. До C++23 вроде как std::launder необходим хотя бы для очистки совести. На вот начиная с C++23...
А точно ли здесь нужно использование std::launder? Вроде бы объект типа T инстанциируется корректно, затем его тип не меняется, а кастинг из std::byte* в T* полностью легален.
Во-первых, сложно и геморройно. Охота за утечками памяти в начале-середине 1990-х годов была одним из основных занятий при разработке на C++.
Во-вторых, умные указатели появились отнюдь не вместе с C++11. Я сам их начал использовать на повседневной основе во второй половине 1990-х (при этом не был пионером, а подсмотрел в чужих разработках, так что умные указатели в мире C++ уже лет тридцать как, если не больше). Ну а самый простой из них под видом std::auto_ptr был прямо в С++98.
ИМХО, этот момент имеет смысл отразить в статье. Потому что выравнивание -- это штука, про которые не все помнят, а кто-то и не знает. Скопируют ваш подход и наступят на грабли где-нибудь.
Могу ли я предположить, что игровой движок -- это небольшая часть конкретного игрового проекта и тот прессинг багов и нереализованных хотелок, про который упоминает автор статьи, относится не к игровому движку?
https://en.cppreference.com/w/cpp/links/libs -- этот список никакой не "официальный" Не знаю как сейчас, а несколько лет назад на cppreference можно было зарегистрироваться и внести изменения в wiki-страничку со списком библиотек. Так что там список того, что успели и не поленились добавить на эту wiki-страничку. Попасть в список awesome-cpp чуть сложнее, там (емнип) PR отправлялся и этот PR ждал пока его примут.
Как раз такое недовольство понятно: взяли и поломали то, что работало до того как. То же, что дали, не покрывает все проблемы. Т.е. взялись делать "хорошо" и недодумали. О чем и речь.
Небезопасно с точки зрения буквы стандарта. Но если компилятор в рамках C++17 начнет ломать код, который требовал бы start_lifetime_as из C++23, то это уже вредительство, т.к. возможностей C++17 для покрытия этих ситуаций не хватает. Ну так и нечего ломать то, что работало до.
Т.е. должно работать негласное правило -- да, есть такой UB, но компилятор в рамках стандарта X не должен его эксплуатировать, т.к. устранить этот UB у программиста возможности нет.
Если такого правила нет, значит и комитет, и писатели компиляторов загоняют еще один гвоздь в крышку гроба C++.
Я выше уже привел пример. Советом был изменить архитектуру.
Ну а если менять архитектуру, то почему бы не пойти еще дальше и не послать в пешее эротическое сам C++?
Да я как бы в курсе. Только вот что делать, если в проекте у меня вместо malloc-а своя функция для аллокации, а вместо read или memcpy -- своя функция побайтового копирования из COM-порта. Получается, что все звери равны, но некоторые равнее.
Так ведь это же нововведения в C++17 и C++20 довели до текущего состояния. Сперва в язык добавили std::launder и сделали вне закона практику, которой был уже не один десяток лет. Но выяснилось, что std::launder не решает всех проблем, т.е. есть еще и создание типов через malloc (унаследованная из Си практика), и десериализация POD-типов в байтовый буфер.
Поэтому в C++20 попробовали это исправить. Ввели неявное начало лайфтайма.
Но выяснилось, что это так же не все покрывает.
Поэтому в C++23 ввели еще и std::start_lifetime_as.
А в таких условиях писать код "уже сейчас" становится вообще еще тем квестом. Скажем, если я живу в рамках 17-го стандарта и у меня есть только std::launder, то мне безопасно вставлять std::launder везде. Если же мой код затем перекомпилируют в рамках C++23, то вдруг окажется, что где-то нужен не std::launder, а std::start_lifetime_as.
При этом если я захочу разобраться а как же должно быть правильно в рамках C++23, то остаются неясными предпосылки для std::launder и std::start_lifetime_as. Советы менять архитектуру идут по известному адресу, т.к. все, блин умные, пока реальный код писать или приводить в рабочее состояние не нужно.
ЗЫ. Собственно, чего хотелось бы иметь:
чтобы std::launder оставался только для случаев, когда пересоздается объект. Т.е. был объект типа A и на него были указатели, затем на том месте, где был объект A был создан новый объект A (или какой-то отнаследованный от него B), старые указатели "протухли", нужно их отмыть через std::launder. Все. Больше ни для чего std::launder не нужен;
чтобы std::start_lifetime_as использовался для случая, когда у нас есть std::byte* или char*, и мы хотим сказать компилятору, что по этому указателю реально живет объект A.
Все. Без всяких неявных умолчаний, что мол memcpy или malloc начинает время жизни.
Да я вообще редкостный урод, смею утвержать, что в C++ что-то сделали
через жопунедодумав.На самом деле это проблема стандартизаторов, которые довели ситуацию до маразма. Выше уже дали ссылку на пропозал, который этот маразм пытается полечить.
Ну дык понятно, что лучше быть здоровым и богатым, чем... ;)
Это может быть тупо невозможно, т.к. каждая функция может работать в своем треде и вообще ничего не знать про другую.
Так даже в таком тривиальном случае:
компилятору нечего оптимизировать, т.к. в моем TU внутри use никаких других объектов Demo или указателей на них нет и не было. Так что здесь, имхо,
start_lifetime_as
была бы даже логичнее.Жаль, что она как-то явно не описана :(
Допустим, у нас есть буфер, в который мы загрузили значение из файла/пайпа. После чего передали указатель на этот буфер в две разные функции. Первая в C++23 должна использовать start_lifetime_as. Тем самым она начинает время жизни объекта.
Что должна делать вторая? Вызывать launder, т.к. первая уже лайфтайм для объекта начала? Или можно еще раз вызвать start_lifetime_as.
Плюс тому, launder/start_lifetime_as ограничены скоупом, в котором мы хотим использовать указатель на что-то. Компилятор понятия не имеет что происходит в других TU, в которых значение по указателю формируется. ХЗ был ли там уже начат лайфтайм или нет.
Так для того же C++17 другого способа и нет. И если компилятор в рамках 17-го стандарта начнет такой UB эксплуатировать, то это будет еще одним свидетельством, что эволюция языка и компиляторостроения пошла куда-то не туда.
Насколько я знаю,
start_lifetime_as
не создает объект, а говорит компилятору о том, что появился новый объект о котором компилятор ранее не знал (т.е. он не был создан через new, как локальная переменная на стеке или каким-то другим известным компилятором способом). Т.е. программист говорит компилятору: вот в этой области памяти сейчас есть объект, пожалуйста, поверь, что это так.В этой связи мне не понятно, чем область памяти, заполненная, например, чтением байт из файла или из пайпа, принципиально отличается от области памяти, в которой объект был ранее сконструирован через placement new.
Я понимаю роль launder вот в таком сценарии: https://wandbox.org/permlink/wh0QVJSGhu41P0LH
И, насколько помню, именно для этого launder и создавался.
Но вот другой сценарий: https://wandbox.org/permlink/wTISrxXoKMlw0PUv
(к сожалению, не удалось протестировать с
start_lifetime_as
, похоже, эту фичу пока еще в компилятор не завезли).Здесь
make_and_use_object
для конструирования объекта вызывает разные функторы. И вот после возврата из функтора что должно применяться --launder
илиstart_lifetime_as
? Ведь каждый из функторов заполняет переданный буфер по-разному?И для
launder
здесь, вроде как, места нет, т.к. не было ранее объектов Data, на которые у меня могли бы быть указатели, и эти объекты бы пересоздавались.В общем, я к тому, что после введения в язык
start_lifetime_as
стало не очень понятно в каком случае требуетсяlaunder
, а в какомstart_lifetime_as
.Да, есть такое дело.
Но ведь в C++23 завозят
std::start_lifetime_as
и, поскольку в статье речь идет о C++26, может бытьstd::start_lifetime_as
уместнее, чемstd::launder
?PS. На самом деле я сам не знаю, что правильно. До C++23 вроде как
std::launder
необходим хотя бы для очистки совести. На вот начиная с C++23...А точно ли здесь нужно использование
std::launder
?Вроде бы объект типа T инстанциируется корректно, затем его тип не меняется, а кастинг из
std::byte*
вT*
полностью легален.Мой ответ вам -- это был сарказм на 146%. И речь шла про то, что в программировании еще никому не удавалось не совершать ошибок.
Неужели и вы про это?
Вы сейчас серьезно?
Так очевидно же, что писать на C++ без утечек памяти, use after free и double free просто: достаточно не совершать ошибок. Делов-то.
Во-первых, сложно и геморройно. Охота за утечками памяти в начале-середине 1990-х годов была одним из основных занятий при разработке на C++.
Во-вторых, умные указатели появились отнюдь не вместе с C++11. Я сам их начал использовать на повседневной основе во второй половине 1990-х (при этом не был пионером, а подсмотрел в чужих разработках, так что умные указатели в мире C++ уже лет тридцать как, если не больше). Ну а самый простой из них под видом std::auto_ptr был прямо в С++98.
ИМХО, этот момент имеет смысл отразить в статье. Потому что выравнивание -- это штука, про которые не все помнят, а кто-то и не знает. Скопируют ваш подход и наступят на грабли где-нибудь.
Ясно-понятно, вопросов больше не имею.
Кем и как этот размер подбирается?
Это утверждение относится к игровым движкам или к программированию на C++ вообще?
Нет ли здесь:
проблем с выравниванием для типа
_type
?Скажем, sizeof(counterSize) у вас 4, а выравнивание для
_type
-- 8.Звучит так, что "долгоживущий код" -- это редкость.
А зачем их брать? Или вы их тоже ревьювите?
Могу ли я предположить, что игровой движок -- это небольшая часть конкретного игрового проекта и тот прессинг багов и нереализованных хотелок, про который упоминает автор статьи, относится не к игровому движку?
https://en.cppreference.com/w/cpp/links/libs -- этот список никакой не "официальный"
Не знаю как сейчас, а несколько лет назад на cppreference можно было зарегистрироваться и внести изменения в wiki-страничку со списком библиотек. Так что там список того, что успели и не поленились добавить на эту wiki-страничку. Попасть в список awesome-cpp чуть сложнее, там (емнип) PR отправлялся и этот PR ждал пока его примут.