Я пытался реализовать свой собственный небольшой распределитель для целей тестирования, и при его разработке я думал, что не знаю, как реализовать его, не нарушив строгое правило псевдонимов.
В большинстве проектов с открытым исходным кодом на GitHub метод распределителя C++ allocate<T> представляет собой адаптер памяти: он обращается к памяти, запрашивает N последовательные std::byte или unsigned char, а затем reinterpret_cast передает эту память T, а после этого отдает память. обратно к вызывающему абоненту.
Однако как это не нарушит строгое правило псевдонимов (хотя вызываемый объект должен сам вызывать конструкторы, мы приводим std::byte* к T*). Как можно обойти эту проблему при реализации простого распределителя буфера, подходящего для большинства контейнеров STL?
Связанное/обман: Реализация контейнера типа std::vector без неопределенного поведения
Строгое псевдонимирование заключается не в переосмыслении указателей, а в их разыменовании. Простой возврат указателя на необработанную память не нарушает строгий псевдоним.
Программисты на C часто говорят, что невозможно написать эквивалент malloc на совместимом C. По той же причине: в какой-то момент вы должны найти немного памяти и решить, что 1/ она изначально не имеет типа и 2/ вы можете ее благословить в один тип. Но по соображениям безопасности программам пользовательского уровня это не разрешено.
Это нормально, потому что после allocate вы construct помещаете объект в выделенную память.
Реализация может связать ту или иную форму неопределенного поведения с каким-либо надежным результатом, а затем использовать тот факт, что она это сделала. Таким образом, обходной путь не требуется. Другими словами, компилятор и (внутренние части) реализации стандартной библиотеки могут свободно использовать конструкции с неопределенным поведением, если они обеспечивают требуемое поведение (например, в данном случае std::allocate<T> ведет себя так, как требуется программисту, использующему компилятор). и стандартная библиотека).





std::allocator<T>::allocate не создает объекты типа T1, std::construct_at создает. Это нормально, чтобы reinterpret_cast от void * до T *, если затем вы продолжаете начинать жизнь T в указанной памяти.
allocate начинает жизнь массива T, но не T подобъектов этого массива.Строгий псевдоним нарушается, когда вы делаете вид, что в определенном месте памяти есть объект, но на самом деле его нет. (Вы делаете это, reinterpret_cast указывая указатель/ссылку, а затем разыменовывая его, где только само разыменование является UB.)
Это не мешает вам использовать Placement-new, чтобы изменить тип объекта, хранящегося в некоторой памяти, а затем получить к нему доступ.
Например:
#include <string>
int main()
{
alignas(std::string) char buf[sizeof(std::string)];
*reinterpret_cast<std::string*>(buf) = "foo"; // UB
std::string *ptr = new(buf) std::string;
*ptr = "foo"; // Legal.
}
Мне кажется, упоминание std::launder здесь также может быть уместным.
@JesperJuhl Пожимаю плечами. Это в некоторой степени связано, но вы можете использовать распределители и без него.
Реализация(и) не обязательно должна следовать правилам C++!