C++ Advent Calender 2020
この記事はC++ Advent Calender 20205日目の記事です。
昨日は @yohhoy 氏のC++20便利機能の紹介:自動joinスレッドと停止機構 std::jthread, stop_tokenでした
明日は @MusicScience37 氏のstd::tuple の型を実行時に選択したいです
はじめに
この記事を書いている私は実のところpointer interconvertibleについてさほど詳しく有りません。多分@k_satoda 氏あたりに聞くほうがこの記事よりはるかにわかりやすく正確な回答が帰ってくるであろうことは火を見るより明らかですが、それでもまあ、自分の勉強も兼ねて筆をすすめることにします。
C++警察御用達のおもちゃ: Strict Aliasing rule
C++警察の皆さんならばつぎのコードが未定義動作であることがわかるはずです。
class A { int foo; };
class B { int foo; };
A a;
B* b = reinterpret_cast<B*>(&a);
b->foo = 3;
詳しくは
(翻訳)C/C++のStrict Aliasingを理解する または - どうして#$@##@^%コンパイラは僕がしたい事をさせてくれないの! - yohhoyの日記
に丸投げします。余談ですがC++20でStrict Aliasing ruleの定義が随分とすっきりしました。
[basic.lval] 11
上の例で言うなら、変数b
に実際にアクセスが行われるb->foo = 3;
のとき、b
はa
へのエイリアスにならないため、未定義動作となるのでした。
ではb->foo = 3;
がなかったらどうでしょうか?実際にアクセスが行われないのでキャストだけではStrict Aliasing ruleに反しません。
じゃあ未定義動作ではないのか?いいえ、実はこのreinterpret_cast
はpointer interconvertibleではないのです。
pointer interconvertible
pointer interconvertibleの定義
規格書の定義を翻訳してみましょう。
[basic.compound] 4
2つのオブジェクトa
, b
がpointer interconvertibleであるのは次のいずれかのときである
- 同一のオブジェクトである
- 片方が共用体オブジェクトで、他方がその共用体の非静的メンバ変数である
- 片方がstandard layoutクラスオブジェクトであり、他方は次のいずれかである場合
-
if (そのオブジェクトが非静的メンバ変数を持つ)
:
→そのオブジェクトの最初の非静的メンバ変数 -
else
:
→基底クラスオブジェクト
-
- 次のようなオブジェクト
c
が存在する:a
とc
がpointer interconvertibleでかつb
とc
がpointer interconvertibleである
pointer interconvertibleの登場場面: reinterpret_cast
異なるオブジェクト型へのポインタ間のキャストは以下で定義されています。https://t.co/QqzA00iyhI
— Kazutoshi SATODA (@k_satoda) November 29, 2020
ここで現れる static_cast について、結果の型の有効なオブジェクトを指すようになる条件が pointer-interconvertible を使って定められています。https://t.co/9O5hRHfjnt
まずreinterpret_cast
がオブジェクトへのポインタをcv T*
に変換するとき、次のように振る舞います。([expr.reinterpret.cast] 7)
static_cast<cv T*>(static_cast<cv void*>(v))
次に、cv1 void*
をcv2 T*
にstatic_cast
で変換するときを考えます。この変換ができるにはcv修飾子cv1
, cv2
を比べた時に同じかcv1
よりもより修飾されている必要があります(例えばconst
をstatic_cast
で外せない、それはそう)([expr.static.cast] 13)。
最後にcv1 void*
をcv2 T*
にstatic_cast
で変換して得たポインタの値が変換後の型として有効かを考えます。その条件は次のとおりです([expr.static.cast] 13)。
- 元となったポインタの値がメモリー上のAというアドレスを指していて、Aが変換後の型
T
のアラインメント要求を満たす
満たさない→得られたポインタの値はunspecified - 元となったポインタの値が
a
というオブジェクトを指していてかつ、cv修飾子を無視した時にa
とpointer interconvertibleなT
型のオブジェクトb
が存在する
満たさない→ポインタの値が変化しない→cv2T*
型のポインタとしては有効ではない1
同じアドレスに2つのオブジェクトがあることとpointer interconvertibleであることは別である
struct A {
int aaa;
double bbb;
};
double foo(int* a)
{
reinterpret_cast<const A*>(a)->bbb;
}
int main()
{
A a{3, 4.2};
std::cout << foo(a.aaa) << std::endl;
}
pointer interconvertibleの定義から、クラスA
へのポインタとクラスAの非静的メンバ変数であるaaa
へのポインタは変換可能です。
このとき、この2つは同じアドレスを指していることから「なるほど、pointer interconvertibleってのはつまり同じアドレスだったら変換できるってことか!」と考える人がいるかも知れません。
#include <cassert>
int main(){
int arr[4];
void* ptr1 = static_cast<void*>(arr);// OK
void* ptr2 = static_cast<void*>(&arr);// OK
assert(ptr1 == ptr2); // => true, same adress
auto ptr1_2 = static_cast<int (*)[4]>(ptr2);// NG: not pointer interconvertible
auto ptr2_2 = static_cast<int*>(ptr1);// NG: not pointer interconvertible
}
しかし上の例を見てください。変数ptr1_2
/ptr2_2
を初期化しようとしているcastはpointer interconvertibleではありません。言い換えると、配列オブジェクトと配列の先頭要素は同じアドレスにありますが、pointer interconvertibleではありません。
配列だけが例外と考えるのではなく、むしろpointer interconvertibleという関係性そのものが歴史的背景などからくる特殊例と考えるべきでしょう。
経緯
pointer interconvertibleのどこが新しいねん!というツッコミが来そうなのでこの記事を書くに至った経緯を書いておくことにします。
先日Qiitaに宣伝記事が出ていたC++入門サイトがあります
https://rinatz.github.io/cpp-book/
このサイトの査読をするうちに、キャストの説明に改善点がみつかり、ついでにStrict Aliasing Ruleの解説を書き加えました。
https://github.com/rinatz/cpp-book/pull/67
しかしその過程でStrict Aliasing Ruleの解釈が不安になりteratailとTwitterで質問をしました。
C++ - strict alias rule違反となるのはキャストした時点か|teratail
解釈についてあっていることが確認できてホッとしたところにC++有識者の @gnaggnoyil 氏からこんな指摘がありました。
そもそも元スレのreinterpret_cast式は現在のWDによるといわゆるstrict aliasing ruleで定義されてません。pointer interconvertibleを満たさない場合には意味がすでに必要とされてないため、文面通りの「未定義行為」です。
— g_naggnoyil(gint, gchar **) (@gnaggnoyil) November 27, 2020
pointer interconvertibleって何???
私にとって未知な用語に触れた私は調査を開始するのでした。
ちょうどほぼ同時並行して @yohhoy 氏がcpprefjpにis_pointer_interconvertible_base_of
についての項目を執筆されているのに励まされながら、遅刻しつつもこの記事は執筆されました。
https://github.com/cpprefjp/site/commit/373db2562dd489679aa96001078a6cb320052725
追記: is_pointer_interconvertible_base_of
の存在意義が問われる事態
https://github.com/cpprefjp/site/issues/824
でその存在意義に疑問符がつき
https://lists.isocpp.org/std-discussion/2020/12/0921.php
std-discussionに質問が飛んで
https://lists.isocpp.org/std-discussion/2020/12/0922.php
p0466の作者にメンションが飛んでる模様です
参考資料
- C++ - strict alias rule違反となるのはキャストした時点か|teratail
- (翻訳)C/C++のStrict Aliasingを理解する または - どうして#$@##@^%コンパイラは僕がしたい事をさせてくれないの! - yohhoyの日記
- c++ - Pointer interconvertibility vs having the same address - Stack Overflow
- Questions on N4430 (Core Issue 1776: Replacement of class objects containing reference members)
- oop - Object-orientation in C - Stack Overflow
- c++ - Casting a char array to an object pointer - is this UB? - Stack Overflow
- std::is_pointer_interconvertible_base_of - cppreference.com
- is_pointer_interconvertible_base_of - cpprefjp C++日本語リファレンス
- P0593R6: Implicit creation of objects for low-level object manipulation#practical-examples
- Accessing Object Representations - p1839r2.pdf
- std::launder関数 - yohhoyの日記
- https://twitter.com/k_satoda/status/1242300634587467777
-
[expr.static.cast] 13には変換によってポインタの値が変化しないとしか書かれてないけど、多分もっと規格書をたどれば有効じゃないってことになる気がする、調べるには時間が足りなかった。 ↩