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