Есть в C++ такие штуки, которые вроде как существуют в стандарте, но некоторые о них даже не задумываются, пока не наткнутся на что‑то совсем странное. Вот, например,std::launder
. Что это вообще? Стирка чего‑то грязного в коде (launder)? Или std::as_const
— зачем делать объект «немного более константным»?
На днях решил покопаться в этих функциях, потому что, честно говоря, они звучат интересно. Так что сегодня расскажу, что я выяснил, зачем это всё нужно, и главное — как использовать эти штуки правильно.
Зачем нужен std::launder?
Допустим, вы создаете объект с помощью placement new поверх уже существующего объекта. Вроде бы всё работает. Но где‑то глубоко в недрах кода зреет коварное неопределённое поведение (UB, как его ласково называют на Stack Overflow). Особенно если у старого объекта были const
‑члены или ссылки.
Вот тут хорошо зайдет std::launder
. Эта функция гарантирует, что доступ к новому объекту через старый указатель не приведёт к катастрофе.
#include <new>
#include <iostream>
struct MyClass {
const int value;
MyClass(int v) : value(v) {}
};
int main() {
alignas(MyClass) char buffer[sizeof(MyClass)];
new(buffer) MyClass(42);
MyClass* p = reinterpret_cast<MyClass*>(buffer);
std::cout << p->value << std::endl; // Может привести к неопределённому поведению
p = std::launder(reinterpret_cast<MyClass*>(buffer));
std::cout << p->value << std::endl; // Всё ок
return 0;
}
Вот что тут происходит: мы создаём объект MyClass
в заранее выделенном буфере. Но если попытаться обратиться к его члену value
через указатель p
, полученный через reinterpret_cast
, может произойти что угодно. Почему? Потому что C++ так решил. А вот std::launder
убирает этот фокус и делает всё нормально.
std::launder
возвращает указатель на объект, находящийся по тому же адресу, что и указатель p
, но при этом гарантирует, что этот объект — «новый». Суть простая: он говорит компилятору, мол, «всё под контролем, можно доверять».
Если формально:
p
указывает на адресA
в памяти.По адресу
A
лежит объектx
.x
находится в пределах времени своей жизни.Тип
x
совпадает сT
(игнорируяconst
и прочее).Тогда
std::launder(p)
возвращает указатель наx
. Всё просто, если закрыть глаза на тонкости.
Когда использовать std::launder?
Вот типичные ситуации:
Placement new. Вы создали новый объект поверх старого, и нужно к нему нормально обращаться.
Вы хотите перепривязать указатель к новому объекту, созданному в той же области памяти.
А что с std::as_const?
Это простая, но полезная штука. std::as_const
— это функция для ленивых (или предусмотрительных). Она берёт объект и превращает его в const
, не изменяя сам объект.
#include <iostream>
#include <utility>
void print(const int& value) {
std::cout << value << std::endl;
}
int main() {
int x = 42;
print(std::as_const(x)); // Передаём x как const int&
return 0;
}
Тут std::as_const(x)
берёт переменную x
и превращает её в const int&
, чтобы мы могли вызвать print
.
Как это работает?
Очень просто. Определение функции выглядит вот так:
template< class T >
constexpr std::add_const_t<T>& as_const(T& t) noexcept {
return t;
}
То есть берём неконстантную ссылку на объект t
и возвращаем его как константную ссылку. Всё.
Когда использовать
Используйте, когда:
Нужно вызвать константную версию метода.
Надо передать объект в функцию, которая ожидает
const
-ссылку, но сам объект менять не хочется.
Итоги
std::launder
и std::as_const
— это такие маленькие, но мощные инструменты. Если вам есть чем поделиться на эту тему, пишите в комментарии, интересно послушать!
По C++ в Otus пройдут следующие открытые уроки, записывайтесь, если интересно:
3 декабря: WebAssembly и C++: разработка высокопроизводительных веб-приложений. Записаться на урок
17 декабря: Обзор фреймворка userver. Записаться на урок
Комментарии (14)
comargo
03.12.2024 14:10у std::as_const() есть еще одна особенность:
int func(const int& val) { return val; } int func2() { return 42; } int main() { func(42); func(func2()); // func(std::as_const(func2())); <--- Error // func(std::as_const(42)); <--- Error }
pi-null-mezon
03.12.2024 14:10Никогда не пользовался ни первым, ни вторым. И честно говоря, кажется, что без них можно обойтись в 99.99 % случаев. Поправьте меня, если это не так.
Videoman
03.12.2024 14:10std::launder используется в библиотечном коде, когда нужно уплотнится и переиспользовать память, в каком-нибудь std::variant или другом типе-сумме. std::launder - внутри использует компиляторозависимую директиву. Не представляю как это можно написать самому.
Без std::as_const можно обойтись, но нужно будет писать свою. А зачем если стандартная есть ?! Используется в основном в дебрях шаблонов в обобщенном коде, где имя типа может быть очень длинным.
Если вы не писали такой сложный библиотечный код, то вам это действительно не нужно.Playa
03.12.2024 14:10Пример без шаблонов - Qt, где при написании чего-то вроде
for (auto & el : cont)
произойдет копирование всего контейнера, если он не const (из-за CoW).
NeoCode
03.12.2024 14:10Выглядит как будто это какие-то костыли к языку, который уже давно прогибается под огромным гнетом "обратной совместимости".
KanuTaH
03.12.2024 14:10Ну это потому что примеры подобраны достаточно бредовые и только запутывают читателя, потому что в первом примере не нужен
std::launder
, а во втором примере не нуженstd::as_const
. Более жизненным примером дляstd::launder
был бы, скажем, какой-то класс-обертка, который внутри себя предоставляет буфер для хранения другого произвольного класса, но при этом по каким-то причинам не хочет хранить еще и указатель, возвращаемый placement new (сильно хочет память сэкономить например). Что касаетсяstd::as_const
, то в первую очередь приходит в голову такой пример:SomeType foo; auto bar = [&foo = std::as_const(foo)](...){...};
то есть мы в лямбде хотим захватить
foo
по ссылке, но не по простой, а по константной, чтобы даже случайно внутри лямбды ничего внутриfoo
не поменять. Или такой пример:struct SomeType { void foo() {...}; void foo() const {...}; }; SomeType bar; bar.foo(); // Вызовет обычную перегрузку std::as_const(bar).foo(); // Вызовет const перегрузку
ZirakZigil
03.12.2024 14:10Для лаундера можно ещё каноничный пример получения start_lifetime_as без 23 стандарта вспомнить:
tempalte<typename T> // trivially copyable + implicit lifetime T *start_lifetime_as(void *p) { std::byte buf[sizeof(T)]; std::memcpy(buf, p, sizeof(T)); new (p) std::byte[sizeof(T)]; std::memcpy(p, buf, sizeof(T)); return std::launder(static_cast<T*>(p)); }
kenomimi
03.12.2024 14:10C++ это как квантмех. Вроде кодишь приклад и думаешь, "я знаю C++ хорошо". Открываешь исходники стандартной либы. Видишь эльфийские письмена, читать которые сложнее ассемблера. Закрываешь. Понимаешь, что ты не знаешь C++ вообще.
std::launder как раз из этой оперы - только седые бородатые гуру с вершин далёких гор знают, что это, и использовали это на практике...
Ilya_JOATMON
03.12.2024 14:10ППКС. Вы еще не видели во что это в машинные коды транслируется. Глянешь в декомпилятор и вздрогнешь.
boldape
03.12.2024 14:10Если я правильно понимаю зачем он нужен, то как раз в рантайме вообще никакого кода лондер не генерирует. Он нужен исключительно компилятору что бы он чего лишнего не оптимизировал.
Тут интереснее, если при использовании лондера ассм код не меняется то либо вы его используете там где он не нужен либо компилятору на него пофиг. С другой стороны если код поменялся значит именно этот компилятор таки что то попытался соптимизировать лишнего, а вы ему успешно сказали, что так было нельзя.
elixirkmc
03.12.2024 14:10В примере с
std::as_const
совершенно непонятно, зачем он там нужен. И так можно вызватьprint
, внутри которой константность работает самым обычным образом.
unreal_undead2
03.12.2024 14:10new(buffer) MyClass(42);
MyClass* p = reinterpret_cast<MyClass*>(buffer);
Почему не MyClass* p =
new(buffer) MyClass(42);
sha512sum
Обычно в использовании std::launder с такой ситуаций нет необходимости. Если там объект попадает в https://eel.is/c++draft/basic.life#8, тогда согласно правилу в https://eel.is/c++draft/basic.life#9, все указатели, ссылки, имена будут указывать на новый объект. Но если там complete const было, тогда уже понадобится использовать std::launder для перепривязки.
Ну или все остальные случаи, когда такое происходит(например там другие типы совсем). То что оно перепривязывается, позволяет не дублировать логику в оператор= для копирования и перемещения, можно взять логику из конструктора:
И использования такого оператора= будет полностью легально, будет работать в constexpr в том числе.
Ещё в статье не хватает более подробного объяснения формальной части этого всего, с ссылками на стандарт.