※この記事の結論は間違っています、信用しないでください。
詳しくは以下をご覧ください。
https://t.co/w5mqNbdfay 例えばint x2 = *std::launder(reinterpret_cast<int*>(&data));がOKになる理由は、アクセス時型情報を根拠とするstruct aliasing ruleとは無関係であり、launder関数によってobject lifetimeに基づいた最適化(可能性)を抑止できるから というのが私の解釈でした。
— yoh (@yohhoy) March 22, 2019
std::launderを用いてもstrict aliasing ruleに違反している状態からは逃れられません。
std::aligned_storage
C++11で追加されたstd::aligned_storage
これはboost::optionalの実装に使われていました(std::optionalはconstexpr対応のために別の方法を用いているので、今は違うかもしれません)。
しかし、そのような使い方をする場合、初期化後にその領域にアクセスする際に問題になる可能性があります。
例えばメンバとして持つような例を書いてみると
template<typename T> class StackStorage { typename std::aligned_storage<24, alignof(T)>::type m_storage{}; public: StackStorage(T&& value) { new(&m_storage) T(std::foward<T>(value)); } T* operator->() { return reinterpret_cast<T*>(&m_storage); } };
用途はともかく、こんな感じになるんじゃないでしょうか。これの何が問題になるかというと、T* operator->()に問題があります。
この中のreinterpret_cast
とはいえおそらく意図通りに動くと思います、忘れたころに未定義動作が降ってくる可能性が残り続けますが・・・
strict aliasing rules ってなんじゃらほい?
あるオブジェクトに対する別名(エイリアス:参照、ポインタ)定義の際にプログラマが守るべきルールです。
これはコンパイラの最適化のためのもので、違反即未定義動作になります。
ある型のオブジェクトにアクセス(読み/書き)する際に許されるのは以下の型のいずれかのみとなります [N4659 §6.10 p8より]
- そのオブジェクトのdynamic type
- CV修飾された、そのオブジェクトのdynamic type
- そのオブジェクトのdynamic typeに対して、signed unsignedの関係にある型
- CV修飾された、そのオブジェクトのdynamic typeに対して、signed unsignedの関係にある型
- dynamic typeに対してtype similarな関係の型
- 非staticメンバに上記の型を持つ集成体か共用体(これらは再帰的に適用される)
- そのオブジェクトのdynamic typeの(おそらくCV修飾された)基底クラス型
- char, unsigned char, std::byte
うーん、訳が悪いのか絶妙に分りませんねえ・・・。読み解いていきましょう
dynamic type
まずは、頻出するdynamic typeという単語から。
dynamic typeとは、glvalueによって参照されているもっとも派生した型 [N4659 §3.9より]
また何とも言い難いですが、おそらく以下のような意味合い。
struct Base{}; struct Derived : Base {}; Derived d{}; Base* b = &d; // 静的な型は decltype(*b) = Base // *bのdynamic typeはDerived int a{1}; int* p = &a; //*pの静的な型もdynamic typeもint
もっとも派生した型というのが分かりづらさを助長していますが、おそらくある基底クラスのポインタによって参照されている先のオブジェクトがさらに別の派生クラスの基底となっている場合を想定しているのだと思われます。
struct Base{}; struct Derived : Base {}; struct MostDerived : Derived {}; MostDerived md{}; Derived* d = &md; Base* b = d; // 静的な型は decltype(*b) = Base // *bのdynamic typeはMostDerived
つまりはあるポインタが指している先の本当の型の事を言っているのだと思います。これを踏まえると前述の条件のうち5つが分かります。
・そのオブジェクトのdynamic type
・CV修飾された、そのオブジェクトのdynamic type
元のオブジェクトと同じ型かその本来の型、及びそのCV修飾された型によるエイリアスはOK
・dynamic typeに対して、signed unsignedの関係にある型
・CV修飾された、そのオブジェクトのdynamic typeに対して、signed unsignedの関係にある型
dynamic type(及びそのCV修飾)が整数型である場合、符号の有無によらずエイリアスはOK
・そのオブジェクトのdynamic typeの(おそらくCV修飾された)基底クラス型
おそらく、単純に以下のようなケースがOK。
struct Base{ int n;}; struct Derived : Base {}; Derived d{{5}}; Base* b = &d; //基底クラスのポインタは派生クラスのエイリアスとなれる std::cout << b->n << std::endl; //5
わざわざdynamic typeの基底型としているのは、dynamic typeが多重継承をしている場合、その異なった基底型からのアクセスを考慮してのことだと思われます。
type similar
次に出てくるのは、type similar
何をもって二つの型が似ていると言えるのか?
N4659 §7.5にCV分解によるtype similarの定義があります、簡単に言えば二つの型がCV修飾の違い以外の差が無い場合にtype similarとなります。
cppreference.comのreinterpret_castのページに例が載っています
- type similarな組み合わせ
- const int * volatile * と int * * const
- const int (* volatile S::* const)[20] と int (* const S::* volatile)[20]
- int (* const *)(int *) と int (* volatile *)(int *)
- int (*)(int * const) と int (*)(int *) (これは同じ型とみなされる)
- type similarでない組
- int (S::*)() const と int (S::*)()
- int (*)(int *) と (*)(const int *)
- const int (*)(int *) と int (*)(int *)
- std::pair < int, int > と std::pair < const int, int >
CV修飾の違いとはCV修飾が付いているか付いていないか、付いているならばconstとvolatileの違いです。
関数ポインタ型の場合は引数型がtype similarでその他の差が無ければ、type similarとなるようです。
クラステンプレートはテンプレートパラメータが違えば別の型なのでtype similarにもならないでしょう。
微妙に意味わからない場合はN4659 §7.5のCV分解について読んでみるといいかもしれません(それはそれで意味わからないのですが・・・)。
ともかく、type similarがなんとなく分かれば残り二つのうち1つが分かります。
・dynamic typeに対してtype similarな関係の型
dynamic typeに対してCV修飾の違いしかない型によるエイリアスはOK(ただし、非const→constは付いてる位置次第で一方通行となると思われる)
集成体と共用体
集成体とは、配列、もしくは条件を満たした構造体(クラス)。
一様初期化構文が来る前からC言語の構造体と同じ初期化ができるような型が集成体です。つまりはC言語の構造体と同じ書き方をしてあるものです(ただし、その要素もしくは非staticデータメンバは集成体でなくてもいい)。
これに関しては日本語のいい解説が調べれば出てくるので説明はそちらに投げます。
・非staticメンバに上記の型を持つ集成体か共用体(これらは再帰的に適用される)
すなわち、エイリアスが許可される型でできている配列か、その型をデータメンバとして持っている構造体か、共用体はエイリアスとなれる。
また、配列の要素型のメンバ、構造体・共用体のメンバのメンバに該当する型がある場合もOK、これはさらに再帰してもいい。
ピンとこないですね・・・
struct Aggregate { int n; }; int f(int* p, Aggregate* p_a) { //この時、p は p_a->n を指している可能性があり、それは最適化の際考慮される //すなわち、Aggregate*はintのエイリアスとなることができる return *p + p_a->n; //とはいえこの処理では考慮されなかったとしても何ら問題はなさそう・・・ } Aggregate a = {10}; //この様に呼んだ場合 auto res = f(&a.n, &a);
すなわちこの文言は、配列の一要素、構造体・共用体のメンバを外から参照するときのためのものでしょう。
また、再帰的というのは以下のような場合です
struct Aggregate { int n; }; struct Aggregate2 { Aggregate a; }; int f(int* p, Aggregate2* p_a) { //この時、p は p_a->a.n を指している可能性があるが、これも考慮される //Aggregate2もまたintのエイリアスとなれる return *p + p_a->a.n; } Aggregate2 a2 = {10}; //この様に呼んだ場合 auto res = f(&a2.a.n, &a2);
配列や共用体についても同様です。
ただし、この文言は共用体によるtype punningを許しているというわけではないので注意です(C99では許可されていますがC++では許可されていないようです)。
サラッと流していましたが、char* unsigned char* std::byteはあらゆる型のエイリアスとなることが出来ます。
つまりはある型のオブジェクトをunsigned char*で参照し、unsigned char*で参照された別の型のオブジェクト領域にmemcpyすることが出来ます。
エイリアスとなれる条件、まとめ
なんかごちゃごちゃややこしく書いてきましたが、まとめてみれば
・そのオブジェクトのdynamic type
・CV修飾された、そのオブジェクトのdynamic type
・そのオブジェクトのdynamic typeに対して、signed unsignedの関係にある型
・CV修飾された、そのオブジェクトのdynamic typeに対して、signed unsignedの関係にある型
・dynamic typeに対してtype similarな関係の型(CV修飾だけが違うような型)
・そのオブジェクトのdynamic typeの(おそらくCV修飾された)基底クラス型
→要するに、元の型に対して同じ型か継承関係にある型のような、「関係のある型」
・非staticメンバに上記の型を持つ集成体か共用体(これらは再帰的に適用される)
→配列の一部やメンバを参照するエイリアスを許可するための文言、上記「関係のある型」を含んでいるような型(クラス/構造体型は集成体でなければならない)
・char, unsigned char, std::byte
→無いと困る
という感じです。
std::aligned_storageの領域に正しくアクセスするには
激しく横道に逸れましたが、本題に戻ります。
std::aligned_storage
でもそんなこと言ったらaligned_storageは全く無意味になってしまいます。合法的にアクセスするにはどうすればいいのか?
その答えは、aligned_storageの領域を遅延初期化するplacement newにあります。
placement newは通常のnewと同じように初期化した領域への有効なポインタを返します。しかし、多くの場合これは無視されています。なぜなら、placement new に渡したポインタと帰ってくるポインタは同じものになるからです。
多くの場合それは問題はないでしょう。しかし、std::aligned_storageの領域に正しくアクセスするためにはこの帰ってくるポインタを使う必要があります。これ以外に、領域への適正なポインタを得る手段がありません。
これを踏まえ、冒頭で書いたクラスを以下のように修正します。
template<typename T> class StackStorage { typename std::aligned_storage<24, alignof(T)>::type m_storage{}; T* m_ptr; public: StackStorage(T&& value) { m_ptr = new(&m_storage) T(std::foward<T>(value)); } T* operator->() { return m_ptr; } };
placement newの返すポインタを受け取るためにT*のデータメンバが一つ増えるのが気に入らないのですが、未定義動作よりはましでしょう?
しかし、何とかならないのか・・・
std::launder
C++14まではこの問題をどうすることもできません(私の知る限りは)。
しかし、C++17には別件で用意された解決策があります。それが、std::launder関数です。
この関数は型Tのポインタを受け取りそれをそのまま返す一見すると意味のない関数ですが、この関数を通すことでオブジェクト生存期間に基づいた最適化の抑止をコンパイラに指示することが出来ます。
そして同時に、std::aligned_storageの領域のアドレスを別の型のポインタへreniterpret_castすることを許可する効果も持っています(ただし、その領域が初期化された型のポインタでアクセスする場合のみ)。まさにポインタロンダリング。
launderさんを使えば冒頭のクラスはほぼそのまま合法化できます。
template<typename T> class StackStorage { typename std::aligned_storage<24, alignof(T)>::type m_storage{}; public: StackStorage(T&& value) { new(&m_storage) T(std::foward<T>(value)); } T* operator->() { return std::launder(reinterpret_cast<T*>(&m_storage)); } };
このように、placement newの返り値を保持しておく必要がなくなります。std::launderは何もしない関数であるので、定数式で使用することもでき、実行時に何かオーバーヘッドを残すこともありません。素敵ですね・・・
ただし、std::launderはstrict aliasing rulesを回避するためのものではありません。std::launderにはいくつか条件があり、今回のようにある型T1の領域をT1と無関係な型T2*で参照したい場合、その領域がT2で初期化されていることが必要になります。そうでない場合、未定義動作のままとなります。
参考文献
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4659.pdf
(翻訳)C/C++のStrict Aliasingを理解する または - どうして#$@##@^%コンパイラは僕がしたい事をさせてくれないの! - yohhoyの日記
strict aliasing rules, type punning解説 その1 - gununuの日記
reinterpret_cast conversion - cppreference.com
std::launder関数 - yohhoyの日記
std::launder - cppreference.com
Aggregates 集成体 - C++と色々