ロンダリングとは掃除する、キレイにする、洗うことを意味します。
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