В комментариях к статье на Хабре зашел разговор об уместности использования std::launder и std::start_lifetime_as.
И, насколько я смог понять из разговора, вот в такой ситуации у нас нет сейчас простого и понятного способа в точке (1) сделать каст указателя к Demo*:
#include <iostream> #include <functional> #include <new> #include <cstddef> #include <cstring> #include <memory> struct Data { int _a{0}; int _b{1}; int _c{2}; }; using CreateFn = std::function<void(std::byte*)>; void make_and_use_object(CreateFn creator) { alignas(Data) std::byte buffer[sizeof(Data) * 3]; std::byte * raw_ptr = buffer + sizeof(Data); creator(raw_ptr); Data * obj = std::launder(reinterpret_cast<Data *>(raw_ptr)); // (1) std::cout << "obj: " << obj->_a << ", " << obj->_b << ", " << obj->_c << std::endl; } int main() { make_and_use_object([](std::byte * ptr) { new(ptr) Data{._a = 1, ._b = 2, ._c = 3}; }); make_and_use_object([](std::byte * ptr) { Data data{ ._a = 25, ._b = 16, ._c = 890}; std::memcpy(ptr, &data, sizeof(data)); }); } |
Суть в том, что когда make_and_use_object вызывается с первой лямбдой и по указателю ptr новый объект Data создается через placement new, то затем raw_ptr нельзя просто так скастить к Data*. Тут требуется именно std::launder. Ну вот требуется и все. Иначе UB.
Тогда как при использовании второй лямбды в точке (1) вообще не нужно вызывать ни std::launder, ни std::start_lifetime_as. Вроде как все дело в том, что std::memcpy неявно начинает время жизни нового объекта. И результирующий указатель можно просто скастить к Data* и все.
Но, допустим, что во второй лямбде я не использую std::memcpy, а заполняю переданный мне буфер собственной функцией побайтового чтения из COM-порта. Что-то вроде:
make_and_use_object([](std::byte * ptr) { com_port_reader reader; reader.init(); while(reader.has_data()) { *ptr = reader.read_next_byte(); ++ptr; } }); |
Естественно, ничего из этого в стандарте не описано и компилятор понятия не имеет, начинается ли здесь какой-нибудь lifetime или нет.
Соответственно, что делать в make_and_use_object после завершения работы лямбда-функции?
Если содержимое объекта внутри буфера было сформировано побайтовым чтением из COM-порта, то std::launder не поможет, тут нужен как раз std::start_lifetime_as (который завезли в C++23, но который, если не ошибаюсь, пока нигде не реализован).
Т.е. мы оказываемся в ситуации, когда у нас есть корректно размещенный в памяти байтовый буфер, внутри которого лежит корректно сформированное значение объекта. Но мы не можем одним простым действием взять и получить легальный указатель на этот объект.
Хотя, казалось бы, C++ -- это такой язык, в котором подобные операции должны были бы делаться легко и просто. Ага, щаз... 🥴 С подачи компиляторописателей в язык напихали столько UB, что подобный кастинг указателя стал отнюдь не простым.
Для полноты картины: там же в комментариях указали на наличие пропозала от Антона Полухина. Смысл этого пропозала -- отказаться от необходимости использовать std::launder вот в таких простых ситуациях:
alignas(T) std::byte storage[sizeof(T)]; auto* p1 = ::new (&storage) T(); auto* p2 = reinterpret_cast<T*>(&storage); bool b = p1 == p2; // b will have the value true. |
Если этот пропозал примут, то ситуация окажется совсем веселая:
- до C++26 подобный кастинг без std::launder будет считаться UB. Т.е. если вы пишете под C++17 или C++20, то должны использовать std::launder, иначе в вашем коде формальный UB;
- начиная с C++26 это уже не UB и можно std::launder не писать.
А теперь представим проект (какую-нибудь библиотеку, вроде RapidJSON), который должен собираться и под C++14, и под C++17, и под C++20, и под C++23, и под C++26. И как в таком проекте быть со всеми этими std::launder и std::start_lifetime_as? Кроме как прятать подобные фокусы за фасадом макросов мне ничего в голову не приходит.
Но пусть даже у нас есть набор нужных вспомогательных макросов... Вернемся к самому первому примеру в посте. Как там понять, требуется ли std::launder, std::start_lifetime_as или же вообще ничего не требуется?
Есть ощущение, что никак. Подобный код нужно перепроектировать. А это мне лично не нравится, ибо тогда мы начинаем заниматься не решением собственных задач, а ублажением компилятора, чтобы компилятор нидайбох не подумал, что у нас есть UB, которое можно эксплуатировать.
Собственно, чего хотелось бы иметь:
- чтобы std::launder оставался только для случаев, когда пересоздается объект. Т.е. был объект типа A и на него были указатели, затем на том месте, где был объект A, создали новый объект A (или даже какой-то отнаследованный от него B -- пример), старые указатели "протухли", нужно их "отмыть" через std::launder. Все. Больше ни для чего std::launder не нужен;
- чтобы std::start_lifetime_as использовался для случая, когда у нас есть std::byte* или char*, и мы хотим сказать компилятору, что по этому указателю реально живет объект A.
И строго так, без всяких неявных умолчаний, что мол memcpy или malloc начинает время жизни.
Но, боюсь, комитет по стандартизации под давлением разработчиков компиляторов добавит нам еще массу ярких ощущений в борьбе с UB. Особенно с такими, про которые ты даже не догадываешься.
PS. Да, я в курсе, что практику с неявным началом времени жизни при использовании ряда функций (вроде malloc или memcpy) в C++20 ввели для того, чтобы узаконить говнокод, написанный в древности или даже вообще на чистом Си (как в случае с GCC -- сперва это был Сишный код, а потом еще стали компилировать как C++). Но ведь C++ все равно потихоньку отказывался от атавизмов, например, ключевое слово auto кардинально поменяло свой смысл, а ключевое слово register сейчас нельзя использовать по его первоначальному назначению. Так что лично для меня этот аргумент из категории "ну такое себе". А если какие-то комитетчики настаивают на то, что этот говнокод нужно оставить как есть и сделать легальным (да еще и за счет умолчаний, про которые мало кто знает), то хочется сказать таким комитетчикам: ну так оставьте легальным и тот говнокод, в котором std::launder не используется.
PPS. На всякий случай ссылки на посты двухгодичной давности, в которых обсуждалась проблематика std::launder: "Продолжение темы про передачу C++объектов через shared memory. Промежуточные выводы" и "В склерозник: ссылки на тему std::launder"
Комментариев нет:
Отправить комментарий