Pull to refresh

Comments 70

std::byte storage[(sizeof(T) + ...)];

Троеточие в арифметическом выражении выглядит как псевдокод. Но ведь компилируется!

Только на звание маленько не дотягивает.

Реализуем эффективный тупль с помощью C++26

Надо было

Эффективно туплим с помощью C++26

Идея интересная, но ваш кортеж (не тупль, пожалуйста) не constexpr

Прикольно было бы реализовать обёртку поверх стандартного кортежа, которая бы пересортировывала элементы, тогда она была бы и эффективной по памяти, и constexpr

Совсем прикольно было бы сделать это всё на С++11 (но не уверен что это возможно) :)

В C++ 11 слишком строгий constexpr для функций. Он по умолчанию вешает const квалификатор. Как то пробовал constexpr variant на C++11 написать, но данная вещь не дала в полной мере разгуляться. Исправили такое поведение в C++14.

Я, может быть, ретроград, но называть C++26 современным, по-моему, немного смело.

Шаг 1: Давайте придумаем и запустим в С++ фичу, чтобы было полегче реализовывать туплы!!!

Шаг 2: Смотрите, как легко можно реализовывать туплы на современном C++, а вы и не знали!

(тем временем публика: "Горшочек, не вари...")

Но если 26 это не современная версия, то какая же тогда современная?

Кому как, мне, например (это очень субъективно) - взять стандарт с годом не больше текущего и вернуться ещё на один. В данном случае - с++20, хотя я стараюсь придерживаться 17.

Но уж во всяком случае не стандарт из будущего.

Подскажите, а такое вообще используется в production коде? Это выглядит замысловато, чтобы разобраться в ней нужно неплохо так шарить в соответствующем стандарте. Реализация подобного, мне кажется, будет занимать приличное время. Тот, кто с этим кодом работает должен еще понять как эта штука работает и какие побочные эффекты существуют, разве нет?

насколько, например, я это понимаю, тупли это фактически анонимные структуры (или классы зависит от контекста и от компилятора). То что эта структура анонимная напрочь убивает возможность повторного использования логики которая привязана к некоторому конкретному типу этой тупли! То есть вы будете писать логику обработки определенного набора переменных снова и снова, потому что даже если вы сможете идентифицировать уже существующюю подобную логику, набор переменных вы должны формировать самостоятельно. Фактически это такой способ одновременно скрывать и размножать копи-паст в коде.

Это же целое сражение с ветряными мельницами! Зачем разрабатывать сортировку членов, если можно сразу нормально объявить на стадии написания кода?

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

Так вы не сможете на стадии написания кода. Почему? Да просто замените дабл инт и чар на хз1, хз2 и хз3 и не забудьте о том, что в разных ревизиях их размеры тоже могут быть разными.

Там выше кто то на отладку жалуется, но целый Раст так делает из коробки вообще во всех структурах по умолчанию и всем норм, а на с++ значит не норм?

 а на с++ значит не норм?

не норм, это кривое поведение раста

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

целый Раст так делает из коробки вообще во всех структурах по умолчанию и всем норм

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

Погодите, нафига менять на хз что?! Целевые платформы известны, если есть шанс, что поплывёт разрядность - фиксируйте её! А самопроизвольное изменение структур данных от лукавого, нафиг такие сюрпризы.

А точно ли здесь нужно использование std::launder?
Вроде бы объект типа T инстанциируется корректно, затем его тип не меняется, а кастинг из std::byte* в T* полностью легален.

P3006R1 говорит, что UB в такой конструкции сейчас есть:

alignas(T) std::byte storage[sizeof(T)];
::new (&storage) T();
// ...
T *ptr_ = reinterpret_cast<T*>(&storage);  // UB

Он ссылается на понятие pointer-interconvertible (появилось в P0137R1). На всё это ведут комменты на stackoverflow.

Если шутят, что программирование на Rust - это борьба с компилятором, то надо шутить, что программирование на C++ - это борьба с оптимизатором (который в рамках стандарта может действовать по принципу "вижу UB - не вижу препятствий").

Да, есть такое дело.
Но ведь в C++23 завозят std::start_lifetime_as и, поскольку в статье речь идет о C++26, может быть std::start_lifetime_as уместнее, чем std::launder?

PS. На самом деле я сам не знаю, что правильно. До C++23 вроде как std::launder необходим хотя бы для очистки совести. На вот начиная с C++23...

std::start_lifetime_as нужен для создания объекта с состоянием уже хранящимся в байтах, где он будет размещен. А std::launder это отмывка указателя на ранее созданный объект, для которого в этом месте кода компилятором потеряна информация о типе хранящегося в этих байтах объекта. Разные же смыслы. У вас как раз launder нужен после того, как вы создали объекты через placement new, но типизированные указатели на них нигде не сохранили.

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.

В этой связи мне не понятно, чем область памяти, заполненная, например, чтением байт из файла или из пайпа, принципиально отличается от области памяти, в которой объект был ранее сконструирован через placement new.

Внутренней бухгалтерией компилятора. Если по указателю находится объект, чей лайфтайм с точки зрения компилятора уже начался, то нужно использовать std::launder. Если лайфтайм ещё не начался - нужно использовать std::start_lifetime_as.

Но вот другой сценарий:

Если это код для C++20+, то в обоих случаях должен использоваться std::launder, ибо memcpy там неявно начинает лайфтайм. Если для более раннего стандарта, то там UB.

Внутренней бухгалтерией компилятора.

Жаль, что она как-то явно не описана :(

Если по указателю находится объект, чей лайфтайм с точки зрения компилятора уже начался, то нужно использовать std::launder

Допустим, у нас есть буфер, в который мы загрузили значение из файла/пайпа. После чего передали указатель на этот буфер в две разные функции. Первая в C++23 должна использовать start_lifetime_as. Тем самым она начинает время жизни объекта.

Что должна делать вторая? Вызывать launder, т.к. первая уже лайфтайм для объекта начала? Или можно еще раз вызвать start_lifetime_as.

Плюс тому, launder/start_lifetime_as ограничены скоупом, в котором мы хотим использовать указатель на что-то. Компилятор понятия не имеет что происходит в других TU, в которых значение по указателю формируется. ХЗ был ли там уже начат лайфтайм или нет.

Если для более раннего стандарта, то там UB.

Так для того же C++17 другого способа и нет. И если компилятор в рамках 17-го стандарта начнет такой UB эксплуатировать, то это будет еще одним свидетельством, что эволюция языка и компиляторостроения пошла куда-то не туда.

Жаль, что она как-то явно не описана :(

Ну почему, косвенно описана. В тех же прекондициях к launder написано, что его можно использовать только если лайфтайм объекта по адресу уже начался. Для start_lifetime_as по идее прекондиции те же что для placement new.

Что должна делать вторая? Вызывать launder, т.к. первая уже лайфтайм для объекта начала?

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

Компилятор понятия не имеет что происходит в других TU, в которых значение по указателю формируется. ХЗ был ли там уже начат лайфтайм или нет.

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

Да.

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

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

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

// Где-то в другом 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 была бы даже логичнее.

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

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

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

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

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

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

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

Неизвестно, что происходит в других TU, только пока вы LTO не пользуетесь, а когда/если начнёте вы или сторонний пользователь вашего кода, тут и окажется, что есть что ещё наоптимизировать.

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

  1. Как ни сделай, все равно кто-то недовольный да найдется;

  2. Писать код нужно уже сейчас, а не тогда, когда сделают все идеально (т.е. никогда - см. пункт 1), поэтому хочешь или не хочешь, а подводные камни учитывать нужно;

  3. В пропозале, который призван "все исправить", тоже есть на мой взгляд проблемы. Например, предложение для добавления относится к static_cast, а добавляемый пример зачем-то использует reinterpret_cast. Вроде мелочь, а глаз режет. Далее, там написано, что placement new мол сам по себе выставляет барьер для оптимизаций в шланге, а все остальное - memcpy, memmove - его тоже выставляет? Как-то не доверяю я тщательности проработки вопроса, в общем.

Как ни сделай, все равно кто-то недовольный да найдется

Так ведь это же нововведения в 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++17 и C++20 довели до текущего состояния.

Потому что нашлись недовольные и сказали "почему мы не можем выполнять такие-то оптимизации, если по букве стандарта указатели здесь алиаситься не могут". Сделали оптимизации - нашлись другие недовольные и т.д.

Скажем, если я живу в рамках 17-го стандарта и у меня есть только std::launder, то мне безопасно вставлять std::launder везде.

Нет, не безопасно. std::launder ни в рамках 17 стандарта, ни в рамках более поздних стандартов лайфтайм не начинает, и пользоваться им для отмывания произвольного указателя нельзя было ни тогда, ни теперь. Скажем в примере ниже с malloc std::launder в рамках 17 стандарта ничем не поможет, а попытка его использования без placement new - это UB.

ЗЫ. Собственно, чего хотелось бы иметь:

Ну сейчас правила еще более просты: если уже тем или иным образом время жизни объекта внутри некоего массива байт уже началось - std::launder. Если еще не началось - std::start_lifetime_as.

Без всяких неявных умолчаний, что мол memcpy или malloc начинает время жизни.

Проблема в том что переписывать древнючий C-стайл код типа такого:

struct X { int a, b; };
X *make_x() {
  X *p = (X*)malloc(sizeof(struct X));
  p->a = 1;
  p->b = 2;
  return p;
}

компилируемый, однако, C++ компилятором (а такого полно) никто не будет. Поэтому ряд функций сделали blessed в плане начала лайфтайма. Чисто ради этого старого кода.

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

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

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

Небезопасно с точки зрения буквы стандарта. Но если компилятор в рамках C++17 начнет ломать код, который требовал бы start_lifetime_as из C++23, то это уже вредительство, т.к. возможностей C++17 для покрытия этих ситуаций не хватает. Ну так и нечего ломать то, что работало до.

Т.е. должно работать негласное правило -- да, есть такой UB, но компилятор в рамках стандарта X не должен его эксплуатировать, т.к. устранить этот UB у программиста возможности нет.

Если такого правила нет, значит и комитет, и писатели компиляторов загоняют еще один гвоздь в крышку гроба C++.

Ну сейчас правила еще более просты: если уже тем или иным образом время жизни объекта внутри некоего массива байт уже началось - std::launder. Если еще не началось - std::start_lifetime_as.

Я выше уже привел пример. Советом был изменить архитектуру.
Ну а если менять архитектуру, то почему бы не пойти еще дальше и не послать в пешее эротическое сам C++?

Чисто ради этого старого кода.

Да я как бы в курсе. Только вот что делать, если в проекте у меня вместо malloc-а своя функция для аллокации, а вместо read или memcpy -- своя функция побайтового копирования из COM-порта. Получается, что все звери равны, но некоторые равнее.

Как раз такое недовольство понятно: взяли и поломали то, что работало до того как.

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

Я выше уже привел пример. Советом был изменить архитектуру.

Ну а если менять архитектуру, то почему бы не пойти еще дальше и не послать в пешее эротическое сам C++?

Ну, гипотетических примеров с заведомо неправильной, но временно работающей архитектурой (ибо любой UB может работать довольно долго, пока не перестанет) можно придумать много. Как я и говорю - недовольные всегда найдутся. Одно дело писать (и собирать) под стандарт, где путей решения не было от слова совсем, там наверняка будут сохранять костыли совместимости, ингибировать оптимизации и т.п. Другое дело - писать и собирать под новый стандарт, там можно уже и какие-то архитектурные изменения внести, а не просто тупо заменить один --std= на другой.

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)

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

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

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

Этим вариантом могут оказаться (и скорее всего окажутся) недовольны компиляторостроители. ИЧСХ, их аргументы сходу отметать, видимо, не получается.

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

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

Я к тому, что они, видимо, тоже по-своему обоснованы.

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

Я вот тут ворвусь в разговор, я вижу, что вы прекрасно понимаете, что и отмывка и начало жизни это просто декларации тела этих функций тождественны айдентити. Это значит что все эти заморочки существуют только в компил тайме и то не во всех компиляторах. Это я к чему? А к тому что УБ настоящее в вашей программе из вакуума ветром не надаует к вам в бинарь. Единожды собранный и протестированный бинарь продолжит работать как надо несмотря ни на какие извращения. А если ещё проще то ни отмывка ни начало жизни использованные вами правильно и по назначению не дают индульгенцию на не тестирование. И обратное тоже верно если вы протестировали код с якобы УБ такого рода и он работает так как вы ожидаете то смело можете слать в пешее всех кто кричит про УБ в вашем коде.

Мне во многом нравится Раст, но там тоже есть подобные мегко говоря странности, например взятие (но без последующего использование) мут указателя на объект у которого есть действующие конст ссылки обьявленно УБ само по себе. Я думаю в рамках все той же логики компиляторщиков, типа нехер лезть в наши закрома даже если мы ещё не придумали как это использовать против вас сегодня ради оптимизаций, но в будущем мы то точно придумаем. Я вот больше практик, работает сегодня и по другому никак/дорого/да просто лень ну и чё? Будет конкретно проблема будем исправлять, а пока и так сгодится, а лондерами пусть пользуются сначало те кто их придумал и на реальных примерах покажут как правильно а как нет.

Слишком дорого тестировать каждый билд. Например, UB сработает, если определённая инструкция располагается на адресе, кратном 16.

И вот, мы всё протестировали, потом появляется задачка доделать одну мелочь, которая никак не связана с UB-функцией. Новый код тестируется - там всё хорошо. А старый код сломался, потому что попал на другой адрес.

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

Это подход примерно как "давайте не использовать мьютексы для доступа к общим данных, пока сами лично не столкнёмся с гонкой данных". Обычно же и без них всё работает. Вот когда на нашем проекте возникнут реальные проблемы, тогда и будем учитывать этот аспект.

Есть очевидные UB: ... type-puning через union

О, моя любимая мозоль. В C этот приём разрешён, потеря гарантий на уровне C на самом деле неочевидна. Оптимизации, ради которых могли пожертвовать гарантиями, тоже неочевидны. Если так решили упростить текст стандарта, то... это тоже неочевидно. То, что замена на memcpy избавляет от UB - по духу стандарта снова неочевидно, соответствующий пункт стандарта избегает описания memcpy между разными типами (T* вместо T1* и T2*), это замечал один из докладчиков на CppCon.

Воспользоваться гипотетическими оптимизациями из-за масштаба "трагедии" нельзя, можно пройтись по гитхабу:language:C++ /(?-i)union/. По-хорошему UB в нынешнем виде должен вызываться лишь новым атрибутом типа [[assume_no_punning]] специально для реализаций tagged union'ов. Обратная совместимость сохраняется. Можно учитывать новое правило только в новом коде. Исчезает нездоровая и отнимающая у всех время ситуация, когда стандарт не описывает поведение компиляторов, и когда отдельные энтузиасты пытаются вычерпать море (такой type punning везде, и в браузере тоже).

То, что замена на memcpy избавляет от UB - по духу стандарта снова неочевидно, соответствующий пункт стандарта избегает описания memcpy между разными типами

Ну я бы не сказал, что это прямо "соответствующий пункт".

23.5.3 Header <cstring> synopsis

[...]

The contents and meaning of the header <cstring> are the same as the C standard library header <string.h>.

OK, лезем в соответствующий раздел скажем C23 Working Draft:

7.25 String handling <string.h>

[...]

The header <string.h> declares one type and several functions, and defines one macro useful for manipulating arrays of character type and other objects treated as arrays of character type.

[...]

For all functions in this subclause, each character shall be interpreted as if it had the type unsigned char (and therefore every possible object representation is valid and has a different value).

Т.е. выстраивается цепочка: при вызове memcpy(T1*, T2*, N) происходит сначала конверсия из T1* и T2* в void*, а затем из void* в unsigned char*, что эквивалентно (см. пункт 5) reinterpret_cast<unsigned char*>(Tx*).

Далее, в этом пункте 5 написано (вместе со ссылкой на то, что означает понятие "type-accessible"), что доступ по результирующему указателю может безопасно осуществляться, если результирующий указатель после конверсии будет в том числе типа char*, unsigned char* или std::byte*. То есть получается, что использование memcpy для "превращения" T1 в T2 вполне себе well-defined (если, конечно, оба этих типа trivially copyable). Например, такой условный код для type punning вполне легален даже в C++11, еще до всех этих уловок с blessed functions:

T1 t1 = [...]; // Вызван конструктор, начался лайфтайм
T2 t2; // Вызван конструктор, начался лайфтайм
memcpy(&t2, &t1, sizeof(t2)); // OK

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

Раздел с тем пунктом описывает требования к представлению типов. И, внезапно, в отличие от аналогичного места в C, разрешает копирование* в символьный буфер и обратно && копирование* в объекты того же типа.

  • Это уже разрешено, потому что разрешён доступ** через char.

  • Зачем тогда повторное разрешение? Можно усомниться - потому что иные действия (побайтовое копирование в другой тип)... не очень определены? И некоторый бардак в стандарте может подкрепить эти сомнения - bit_cast так же избегает ссылок на другие разделы, словно аналогичные эффекты у стандартного и/или самописного memcpy не определены (курсив в скобках чуть подкрепляет и эти сомнения):

    • "Padding bits of the result are unspecified" - дублирует "Padding bits have unspecified value" из примечания (но примечания не нормативны)

    • "if there is no value of the object’s type corresponding to the value representation..." (дублирующей фразы не нахожу)

  • Или через эти разрешения хотели лишь запретить побайтовое копирование в случае нетривиально копируемых типов? Тогда пригодилось бы очередное "The intent is that...". Хотя бы.

* побайтовое копирование объектов тривиально копируемых типов, точнее говоря.
** с тем новым термином "type-accessible" (+ссылка на стандарт вместо cppreference).

-----
Это ж какая языко-юридическая практика нужна. И standardese на уровне носителя языка.

-----
Что ещё касается union, Страуструп говорит, что каламбуры только через его труп категорически против легализации существующей практики.

Зачем тогда повторное разрешение? Можно усомниться - потому что иные действия (побайтовое копирование в другой тип)... не очень определены?

Ну побайтовые копирования в другой тип и правда "не очень определены", потому что ЕМНИП нельзя например "в лоб" превратить любой набор байт в long double, потому что там есть запрещенные паттерны, как минимум в x86-64. А вот если скопировать объект в байтовый буфер, а потом обратно, или побайтово скопировать объекты одинакового типа один в другой, то тут предоставляются максимально возможные гарантии насчет значения результирующего объекта, так сказать. Описание std::bit_cast это просто более явно проговаривает.

Ну побайтовые копирования в другой тип и правда "не очень определены"

Описание std::bit_cast это просто более явно проговаривает.

По-моему, получилось так, что только в [bit.cast] описывают некоторые правила, которые на самом деле должны быть общими (охватывать и самописный memcpy), должны быть быть в разделе о представлении типов, как было в C (ссылка на ту же страницу, п. 5-6, upd: и 8? "unspecified which representation is used").

Интересно, что ограничили начало жизни объекта определёнными функциями хотя обычно в C++ стараются делать такое расширяемым.

Было бы уместней ввести атрибут и тогда пользовательские функции как скажем HeapAlloc могли бы работать также как и malloc.

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

В стандарте C++ поведение reinterpret_cast описывается через static_cast. Первоочередная задача предложения убрать UB при reinterpret_cast масива байт после placement new. Соответственно дорабатывается wording для static_cast, и ставится пример с reinterpret_cast с целью подсветить "вот так должно работать!"

все остальное - memcpy, memmove - его тоже выставляет?

Зависит от происходящего. Предложение launder less не про все случаи, а про один конкретный, с которым косячат прям все пользователи.

Если хотите улучшать другие места - это похвально! Пишите proposal, прорабатывайте вопрос - все вам спасибо скажут!

А как вы себе представляете этот launder_less в случае отдельных TU? В одном сделали placement new, в другом сделали reinterpret_cast, как компилятор в TU2 узнает, что в TU1 в сторадже делался placement new, если в TU2 может вообще не попасть код с placement new, поэтому цепочка reinterpret -> launder не нужна?

Сейчас все основные компиляторы имеют Link-Time Optimization в конфигурациях -O2 -O3. Компиляция лишь валидирует исходник и конвертирует его в некое представление. А линковка генерит код, оптимизируя вызовы между TU.

Не все пользуются LTO, т.к. она очень сильно тормозит сборку, а валидность кода не должна зависеть от опций компилятора. Попробуйте найти, например в Conan Centre пакеты, у которых в рецепте изначально включена LTO. Или жирные (т.е. содержащие не только бинарный код, но и IR) библиотеки в составе системных пакетов на Linux. Кто будет это всё переделывать?

валидность кода не должна зависеть от опций компилятора

Валидность корректного кода.
На то оно и UB, чтобы в debug работать, а в release падать.

Ровно так, как это работает сейчас - вызов функции заставляет компилятор перечитать значение https://godbolt.org/z/WM45Kb8Ga

Или вы про что-то другое? Напишите пожалуйста кодом

Уж не этого я ожидал от эффективного тупла на С++26...

alignas(T...) std::byte storage[(sizeof(T) + ...)];

если у вас tuple<int32_t, char>, то размер будет 5, это неправильно (он должен быть 8)

Плюс ручная реализация конструктора, деструктора и операторов гораздо хуже чем встроенная от компилятора (по многим причинам), мув конструктора и копирования вообще не видно (по дефолту сгенерированный это уб)


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

то размер будет 5, это неправильно (он должен быть 8)

Хорошее замечанение. Если это так, то серьёзная ошибка. Я проверил на godbolt, размер 8.
Видимо, когда компилятор видит alignas перед массивом, он корректирует его sizeof до этого выравнивания.

Видимо, когда компилятор видит alignas перед массивом, он корректирует его sizeof до этого выравнивания.

Это не так работает. sizeof(storage) будет 5, но вот если этот storage является полем структуры, то тогда размер самой этой структуры будет 8 из-за alignas.

Сортировать члены кортеже - так себе идея. Кому надо, тот изначально отсортирует, а остальным же такая сортировка может создать неожиданности в отладке.

Статью можно рассматривать как упражнение в метапрограммировании.
В случае, когда надо сохранить порядок полей, можно вернуться к std::tuple.
Пример, когда порядок полей важен, это синтаксис инициализации. Например, определённый порядок полей "красиво" вписывается в логику предметной области, а поменять его ради оптимального хранения в памяти - каждый раз глаз будет спотыкаться.

Теперь вы знаете, как легко и эффективно реализовать тупль на собеседовании!

Если кто-то на собеседовании напишет такое, то навряд ли он получит позицию. 😉

Основание отказа - overqualified?

Может быть, но скорее - tends to overcomplicate things. Не видел я, чтобы на собеседованиях нужно было писать что-то подобное. Да и в реальной жизни тоже, хотя, конечно, это зависит от проекта.

Так грамотный разработчик, прежде чем бросаться писать код, должен договориться на берегу - требуется ли от tuple оптимизация размещения в памяти или требуется сохранение порядка полей.

Замечение по коду -

  constexpr tuple(T&&... args) {
    initialize_storage(std::make_index_sequence<sizeof...(T)>(), std::forward<T>(args)...);
  }

Здесь std:: forward не нужен, у вас здесь всегда rvalue ссылки. Можно обойтись std::move.

Чтобы конструировать tuple не из rvalue ссылок, то нужен шаблонный конструктор. Там и нужно использовать std::forward:

  template<typename... U>
  constexpr tuple(U&&... args) {
    initialize_storage(std::make_index_sequence<sizeof...(T)>(), std::forward<U>(args)...);
  }

И тогда придётся переделать initialize_storage, чтобы она принимала эти forward-нутые типы

Sign up to leave a comment.

Articles