restrict type qualifier (since C99)
C言語の
型システム
における各々の型には、その型のいくつかの
修飾版
が存在し、
const
、
volatile
、およびオブジェクト型へのポインタについては
restrict
修飾子のうち1つ、2つ、またはすべての3つに対応します。このページでは
restrict
修飾子の効果について説明します。
restrict修飾できるのは オブジェクト型 へのポインタ、または(多次元配列を含む)その配列のみ (C23以降) である。特に、以下は 誤り である:
- int restrict * p
- float ( * restrict f9 ) ( void )
restrict セマンティクスは左辺値式にのみ適用されます。例えば、restrict 修飾されたポインタへのキャストや、restrict 修飾されたポインタを返す関数呼び出しは左辺値ではなく、修飾子は効果を持ちません。
制限付きポインタ
P
が宣言されているブロックの各実行中(通常は
P
が関数パラメータである関数本体の各実行中)、
P
を通じてアクセス可能なオブジェクト(直接的または間接的に)が何らかの方法で変更される場合、そのブロック内での当該オブジェクトへのすべてのアクセス(読み取りと書き込みの両方)は
P
を通じて(直接的または間接的に)行われなければなりません。そうでない場合、動作は未定義です:
void f(int n, int * restrict p, int * restrict q) { while (n-- > 0) *p++ = *q++; // *pを通して変更されるオブジェクトは、*qを通して読み取られる // どのオブジェクトとも同じではない // コンパイラは最適化、ベクトル化、ページマップなどを自由に行える } void g(void) { extern int d[100]; f(50, d + 50, d); // OK f(50, d + 1, d); // 未定義動作: d[1]がf内でpとqの両方を通してアクセスされる }
オブジェクトが決して変更されない場合、それはエイリアス化され、異なるrestrict修飾ポインタを通じてアクセスされる可能性があります(エイリアス化されたrestrict修飾ポインタが指すオブジェクトが、さらにポインタである場合、このエイリアシングは最適化を妨げる可能性があることに注意してください)。
ある制限付きポインタから別の制限付きポインタへの代入は未定義動作です。ただし、外側のブロック内のオブジェクトへのポインタから内側のブロック内のポインタへの代入(制限付きポインタ引数を持つ関数を呼び出す際に制限付きポインタ引数を使用する場合を含む)、または関数からの戻り時(およびその他の場合で元のポインタのブロックが終了した時)は例外です:
int* restrict p1 = &a; int* restrict p2 = &b; p1 = p2; // 未定義動作
制限付きポインタは制限なしポインタに自由に代入できます。コンパイラがコードを解析できる限り、最適化の機会は維持されます:
void f(int n, float * restrict r, float * restrict s) { float *p = r, *q = s; // OK while (n-- > 0) *p++ = *q++; // ほぼ確実に *r++ = *s++ と同様に最適化される }
|
restrict型修飾子を使用して配列型が宣言された場合( typedef の使用を通じて)、配列型自体はrestrict修飾されませんが、その要素型はrestrict修飾されます: |
(C23まで) |
|
配列型とその要素型は常に同一のrestrict修飾を持つものと見なされます: |
(C23以降) |
typedef int *array_t[10]; restrict array_t a; // aの型は int *restrict[10] // 注記: clangとiccはarray_tがポインタ型ではないことを理由にこれを拒否する void *unqual_ptr = &a; // C23まではOK; C23以降はエラー // 注記: clangはC89-C17モードでもC++/C23のルールを適用する
関数宣言において、キーワード
restrict
は、関数パラメータの配列型を宣言するために使用される角括弧内に現れることがあります。これは配列型が変換されるポインタ型を修飾します:
void f(int m, int n, float a[restrict m][n], float b[restrict m][n]); void g12(int n, float (*p)[n]) { f(10, n, p, p+10); // 正常 f(20, n, p, p+10); // 未定義動作の可能性あり(fの実装による) }
目次 |
注記
restrict修飾子の意図された使用法(registerストレージクラスと同様)は最適化を促進するためのものであり、適合プログラムを構成するすべてのプリプロセス翻訳単位からこの修飾子のすべてのインスタンスを削除しても、その意味(すなわち、観測可能な動作)は変化しません。
コンパイラは
restrict
の使用に伴うエイリアシングの影響を任意に無視することができます。
未定義動作を避けるためには、プログラマは restrict 修飾ポインタによってなされるエイリアシングの表明が侵害されないことを保証しなければなりません。
多くのコンパイラは、言語拡張として
restrict
の逆機能を提供しています:ポインタが型が異なっていてもエイリアスする可能性があることを示す属性です:
may_alias
(gcc)、
使用パターン
restrict修飾ポインターにはいくつかの一般的な使用パターンがあります:
ファイルスコープ
ファイルスコープのrestrict修飾ポインタは、プログラムの実行期間中、単一の配列オブジェクトを指さなければなりません。その配列オブジェクトは、restrict修飾ポインタと、その宣言名(存在する場合)または別のrestrict修飾ポインタの両方を介して参照されてはなりません。
ファイルスコープで制限されたポインタは、動的に割り当てられたグローバル配列へのアクセスを提供する際に有用です。restrict セマンティクスにより、このポインタを通じた参照を、宣言された名前を通じた静的配列への参照と同様に効果的に最適化することが可能になります:
float *restrict a, *restrict b; float c[100]; int init(int n) { float * t = malloc(2*n*sizeof(float)); a = t; // aは前半部分を参照 b = t + n; // bは後半部分を参照 } // コンパイラはrestrict修飾子から、 // 名前a、b、cの間に潜在的なエイリアシングがないことを推論できる
関数パラメータ
restrict修飾ポインタの最も一般的な使用例は、関数パラメータとしての使用です。
以下の例では、コンパイラは変更されたオブジェクトのエイリアシングが存在しないと推論し、ループを積極的に最適化する可能性があります。
f
へのエントリ時、restrictポインタaはその関連配列への排他的アクセスを提供しなければなりません。特に
f
内では、
b
も
c
も
a
に関連付けられた配列を指してはならず、これはどちらも
a
に基づくポインタ値が割り当てられていないためです。
b
についてはこれはその宣言におけるconst修飾子から明らかですが、
c
については
f
の本体の検査が必要です:
float x[100]; float *c; void f(int n, float * restrict a, float * const b) { int i; for ( i=0; i<n; i++ ) a[i] = b[i] + c[i]; } void g3(void) { float d[100], e[100]; c = x; f(100, d, e); // 正常 f( 50, d, d+50); // 正常 f( 99, d+1, d); // 未定義動作 c = d; f( 99, d+1, e); // 未定義動作 f( 99, e, d+1); // 正常 }
cがbに関連付けられた配列を指すことが許可されていることに注意してください。また、これらの目的において、特定のポインタに関連付けられた「配列」とは、そのポインタを通じて実際に参照される配列オブジェクトの部分のみを意味することにも注意してください。
上記の例では、bのconst性によって関数本体内でaに依存することが保証されないため、コンパイラはaとbがエイリアスしないと推論できることに注意してください。同様に、プログラマは void f ( int n, float * a, float const * restrict b ) と記述することもでき、その場合コンパイラはbを通じて参照されるオブジェクトが変更されないと判断できるため、bとaの両方を使用して変更されたオブジェクトを参照できないと推論できます。もしプログラマが void f ( int n, float * restrict a, float * b ) と記述した場合、コンパイラは関数本体を検査せずにaとbの非エイリアシングを推論することはできません。
一般的に、関数のプロトタイプ内のすべての非エイリアシングポインタは restrict で明示的に注釈することが推奨されます。
ブロックスコープ
ブロックスコープのrestrict修飾ポインタは、そのブロックに限定されたエイリアシング表明を行います。これにより、タイトなループのような重要なブロックにのみ適用されるローカルな表明が可能になります。また、restrict修飾ポインタを受け取る関数をマクロに変換することも可能にします:
float x[100]; float *c; #define f3(N, A, B) \ do \ { int n = (N); \ float * restrict a = (A); \ float * const b = (B); \ int i; \ for ( i=0; i<n; i++ ) \ a[i] = b[i] + c[i]; \ } while(0)
構造体メンバー
restrict修飾されたポインタが構造体のメンバである場合に作成されるエイリアシング表明のスコープは、その構造体へのアクセスに使用される識別子のスコープです。
構造体がファイルスコープで宣言されている場合でも、構造体へのアクセスに使用される識別子がブロックスコープを持つとき、構造体内のエイリアシング表明もブロックスコープを持ちます。エイリアシング表明は、この構造体型のオブジェクトがどのように作成されたかに応じて、ブロックの実行内または関数呼び出し内でのみ有効です:
struct t // restrictポインタは、メンバーが互いに素な記憶域を指すことを表明する { int n; // メンバーが互いに素な記憶域を指す float * restrict p; float * restrict q; }; void ff(struct t r, struct t s) { struct t u; // r,s,uはブロックスコープを持つ // r.p, r.q, s.p, s.q, u.p, u.qはすべて、ffの各実行中に // 互いに素な記憶域を指すべきである // ... }
キーワード
例
コード生成の例; -S (gcc, clangなど) または /FA (Visual Studio) でコンパイル
int foo(int *a, int *b) { *a = 5; *b = 6; return *a + *b; } int rfoo(int *restrict a, int *restrict b) { *a = 5; *b = 6; return *a + *b; }
出力例:
; 64ビットIntelプラットフォームで生成されたコード: foo: movl $5, (%rdi) ; *aに5を格納 movl $6, (%rsi) ; *bに6を格納 movl (%rdi), %eax ; 前回の格納操作で変更された可能性があるため*aから再読み込み addl $6, %eax ; *aから読み込んだ値に6を加算 ret rfoo: movl $11, %eax ; 結果は11(コンパイル時定数) movl $5, (%rdi) ; *aに5を格納 movl $6, (%rsi) ; *bに6を格納 ret
参考文献
- C23規格 (ISO/IEC 9899:2024):
-
- 6.7.3.1 restrictの形式的定義 (p: TBD)
- C17規格 (ISO/IEC 9899:2018):
-
- 6.7.3.1 restrictの形式的定義 (p: 89-90)
- C11標準 (ISO/IEC 9899:2011):
-
- 6.7.3.1 restrictの形式的定義 (p: 123-125)
- C99規格 (ISO/IEC 9899:1999):
-
- 6.7.3.1 restrictの形式的定義 (p: 110-112)