第55章 std::launder ( C++17 )

 ロンダリングとは掃除する、キレイにする、洗うことを意味します。

 C++17 からは、以前にはなかったメモリーロンダリングという要件が追加されてます。

 具体的に必要となるシチュエーションは 2 つあります。

例(const データメンバーを持つポインターの再利用). 

//main.cpp
  1 #include <iostream>
  2
  3 struct Foo {
  4   const int x;
  5 };
  6
  7 int main()
  8 {
  9   Foo *foo_old = new Foo{100};
 10   Foo *foo_new = new (foo_old) Foo{200}; //危険!
 11   const int foo_x_valid = foo_new->x; //OK
 12   const int foo_x_invalid = foo_old->x; //未定義動作(undefined bahavior)
 13   std::cout << "foo_old->x == " << foo_x_invalid << " @ " << foo_old << '\n'
 14   std::cout << "foo_new->x == " << foo_x_valid << " @ " << foo_new <<'\n';
 15   return 0;
 16 }

ビルドと実行結果. 

$ g++ main.cpp -std=c++17
$ ./a.out
foo_old->x == 200 @ 0x5611f8646e70
foo_new->x == 200 @ 0x5611f8646e70

例(配置 new をした記憶域に対して reinterpret_cast をして得たポインターからのオブジェクトアクセス). 

//main.cpp
  1 #include <iostream>
  2 #include <cstddef>
  3
  4 struct Bar {
  5   int x;
  6 };
  7
  8 int main()
  9 {
 10   alignas(Bar) std::byte bar_buf[32];
 11   Bar *bar = new (bar_buf) Bar{100};
 12   int val = reinterpret_cast<Bar*>(&bar_buf)->x; //未定義動作
 13   std::cout << val << " @ " << &bar_buf << '\n';
 14   std::cout << bar->x << " @ " << bar << '\n';
 15   return 0;
 16 }

ビルドと実行結果. 

$ g++ main.cpp -std=c++17
$ ./a.out
100 @ 0x7ffe745bdd00
100 @ 0x7ffe745bdd00

 どちらの未定義動作もコンパイルは通るはずですしサンプルコードの実行結果にも奇妙な動きは見られませんが、 C++17 の標準規格には適合しない未定義動作( undefined behaviour )に分類されてしまうらしいです。

 未定義動作はバグの原因になったり、セキュリティーホールになる余地があるにも関わらず、結果的にコンパイルが通ってしまう状況を作りますんで、こうした一見問題なさそうな例になってしまいます。

 基本的に標準規格が想定しないような逸脱したコードを書くと未定義動作なので、まあ気をつけたほうが良いでしょう。

 つまり上述 2 つの例は不適切なオブジェクトへのアクセスってことです。

 これを解決してくれるのが C++17 から導入された std::launder() 関数です。

#include <new>
template <class T>
constexpr T* launder(T* p) noexcept;

 引数 p はロンダリングが必要なポインターです。

 std::launder(p) によって以下の 2 種類のロンダリングが可能となります。

例(const データメンバーを持つポインターのロンダリング). 

  1 #include <iostream>
  2 #include <new>
  3
  4 struct Foo {
  5   const int x;
  6 };
  7
  8 int main()
  9 {
 10   Foo *foo_old = new Foo{100};
 11   Foo *foo_new = new (foo_old) Foo{200}; //危険!
 12   const int foo_x = std::launder(foo_old)->x;
 13   std::cout << "std::launder(foo_old)->x == " << foo_x << " @ " << foo_old << '\n';
 14   return 0;
 15 }

ビルドと実行結果. 

$ g++ main.cpp -std=c++17
$ ./a.out
std::launder(foo_old)->x == 200 @ 0x55c12c3a8e70

 この const メンバーは不変なわけですが配置 new を使うことで以下のように値を変更ができてしまいます。

 10   Foo *foo_old = new Foo{100};
 11   Foo *foo_new = new (foo_old) Foo{200}; //危険!

 これは未定義動作に該当するために std::launder() 関数を使ってロンダリングする必要があります。

 13   std::cout << "std::launder(foo_old)->x == " << foo_x << " @ " << foo_old << '\n';

 こんな感じで古いポインターをロンダします。

例(配置 new をした記憶域に対して reinterpret_cast をして得たポインターのロンダリング). 

  1 #include <iostream>
  2 #include <cstddef>
  3 #include <new>
  4
  5 struct Bar {
  6   int x;
  7 };
  8
  9 int main()
 10 {
 11   alignas(Bar) std::byte bar_buf[32];
 12   Bar *bar = new (bar_buf) Bar{100};
 13   int val = std::launder(reinterpret_cast<Bar*>(&bar_buf))->x;
 14   std::cout << val << " @ " << &bar_buf << '\n';
 15   return 0;
 16 }

ビルドと実行結果. 

$ g++ main.cpp -std=c++17
$ ./a.out
100 @ 0x7ffcec143240

 このケースでは std::byte[] 型のメモリー領域の上に Bar 型の配置 new をしています。

 11   alignas(Bar) std::byte bar_buf[32];
 12   Bar *bar = new (bar_buf) Bar{100};

 まあ新たに割り当てた bar ポインターを使うなら問題なしなんですが、 bar_buf は古いポインターであり、これへのアクセスは未定義動作となるとのことです。

 前のケースと同様に古いポインターを std::launder でロンダリングします。

 13   int val = std::launder(reinterpret_cast<Bar*>(&bar_buf))->x;

 どちらのケースも面倒ではありますが、何気なく使ってしまいそうな式っぽいので気をつけましょうね。

 C++17 から導入された std::launder() はアロケーター設計者の宿敵(ネメシス)とでも言うべき機能ですが、少なくとも問題となる前提条件を抑えておけば問題は無いと… 思います。

Copyright 2018-2019, by Masaki Komatsu