Есть в 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 пройдут следующие открытые уроки, записывайтесь, если интересно:

Комментарии (14)


  1. sha512sum
    03.12.2024 14:10

    Вы хотите перепривязать указатель к новому объекту, созданному в той же области памяти.

    Обычно в использовании std::launder с такой ситуаций нет необходимости. Если там объект попадает в https://eel.is/c++draft/basic.life#8, тогда согласно правилу в https://eel.is/c++draft/basic.life#9, все указатели, ссылки, имена будут указывать на новый объект. Но если там complete const было, тогда уже понадобится использовать std::launder для перепривязки.

    Ну или все остальные случаи, когда такое происходит(например там другие типы совсем). То что оно перепривязывается, позволяет не дублировать логику в оператор= для копирования и перемещения, можно взять логику из конструктора:

    struct SomeStruct {
      int* ptr;
      constexpr SomeStruct(int value) : ptr(new int{value}) {};
      constexpr SomeStruct(SomeStruct&& other) : ptr(other.ptr) {
        other.ptr = nullptr;
      };
      SomeStruct(const SomeStruct&) = delete;
      constexpr ~SomeStruct() {
        if(this->ptr) {
          delete this->ptr;
        };
      };
      constexpr auto operator=(SomeStruct&& other) -> SomeStruct& {
        if(&other == this) {
          return *this;
        };
        std::destroy_at(this);
        std::construct_at(this, std::move(other));
        return *this;
      };
    };


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


    Ещё в статье не хватает более подробного объяснения формальной части этого всего, с ссылками на стандарт.


  1. 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
    }


  1. pi-null-mezon
    03.12.2024 14:10

    Никогда не пользовался ни первым, ни вторым. И честно говоря, кажется, что без них можно обойтись в 99.99 % случаев. Поправьте меня, если это не так.


    1. Videoman
      03.12.2024 14:10

      std::launder используется в библиотечном коде, когда нужно уплотнится и переиспользовать память, в каком-нибудь std::variant или другом типе-сумме. std::launder - внутри использует компиляторозависимую директиву. Не представляю как это можно написать самому.

      Без std::as_const можно обойтись, но нужно будет писать свою. А зачем если стандартная есть ?! Используется в основном в дебрях шаблонов в обобщенном коде, где имя типа может быть очень длинным.

      Если вы не писали такой сложный библиотечный код, то вам это действительно не нужно.


      1. Playa
        03.12.2024 14:10

        Пример без шаблонов - Qt, где при написании чего-то вроде for (auto & el : cont) произойдет копирование всего контейнера, если он не const (из-за CoW).


  1. NeoCode
    03.12.2024 14:10

    Выглядит как будто это какие-то костыли к языку, который уже давно прогибается под огромным гнетом "обратной совместимости".


    1. 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 перегрузку


      1. 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));
        }


        1. NeoCode
          03.12.2024 14:10

          А какую практическую задачу решает этот пример?


  1. kenomimi
    03.12.2024 14:10

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

    std::launder как раз из этой оперы - только седые бородатые гуру с вершин далёких гор знают, что это, и использовали это на практике...


    1. Ilya_JOATMON
      03.12.2024 14:10

      ППКС. Вы еще не видели во что это в машинные коды транслируется. Глянешь в декомпилятор и вздрогнешь.


      1. boldape
        03.12.2024 14:10

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

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


  1. elixirkmc
    03.12.2024 14:10

    В примере с std::as_const совершенно непонятно, зачем он там нужен. И так можно вызвать print, внутри которой константность работает самым обычным образом.


  1. unreal_undead2
    03.12.2024 14:10

    new(buffer) MyClass(42);

    MyClass* p = reinterpret_cast<MyClass*>(buffer);

    Почему не MyClass* p = new(buffer) MyClass(42);