Есть в 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. Записаться на урок