yohhoyの日記

技術的メモをしていきたい日記

std::launder関数

C++1z(C++17)標準ライブラリに追加されるstd::launder関数テンプレートについて。メモリ・ロンダリング関数。*1

まとめ:

  • オブジェクト生存期間(lifetime)に基づいた最適化の抑止コンパイラに伝える関数。プログラマ視点では “何もしない関数” にみえる。*2
  • 配置new(placement new)利用時のみ役立つ関数。アプリケーション開発者向けの機能ではなく、メモリアロケータやライブラリ内部実装者向け。
  • C++怖い。

A language support tool (an "optimisation barrier") to allow libraries to reuse storage and access that storage through an old pointer, which was previously not allowed. (This is an expert tool for implementers and not expected to show up in "normal" code.)

P0636R0 Changes between C++14 and C++17 DIS

21.6.4 Pointer optimization barrier
template <class T> constexpr T* launder(T* p) noexcept;

1 Requires: p represents the address A of a byte in memory. An object X that is within its lifetime (6.8) and whose type is similar (7.5) to T is located at the address A. All bytes of storage that would be reachable through the result are reachable through p (see below).
2 Returns: A value of type T * that points to X.
3 Remarks: An invocation of this function may be used in a core constant expression whenever the value of its argument may be used in a core constant expression. A byte of storage is reachable through a pointer value that points to an object Y if it is within the storage occupied by Y, an object that is pointer-interconvertible with Y, or the immediately-enclosing array object if Y is an array element. The program is ill-formed if T is a function type or cv void.
4 [Note: If a new object is created in storage occupied by an existing object of the same type, a pointer to the original object can be used to refer to the new object unless the type contains constor reference members; in the latter cases, this function can be used to obtain a usable pointer to the new object. See 6.8. -- end note]
5 [Example:

struct X { const int n; };
X *p = new X{3};
const int a = p->n;
new (p) X{5};  // p does not point to new object (6.8) because X::n is const
const int b = p->n;                // undefined behavior
const int c = std::launder(p)->n;  // OK

-- end example]

N4659 Working Draft, Standard for Programming Language C++

std::launder関数の効果は下記サンプルコードを参照。参照型やconstデータメンバを含む構造体/クラスを扱う」または「汎用メモリストレージを具体的な型にキャストする」とき、配置newとの組み合わせ利用には細心の注意を払うこと。

  • 2021-05-10追記:C++20ではNBコメント RU007 を解決する P1971R0 が採択され、参照型やconstデータメンバを含む構造体に関するルールが緩和された。これらの同データメンバを含む構造体に対してのstd::launder関数は不要となる。StackOverflowの質問と回答も参照のこと。
  • 2022-01-14追記:C++2b(C++23)にむけて(PDF) P1413R3 が採択予定となっており、std::aligned_storageは非推奨(deprecated)となる見込み。alignas(int) std::byte data[sizeof(int)];で代替可能。
// constデータメンバを"含む"構造体
struct X {
  const int n;
  int m;
};

X *p = new X{3};
new(p) X{5};  // 配置new(戻り値ポインタは破棄)

// pはlifetimeが切れたオブジェクトを指しているため
// 下記コードは"いずれも"未定義動作を引き起こす。
int a0 = p->n;  // NG
int b0 = p->m;  // NG: 非constメンバもダメ

// std::launder(p)は配置newによる新しいオブジェクトを指すため
// 下記コードはいずれもwell-definedとなる。
int a1 = std::launder(p)->n;  // OK: C++1z
int b1 = std::launder(p)->m;  // OK: C++1z
std::aligned_storage<sizeof(int), alignof(int)>::type data;
int* ptr = new(&data) int{42};

// dataとintは異なる型のため、launder関数を通す必要がある
int x1 = *reinterpret_cast<int*>(&data);                // NG
int x2 = *std::launder(reinterpret_cast<int*>(&data));  // OK: C++1z

// 配置new式が返したポインタptrは、必ず新しいオブジェクトを指す
int x0 = *ptr;  // OK

N4659(C++1z DIS) 4.5/p2, 6.8/p8より引用(下線部は強調)。
2021-05-10追記:P1971R0 採択により、C++20言語仕様では下記引用にあるNote部およびExample部は削除された。

Objects can contain other objects, called subobjects. A subobject can be a member subobject (12.2), a base class subobject (Clause 13), or an array element. An object that is not a subobject of any other object is called a complete object. If an object is created in storage associated with a member subobject or array element e (which may or may not be within its lifetime), the created object is a subobject of e's containing object if:

  • the lifetime of e's containing object has begun and not ended, and
  • the storage for the new object exactly overlays the storage location associated with e, and
  • the new object is of the same type as e (ignoring cv-qualification).

[Note: If the subobject contains a reference member or a const subobject, the name of the original subobject cannot be used to access the new object (6.8). -- end note] [Example:

struct X { const int n; };
union U { X x; float f; };
void tong() {
  U u = {{ 1 }};
  u.f = 5.f;                           // OK, creates new subobject of u (12.3)
  X *p = new (&u.x) X {2};             // OK, creates new subobject of u
  assert(p->n == 2);                   // OK
  assert(*std::launder(&u.x.n) == 2);  // OK
  assert(u.x.n == 2);                  // undefined behavior, u.x does not name new subobject
}

-- end example]

If, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, a new object is created at the storage location which the original object occupied, a pointer that pointed to the original object, a reference that referred to the original object, or the name of the original object will automatically refer to the new object and, once the lifetime of the new object has started, can be used to manipulate the new object, if:

  • the storage for the new object exactly overlays the storage location which the original object occupied, and
  • the new object is of the same type as the original object (ignoring the top-level cv-qualifiers), and
  • the type of the original object is not const-qualified, and, if a class type, does not contain any non-static data member whose type is const-qualified or a reference type, and
  • the original object was a most derived object (4.5) of type T and the new object is a most derived object of type T (that is, they are not base class subobjects).

(snip) [Note: If these conditions are not met, a pointer to the new object can be obtained from a pointer that represents the address of its storage by calling std::launder (21.6). -- end note]

メモ:P0532R0によればstd::launderの導入だけでは、std::vector<T>をはじめとするアロケータ・サポートのある標準コンテナで、const修飾もしくは参照型データメンバを含むクラスTを正しく扱えないという問題が残るようにも読める。

関連URL

*1:http://ejje.weblio.jp/content/launder wikipedia:資金洗浄

*2:関数入出力で型も値も変化しない恒等変換となっている。コンパイラへの指示だけを目的とする関数としては、他に std::kill_dependency が存在する。