Skip to content

Commit 04deaac

Browse files
committed
未初期化領域への暗黙的なオブジェクト構築 : 共用体の例と問題解決例を追加
1 parent 496bf9c commit 04deaac

File tree

1 file changed

+189
-20
lines changed

1 file changed

+189
-20
lines changed

lang/cpp20/implicit_creation_of_objects_for_low-level_object_manipulation.md

Lines changed: 189 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ X *make_x() {
1515
// malloc()はメモリの確保だけを行う
1616
X *p = (X*)malloc(sizeof(struct X));
1717

18-
// pの領域にはオブジェクトが構築されていない
18+
// pの領域にはXのオブジェクトが構築されていない
1919
p->a = 1; // 💀 UB
2020
p->b = 2; // 💀 UB
2121

@@ -31,7 +31,7 @@ X *make_x() {
3131
// new式はメモリの確保とオブジェクト構築を行う
3232
X *p = new X;
3333
34-
// pの領域にはオブジェクトが構築済
34+
// pの領域にはXのオブジェクトが構築済
3535
p->a = 1; // ✅ ok
3636
p->b = 2; // ✅ ok
3737
@@ -50,12 +50,12 @@ X *make_x() {
5050
`new`式ではなく`operator new()`を直接使用する場合は同様の問題がある。
5151

5252
```cpp
53-
// new式を使用する場合
53+
// new演算子を使用する場合
5454
X *make_x() {
5555
// operator new()はメモリの確保だけを行う
5656
X *p = (X*)::operator new(sizeof(struct X));
5757

58-
// pの領域にはオブジェクトが構築されていない
58+
// pの領域にはXのオブジェクトが構築されていない
5959
p->a = 1; // 💀 UB
6060
p->b = 2; // 💀 UB
6161

@@ -195,6 +195,25 @@ C++においては、ポインタに対する演算(`+ -`など)はそのポ
195195
196196
また、これらの操作に限らず、`char, unsigned char, std::byte`の配列オブジェクトを構築しその生存期間を開始する操作は、その配列オブジェクトが占有する領域内にその要素のオブジェクトを暗黙的に構築する。
197197
198+
#### 共用体のコピー操作
199+
200+
共用体のデフォルトのコピー/ムーブコンストラクタと代入演算子では、次のようにオブジェクトを暗黙的に構築する
201+
202+
- コンストラクタ
203+
- コピー元オブジェクトにネストした各オブジェクトに対して、コピー先内で対応するオブジェクト`o`を
204+
- サブオブジェクトの場合 : 特定する
205+
- それ以外の場合 : 暗黙的に構築する
206+
- 別のオブジェクトにストレージを提供している場合やサブオブジェクトのサブオブジェクトなど
207+
- `o`の生存期間はコピーの前に開始される
208+
- 代入演算子
209+
- 代入元と代入先が同じオブジェクトではない場合
210+
- コピー元オブジェクトにネストした各オブジェクトに対して、コピー先内で対応するオブジェクト`o`が暗黙的に構築され
211+
- `o`の生存期間はコピーの前に開始される
212+
213+
どちらの場合も、コピー元で生存期間内にあるオブジェクトがコピー先で(可能なら)暗黙的に構築される。
214+
215+
クラス型をメンバとして保持する場合など、デフォルトのコンストラクタ/代入演算子が`delete`されている場合はこれは行われない。
216+
198217
### 暗黙的なオブジェクト構築
199218
200219
オブジェクトを暗黙的に構築する操作では、そうすることでプログラムが定義された振る舞いをするようになる(すなわち、未定義動作が回避できる)場合に、*implicit-lifetime types*の0個以上のオブジェクトを暗黙的に構築しその生存期間を開始させる。そのような、暗黙的なオブジェクト構築によってプログラムに定義された振る舞いをもたらすオブジェクトが1つも存在しない場合は未定義動作となる(これは今まで通り)。逆に、そのようなオブジェクトが複数存在している場合は、どのオブジェクトが暗黙的に構築されるかは未規定(これは、都度適切なオブジェクトが選択され構築されることを意図している)。
@@ -237,32 +256,182 @@ static_assert(f() == 123); // C++20からはUBが起こるため不適格、C++
237256

238257
したがって、これらの変更によって実行時に何かすべきことが増えるわけではなく、暗黙的なオブジェクト構築は実際にコンストラクタを呼んだり何か初期化を行うものではないし、擬似デストラクタ呼び出しが実行時に何かをするようになるわけでもない。
239258

240-
##
241-
(執筆中)
242-
```cpp example
243-
// (ここには、言語機能の使い方を解説するための、サンプルコードを記述します。)
244-
// (インクルードとmain()関数を含む、実行可能なサンプルコードを記述してください。そのようなコードブロックにはexampleタグを付けます。)
259+
## 以前の問題の修正例
260+
261+
### `malloc()`/ `operator new`
262+
263+
```cpp
264+
// Xはimplicit-lifetime class types
265+
struct X {
266+
int a;
267+
int b;
268+
};
269+
270+
X *make_x() {
271+
// 後続のXのメンバアクセスを定義された振る舞いとするために
272+
// malloc()はメモリの確保とともにXのオブジェクト(とメンバオブジェクト)を暗黙的に構築する
273+
// そして、構築されたXのオブジェクトへの適切なポインタを返す
274+
X *p = (X*)malloc(sizeof(struct X));
275+
276+
// pの領域にはXのオブジェクトが暗黙的に構築されている
277+
p->a = 1; // ✅ ok
278+
p->b = 2; // ✅ ok
279+
280+
return p;
281+
}
282+
```
283+
284+
```cpp
285+
// new演算子を使用する場合
286+
X *make_x() {
287+
// 後続のXのメンバアクセスを定義された振る舞いとするために
288+
// operator new()はメモリの確保とともにXのオブジェクト(とメンバオブジェクト)を暗黙的に構築する
289+
// そして、構築されたXのオブジェクトへの適切なポインタを返す
290+
X *p = (X*)::operator new(sizeof(struct X));
291+
292+
// pの領域にはXのオブジェクトが暗黙的に構築されている
293+
p->a = 1; // ✅ ok
294+
p->b = 2; // ✅ ok
295+
296+
return p;
297+
}
298+
```
299+
300+
### 共用体のコピー
301+
302+
```cpp
303+
union U {
304+
int n;
305+
float f;
306+
};
307+
308+
float pun(int n) {
309+
// U::nの生存期間が開始
310+
U u = {.n = n};
311+
312+
// このコピーではuのオブジェクト表現がコピーされるとともに
313+
// uのアクティブメンバに対応するメンバがコピー先でアクティブとなる
314+
U u2 = u;
315+
316+
// u2.fは非アクティブ
317+
return u2.f; // 💀 UB
318+
}
319+
```
320+
321+
共用体のコピーにおいてはあくまでコピー元で生存期間内にあったオブジェクトに対応するオブジェクトがコピー先でも生存期間内にあることが保証されるだけで、*type-punning*のようなことを可能にするわけではない。
322+
323+
```cpp
324+
int f(int n) {
325+
U u = {.n = n};
326+
327+
U u2 = u;
328+
329+
// これならok
330+
return u2.n; // ✅ ok
331+
}
332+
```
333+
334+
### バイト配列の読み込み
245335

246-
#include <iostream>
336+
```cpp
337+
// 何かバイト列ストリームを受け取って処理する関数とする
338+
void process(Stream *stream) {
339+
// バイト配列の読み出し
340+
std::unique_ptr<char[]> buffer = stream->read();
247341

248-
int main()
249-
{
250-
int variable = 0;
251-
std::cout << variable << std::endl;
342+
// 先頭バイトの状態によって分岐
343+
if (buffer[0] == FOO) {
344+
process_foo(reinterpret_cast<Foo*>(buffer.get())); // #1
345+
} else {
346+
process_bar(reinterpret_cast<Bar*>(buffer.get())); // #2
347+
}
252348
}
253349
```
254-
* variable[color ff0000]
255350
256-
(コードブロック中の識別子に、文字色を付ける例です。)
351+
`Foo`も`Bar`も*implicit-lifetime types*だとして、以前のこのコードに対して`Stream::read()`が次のように実装されている場合
257352
258-
### 出力
353+
```cpp
354+
unique_ptr<char[]> Stream::read() {
355+
// ... determine data size ...
356+
unique_ptr<char[]> buffer(new char[N]);
357+
// ... copy data into buffer ...
358+
return buffer;
359+
}
259360
```
260-
0
361+
362+
この`read()`内の`new char[N]`によって呼ばれる`operator new[]`によって`Foo`/`Bar`のオブジェクトが暗黙的に構築される。この場合、`buffer[0] == FOO`による分岐によってプログラムに定義された振る舞いをもたらすオブジェクトは、`Foo``Bar`のオブジェクトとして2つ存在する。したがって、ここでは先頭バイトの状態に応じて適切なオブジェクトが構築される(そうすることでプログラムに定義された振る舞いをもたらす)ため、`process()`内では未定義動作は回避される。
363+
364+
```cpp
365+
void process(Stream *stream) {
366+
// バイト配列の読み出し
367+
std::unique_ptr<char[]> buffer = stream->read();
368+
369+
// 先頭バイトの状態によって適切なオブジェクトがStream::read()内で構築されている
370+
if (buffer[0] == FOO) {
371+
process_foo(reinterpret_cast<Foo*>(buffer.get())); // ✅ ok
372+
} else {
373+
process_bar(reinterpret_cast<Bar*>(buffer.get())); // ✅ ok
374+
}
375+
}
261376
```
262377
263-
(ここには、サンプルコードの実行結果を記述します。何も出力がない場合は、項目を削除せず、空の出力にしてください。)
264-
(実行結果が処理系・実行環境によって異なる場合は、項目名を「出力例」に変更し、可能であればその理由も併記してください。)
378+
### 動的配列の実装
379+
380+
```cpp
381+
// std::vectorの様な動的配列型を実装したい
382+
template<typename T>
383+
struct Vec {
384+
char *buf = nullptr;
385+
char *buf_end_size = nullptr;
386+
char *buf_end_capacity = nullptr;
387+
388+
void reserve(std::size_t n) {
389+
// 後続の操作を適格にするためのオブジェクトを暗黙的に構築する
390+
// ここでは、Tの配列型T[]のオブジェクトが暗黙的に構築される(要素のオブジェクトは構築されない)
391+
// 同時に、char[]のオブジェクトも暗黙的に構築される
392+
char *newbuf = (char*)::operator new(n * sizeof(T), std::align_val_t(alignof(T)));
393+
394+
// newbufにはT[]のオブジェクトが生存期間内にあるため、ポインタT*をイテレータとして使用可能となる
395+
// ここで、T[]の要素のTのオブジェクトが構築される(明示的)
396+
std::uninitialized_copy(begin(), end(), (T*)newbuf); // #a ✅ ok
397+
398+
::operator delete(buf, std::align_val_t(alignof(T)));
399+
400+
// newbufにはchar[]のオブジェクトが生存期間内にあるため、newbuf(char*)をイテレータとして使用可能となる
401+
buf_end_size = newbuf + sizeof(T) * size(); // #b ✅ ok
402+
buf_end_capacity = newbuf + sizeof(T) * n; // #c ✅ ok
403+
buf = newbuf;
404+
}
405+
406+
void push_back(T t) {
407+
if (buf_end_size == buf_end_capacity)
408+
reserve(std::max<std::size_t>(size() * 2, 1));
409+
new (buf_end_size) T(t);
410+
411+
// buf_end_sizeの指す領域にはchar[]のオブジェクトが生存期間内にあるため、ポインタをイテレータとして使用可能
412+
buf_end_size += sizeof(T); // #d ✅ ok
413+
}
414+
415+
T *begin() { return (T*)buf; }
416+
417+
T *end() { return (T*)buf_end_size; }
418+
419+
// buf及びbuf_end_sizeの指す領域にはT[]のオブジェクトが生存期間内にあるため、ポインタをイテレータとして使用可能
420+
std::size_t size() { return end() - begin(); } // #e ✅ ok
421+
};
422+
423+
int main() {
424+
Vec<int> v;
425+
v.push_back(1);
426+
v.push_back(2);
427+
v.push_back(3);
428+
429+
// 実装内部で暗黙的に配列オブジェクトが構築されることでUBが回避される
430+
for (int n : v) { /*...*/ } // #f ✅ ok
431+
}
432+
```
265433

434+
この例では、`reserve()``newbuf`及びそれを保存している`Vec::buf`の領域に`T[]``T`の配列型)と`char[]`のオブジェクトが暗黙的に構築され、同時に生存期間内にあることで、問題(配列オブジェクトを指さないポインタのイテレータとしての使用)が解消され、すべての箇所で定義された振る舞いをもたらしている。
266435

267436
## この機能が必要になった背景・経緯
268437
(執筆中)

0 commit comments

Comments
 (0)