厳密なエイリアスとは何ですか? パート1

(または、めくタイピング、あいまいな振る舞い、整合性、なんてこった!)



みなさん、こんにちは。数週間後に、 「C ++ Developer」コースで新しいスレッドを開始します。 このイベントは、今日の資料に捧げられます。



厳密なエイリアスとは何ですか? まず、エイリアシングとは何かを説明し、次に厳密性が何であるかを調べます。



CおよびC ++では、エイリアシングは、保存された値へのアクセスが許可されている式のタイプに関連しています。 CとC ++の両方で、標準はどの命名式がどのタイプに対して有効であるかを定義します。 コンパイラとオプティマイザは、エイリアシングのルール、つまり厳密なエイリアシングのルール(厳密なエイリアシングルール)に厳密に従うことを前提としています。 無効な型を使用して値にアクセスしようとすると、未定義の動作(UB)として分類されます。 不確実な行動がある場合、すべての賭けが行われ、プログラムの結果は信頼できなくなります。



残念ながら、エイリアシング違反が厳しいと、期待どおりの結果が得られることが多く、新しい最適化を備えたコンパイラの将来のバージョンが有効と見なしたコードに違反する可能性があります。 これは望ましくありません。エイリアシングの厳密な規則を理解し、それらを破らないようにする価値があります。







なぜこれが私たちに関係するのかをよりよく理解するために、厳密なエイリアスルールに違反する場合に発生する問題、厳密なエイリアスルールでよく使用される「punning」、および適切にpunを作成する方法について説明します。 C ++ 20のいくつかの可能なヘルプは、しゃれを単純化し、エラーの可能性を減らします。 厳密なエイリアシング規則の違反を検出するためのいくつかの方法を検討することにより、議論を要約します。



予備的な例



いくつかの例を見てみましょう。次に、標準で正確に述べられていることを議論し、いくつかの追加の例を検討してから、厳密なエイリアスを回避し、見逃した違反を特定する方法を見てみましょう。 ここにあなたを驚かせるべきではないがあります:



int x = 10; int *ip = &x; std::cout << *ip << "\n"; *ip = 12; std::cout << x << "\n";
      
      





int *はintが占有しているメモリを指し、これは有効なエイリアシングです。 オプティマイザーは、ipを介した割り当てによってxが占める値を更新できると想定する必要があります。



次のは、未定義の動作を引き起こすエイリアシングを示しています。



 int foo( float *f, int *i ) { *i = 1; *f = 0.f; return *i; } int main() { int x = 0; std::cout << x << "\n"; // Expect 0 x = foo(reinterpret_cast<float*>(&x), &x); std::cout << x << "\n"; // Expect 0? }
      
      





foo関数では、int *とfloat *を使用します。 この例では、fooを呼び出し、両方のパラメーターが同じメモリ位置を指すように設定します。この例ではintが含まれています。 reinterpret_castは、テンプレートパラメータで指定された型を持っているかのように式を扱うようにコンパイラに指示することに注意してください。 この場合、&x式をfloat *型であるかのように処理するように彼に伝えます。 2番目のcoutの結果は単純に0であると予想できますが、-O2とgccを使用して最適化を有効にすると、clangは次の結果を取得します。

0

1



これは予期しないことかもしれませんが、未定義の動作を引き起こしたため、完全に正しいです。 floatをintオブジェクトの有効なエイリアスにすることはできません。 したがって、オプティマイザーは、fを介した保存がintオブジェクトに正しく影響を与えないため、iの逆参照中に格納された定数1が戻り値になると想定できます。 Compiler Explorerでコードを接続すると、これがまさに起こることがわかります( )。



 foo(float*, int*): # @foo(float*, int*) mov dword ptr [rsi], 1 mov dword ptr [rdi], 0 mov eax, 1 ret
      
      





Type-Based Alias Analysis(TBAA)を使用するオプティマイザーは、1が返されることを想定しており、戻り値を格納するeaxレジスターに定数値を直接移動します。 TBAAは、ロードおよびストレージを最適化するために、エイリアスに使用できるタイプに関する言語規則を使用します。 この場合、TBAAはfloatをintのエイリアスにできないことを認識しており、iロードを最適化して死に至らせます。



今、リファレンスに



私たちが許可されていることと許可されていないことについて、規格は正確に何を言っていますか? 標準言語は単純ではないため、各要素について、意味を示すコード例を提供しようとします。



C11標準には何と書かれていますか?



C11標準では、段落7の「6.5式」セクションで次のように記述されています。



オブジェクトには独自の保存された値が必要です。この値へのアクセスは、次のいずれかのタイプの左辺値式の助けを借りてのみ実行されます。88)-オブジェクトの有効なタイプと互換性のあるタイプ



 int x = 1; int *p = &x; printf("%d\n", *p); //* p   lvalue-  int,    int
      
      





-現在のオブジェクトのタイプと互換性のあるタイプの修飾バージョン、



 int x = 1; const int *p = &x; printf("%d\n", *p); // * p   lvalue-  const int,    int
      
      





-修飾されたタイプのオブジェクトに対応する符号付きまたは符号なしのタイプであるタイプ、



 int x = 1; unsigned int *p = (unsigned int*)&x; printf("%u\n", *p ); // *p   lvalue-  unsigned int,     
      
      





gcc / clang拡張については脚注12を参照してください 。これにより、互換性のない型であっても、unsigned int * int *を割り当てることができます。



-現在のタイプのオブジェクトの修飾バージョンに対応する符号付きまたは符号なしのタイプであるタイプ、



 int x = 1; const unsigned int *p = (const unsigned int*)&x; printf("%u\n", *p ); // *p   lvalue-  const unsigned int,     ,       
      
      





-メンバーに上記のタイプのいずれかを含む集約または複合タイプ(再帰的に、サブ集約または包含された関連付けのメンバーを含む)、または



 struct foo { int x; }; void foobar( struct foo *fp, int *ip );// struct foo -  ,   int   ,       *ip // foo f; foobar( &f, &f.x );
      
      





-文字タイプ。



 int x = 65; char *p = (char *)&x; printf("%c\n", *p ); // * p   lvalue-  char,    . //    -    .
      
      





C ++ 17 Draft Standardが言うこと



セクション11 [basic.lval]のC ++ 17プロジェクト標準では、プログラムが次のタイプのいずれか以外のglvalueを介してオブジェクトの保存された値にアクセスしようとすると、動作は未定義です:63(11.1)は動的なタイプのオブジェクト、



 void *p = malloc( sizeof(int) ); //   ,       int *ip = new (p) int{0}; // placement new      int std::cout << *ip << "\n"; // * ip   glvalue-  int,      
      
      





(11.2)-オブジェクトの動的型のcv修飾(cv-constおよびvolatile)バージョン、



 int x = 1; const int *cip = &x; std::cout << *cip << "\n"; // * cip    glvalue  const int,   cv-    x
      
      





(11.3)-オブジェクトの動的タイプに類似したタイプ(7.5で定義)



//







(11.4)-オブジェクトの動的型に対応する符号付きまたはなしの型である型、

// si ui ,





// godbolt (https://godbolt.org/g/KowGXB) , .







 signed int foo( signed int &si, unsigned int &ui ) { si = 1; ui = 2; return si; }
      
      





(11.5)-符号付きまたは符号なしの型であり、オブジェクトの動的型のcv修飾バージョンに対応する型。



 signed int foo( const signed int &si1, int &si2); //  ,    
      
      





(11.6)は、その要素または非静的データ要素(再帰的に、サブ集約またはデータ要素または非静的データ要素を含む)の中に上記の型のいずれかを含む集約型または結合型です。



 struct foo { int x; };
      
      





// Compiler Explorer (https://godbolt.org/g/z2wJTC)







 int foobar( foo &fp, int &ip ) { fp.x = 1; ip = 2; return fp.x; } foo f; foobar( f, fx );
      
      





(11.7)-型(おそらくcv修飾)動的オブジェクト型の基本クラス型、



 struct foo { int x ; }; struct bar : public foo {}; int foobar( foo &f, bar &b ) { fx = 1; bx = 2; return fx; }
      
      





(11.8)-char、unsigned char、またはstd :: byteを入力します。



 int foo( std::byte &b, uint32_t &ui ) { b = static_cast<std::byte>('a'); ui = 0xFFFFFFFF; return std::to_integer<int>( b ); // b   glvalue-  std::byte,      uint32_t }
      
      





signed char



上記のリストに含まれていないことに注意する価値signed char



あります。これは、文字の種類について話すCとの顕著な違いです。



微妙な違い



したがって、CとC ++はエイリアシングについて同様のことを言っていることがわかりますが、注意すべき違いがいくつかあります。 C ++には、 有効または互換性のある型のC概念がなく、Cには動的または類似の型のC ++概念がありません。 両方とも左辺値と右辺値の式がありますが、C ++にはglvalue、prvalue、xvalueの式もあります。 これらの違いは主にこの記事の範囲外ですが、興味深い例の1つは、mallocが使用するメモリからオブジェクトを作成する方法です。 Cでは、たとえばlvalueまたはmemcpyを介してメモリに書き込むなど、有効なタイプを設定できます。



 //     C,    C ++ void *p = malloc(sizeof(float)); float f = 1.0f; memcpy( p, &f, sizeof(float)); //   *p - float  C //  float *fp = p; *fp = 1.0f; //   *p - float  C
      
      





これらのメソッドはいずれも、C ++では十分ではありません。新しいC ++を配置する必要があります。



 float *fp = new (p) float{1.0f} ; //   *p  float
      
      





int8_tおよびuint8_tはchar型ですか?



理論的には、int8_tもuint8_tもchar型ではありませんが、実際にはそのように実装されています。 これが重要なのは、それらが本当に文字型である場合、char型のようなエイリアスでもあるためです。 これを認識していない場合、予期しない パフォーマンスの 低下につながる可能性があります。 glibc typedef



signed char



unsigned char



それぞれint8_t



uint8_t



であることglibc typedef



int8_t



ます。



C ++ではABIギャップになるため、変更するのは難しいでしょう。 これにより、名前の歪みが変更され、インターフェイスでこれらのタイプのいずれかを使用しているAPIが破損します。



最初の部分の終わり。 また、入力と配置のしゃれについては、数日中にお知らせします。



コメントを書いて、3月6日にRambler&Coの技術開発責任者であるDmitry Shebordaevが開催する公開ウェビナーをお見逃しなく。



All Articles