utility_launder

utility_launder (객체 포인터 재보정)

std::launder는 C++17에서 추가된 유틸리티 함수로, 주어진 포인터가 나타내는 주소에 있는 객체를 가리키는 새로운 포인터를 반환해요. 객체 수명이 끝난 후 같은 저장 공간에 새로 생성된 객체에 접근해야 할 때, 특히 base class 하위 객체와 관련된 상황에서 유용하게 사용할 수 있어요. 이 함수는 컴파일러의 가정을 조정하는 일종의 "비가상화 펜스" 역할을 해요.

출처: cppreference

본문

정의

<new> 헤더에 정의되어 있어요.

<new> 헤더에 정의됨
template < class T > constexpr T * launder ( T * p ) noexcept ; (C++17 이후)

설명

p에 대한 비가상화 펜스예요. p가 나타내는 같은 주소에 있는 객체를 가리키는 포인터를 반환해요. 이때 객체는 원래 *p 객체의 최종 파생 클래스와 다른 최종 파생 클래스를 가진 새로운 base class 하위 객체일 수 있어요.

형식적으로 말하면, 다음 조건이 주어졌을 때:

  • 포인터 p가 메모리 바이트의 주소 A를 나타내고,
  • 객체 x가 주소 A에 있고,
  • x가 수명 기간 내에 있고,
  • x의 타입이 모든 수준에서 cv-한정자를 무시할 때 T와 동일하고,
  • 결과를 통해 도달할 수 있는 모든 바이트가 p를 통해 도달 가능해야 해요. (바이트는 객체 y를 가리키는 포인터를 통해 도달할 수 있는데, 그 바이트가 y와 포인터 상호 변환 가능한 객체 z의 저장 공간 안에 있거나, z가 요소인 바로 둘러싼 배열 안에 있는 경우를 말해요.)

그러면 std::launder(p)는 객체 x를 가리키는 T* 타입 값을 반환해요. 그렇지 않으면 동작이 정의되지 않아요.

T가 함수 타입이거나 (cv-한정될 수 있는) void라면 프로그램은 ill-formed예요.

std::launder는 인자의 (변환된) 값을 함수 호출 대신 사용할 수 있는 경우에만 핵심 상수 표현식에서 사용할 수 있어요. 즉, std::launder는 상수 평가에서 제한을 완화하지 않아요.

Notes

std::launder는 인자에 아무 효과도 없어요. 반환 값을 객체 접근에 사용해야 해요. 따라서 반환 값을 버리는 것은 항상 오류예요.

std::launder의 일반적인 용도는 다음과 같아요:

  • 같은 타입의 기존 객체 저장 공간에 생성된 객체를 가리키는 포인터를 얻는 경우 (예를 들어 두 객체 중 하나가 base class 하위 객체라서 이전 객체에 대한 포인터를 재사용할 수 없는 경우);
  • 그 객체를 위한 저장 공간을 제공하는 객체에 대한 포인터로부터 placement new로 생성된 객체를 가리키는 포인터를 얻는 경우.

도달 가능성 제한은 std::launder를 원래 포인터로 접근할 수 없는 바이트에 접근하는 데 사용할 수 없도록 보장해서, 컴파일러의 이스케이프 분석을 방해하지 않아요.

int x[10];
auto p = std::launder(reinterpret_cast<int(*)[10]>(&x[0])); // OK

int x2[2][10];
auto p2 = std::launder(reinterpret_cast<int(*)[10]>(&x2[0][0]));
// Undefined behavior: x2[1] would be reachable through the resulting pointer to x2[0]
// but is not reachable from the source
   
struct X { int a[10]; } x3, x4[2]; // standard layout; assume no padding
auto p3 = std::launder(reinterpret_cast<int(*)[10]>(&x3.a[0])); // OK
auto p4 = std::launder(reinterpret_cast<int(*)[10]>(&x4[0].a[0]));
// Undefined behavior: x4[1] would be reachable through the resulting pointer to x4[0].a
// (which is pointer-interconvertible with x4[0]) but is not reachable from the source

struct Y { int a[10]; double y; } x5;
auto p5 = std::launder(reinterpret_cast<int(*)[10]>(&x5.a[0]));
// Undefined behavior: x5.y would be reachable through the resulting pointer to x5.a
// but is not reachable from the source

Example

#include <cassert>
#include <cstddef>
#include <new>

struct Base
{
    virtual int transmogrify();
};

struct Derived : Base
{
    int transmogrify() override
    {
        new(this) Base;
        return 2;
    }
};

int Base::transmogrify()
{
    new(this) Derived;
    return 1;
}

static_assert(sizeof(Derived) == sizeof(Base));

int main()
{
    // Case 1: the new object failed to be transparently replaceable because
    // it is a base subobject but the old object is a complete object.
    Base base;
    int n = base.transmogrify();
    // int m = base.transmogrify(); // undefined behavior
    int m = std::launder(&base)->transmogrify(); // OK
    assert(m + n == 3);
    
    // Case 2: access to a new object whose storage is provided
    // by a byte array through a pointer to the array.
    struct Y { int z; };
    alignas(Y) std::byte s[sizeof(Y)];
    Y* q = new(&s) Y{2};
    const int f = reinterpret_cast<Y*>(&s)->z; // Class member access is undefined
                                               // behavior: reinterpret_cast<Y*>(&s)
                                               // has value "pointer to s" and does
                                               // not point to a Y object
    const int g = q->z; // OK
    const int h = std::launder(reinterpret_cast<Y*>(&s))->z; // OK
    
    [](...){}(f, g, h); // evokes [[maybe_unused]] effect
}

Defect reports

다음 동작 변경 결함 보고서는 이전에 발표된 C++ 표준에 소급 적용되었어요.

DR 적용 대상 게시된 동작 올바른 동작
LWG 2859 C++17 도달 가능의 정의가 포인터 상호 변환 가능한 객체로부터의 포인터 연산을 고려하지 않았음 포함됨
LWG 3495 C++17 std::launder가 상수 표현식에서 비활성 멤버를 가리키는 포인터를 역참조 가능하게 만들 수 있었음 금지됨

더 알아보기 (Learn more)

cppreference