Как стать автором
Обновить
88
0
Евгений Охотников @eao197

Велосипедостроитель, программист-камикадзе

Отправить сообщение

Например, стратегией может быть наличие affinity треда пушащего работу с каким-либо тредом внутри тредпула

Может проще было эту мысль выразить непосредственно на английском языке? Зачем этот жалкий транслит русскими буквами?

В нашем примере, если автор сторонней библиотеки предоставляет интерфейс, совместимый с std::execution

Вообще-то здесь сразу два "если":

  • если автор библиотеки выставляет наружу интерфейс для интеграции с использованным внутри библиотеки тред-пулом;

  • если этот самый интерфейс совместим с std::execution.

Можно предположить, что в большом количестве случаев все закончится еще на первом "если".

Я понимаю. Но моя точка зрения, что пользователей языка на пару порядков больше, чем компиляторописателей, а их средний уровень гораздо ниже. Если сделать лучше нескольким миллионам пользователей языка ценой ущерба для нескольких тысяч разработчиков компиляторов, то это было бы разумно. ИМХО.

их аргументы сходу отметать, видимо, не получается.

Скорее всего так и есть.

UB - это такая штука, да. Сегодня работает, а завтра нет.

Так ведь есть и другой способ: перестать считать какое-то поведение UB и легализовать его.

ибо любой UB может работать довольно долго, пока не перестанет

Боюсь, здесь нужно начинать разбираться в сортах говна.
Есть очевидные UB: чтение из неинициализированных переменных, обращение по невалидным указателям, type-puning через union и пр. Что характерно, объяснить почему так делать -- это фу-фу-фу -- не так уж и сложно.

Есть менее очевидные, но все же вполне объясняемые UB. Вроде переполнения знаковых целых. Что характерно, объяснить почему это таки UB, и почему нельзя поступить как с беззнаковыми уже тяжелее.

И есть совсем неочевидные UB. Ну вот типа этих самых lifetime. Тут сразу две проблемы:

a) про них вообще мало кто знает;
b) даже тем, кто про такие UB знает, непросто объяснить другим почему же это UB.

Типа такого:

alignas(Demo) char raw_bytes[sizeof(Demo)];
read_from_some_source(raw_bytes, sizeof(Demo));
Demo * d = reinterpret_cast<Demo *>(raw_bytes);

Это же, в принципе, прямой донельзя код. Причем, судя по декларациям назначения C++, это как раз такой код, который должен писаться на C++ легко и непринужденно.

Но здесь в дело вступают заморочки компиляторописателей. И оказывается, что программист должен думать о том, чтобы компилятор узнал, что где-то начинает жить объект типа Demo. При том, что компилятор здесь программисту помочь вообще никак не может.

И это при том, что без малого 40 лет до появления C++23 все это работало, а тут вдруг все, бабушка приехала (c)

Как я и говорю - недовольные всегда найдутся.

ИМХО, некоторое недовольство, высказанное вслух, может быть конструктивным, т.к. может указывать, что направление движения выбрали не туда.

Сделали оптимизации - нашлись другие недовольные и т.д.

Как раз такое недовольство понятно: взяли и поломали то, что работало до того как. То же, что дали, не покрывает все проблемы. Т.е. взялись делать "хорошо" и недодумали. О чем и речь.

Нет, не безопасно.

Небезопасно с точки зрения буквы стандарта. Но если компилятор в рамках 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 начинает время жизни.

Да я вообще редкостный урод, смею утвержать, что в C++ что-то сделали через жопу недодумав.

"Доктор, когда я делаю так, у меня тут болит"

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

Но вообще, конечно, лучше продумать архитектурку так

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

Да.

Это может быть тупо невозможно, т.к. каждая функция может работать в своем треде и вообще ничего не знать про другую.

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

Так даже в таком тривиальном случае:

// Где-то в другом 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, но типизированные указатели на них нигде не сохранили.

Я понимаю роль 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.

ИМХО, этот момент имеет смысл отразить в статье. Потому что выравнивание -- это штука, про которые не все помнят, а кто-то и не знает. Скопируют ваш подход и наступят на грабли где-нибудь.

Ясно-понятно, вопросов больше не имею.

Кем и как этот размер подбирается?

1
23 ...

Информация

В рейтинге
4 567-й
Откуда
Гомель, Гомельская обл., Беларусь
Зарегистрирован
Активность