strtol の restrict 修飾について

投稿者: Anonymous

時に、以下のようなプログラム(プログラム自体は特に意味は無いです)を見かけます。

#include <stdio.h>
#include <stdlib.h>

int main(void){
    char *input = "1 5 9 2 6 4 8";
    int array[16];

    char *p = input;
    int i;
    for(i = 0; i < 16 && *p; ++i){
        array[i] = strtol(p, &p, 10);
    }
    for(int n = i, i = 0; i < n; ++i)
        printf("%d ", array[i]);
    return 0;
}

気になるのは、strtol(p, &p, 10) の部分です。

strtol のプロトタイプを見ると、

long strtol(
    const char * restrict nptr,
    char ** restrict endptr
    int base
);

のようになっていて、
第1引数と第2引数にrestrict修飾がされています。
私のざっくりとした理解ではそれは関数内でその引数を使う時に他の引数とかぶってはいけない(同じオブジェクト指すようなものであってはいけない)ということです。(つまり、私の理解では、制約違反。理解が間違っている場合そこら辺も解説して欲しいです。)
この場合、restrict制約に違反していることになるのでしょうか?

それとも型が違うのだから大丈夫?
型が違うから大丈夫なら、そもそも2つの引数にrestrict修飾する必要などないのでは?
逆に、このケースでrestrict制約違反になる場合とはどんな場合?

あと、
restrict制約に違反するようなプログラムを書いた場合、
それはすなわち動作未定義ですか?(例えば第一引数はconstでオブジェクトを変更しないことが明らかなので実質問題無いように思える)


(他に書く所がないのでここで)
コメントで、memcpy(p, p, 0) が動作未定義かどうか?
ということを書いたのですが、
規格のサンプルで、

void h(int n, int * restrict p, int * restrict q, int * restrict r)
{
    int i;
    for (i = 0; i < n; i++)
    p[i] = q[i] + r[i];
}

の場合、

h(100, a, b, b) has defined behavior, because array b is not modified within function h.

って書いてある。
(この場合第1引数と第2・3引数との制約で、第2と第3引数の間の制約ではないとも言える。)
理由として変更がされないからということであれば、
動作未定義かどうかは単にパラメータ(のポインタが同一オブジェクトを指すかどうか)だけでは(実装を知らない使用者側としては)判断ができずに、その実装によることになる。(実際には動作の説明が必要不可欠)
(逆に言えば、未定義動作(期待しないような動作)するような状況になったら、動作未定義だったのだなとわかるw)
だから、memcpy(p, p, 0)の場合も、明らかに変更するような動作が行われないので、defined behaviorだと言える。

解決

前提:C99のrestrictキーワードは「処理系に対する最適化のためのヒント情報」であり、妥当(valid)な Cソースコードから全てのrestrictキーワードを削除しても、プログラムの意味は変化しません[C99 §6.7.3.1/3]。またrestrict修飾を理解するには、前提知識として Strict Aliasing Rules(厳密な別名付けの規則) を理解している必要があります。同規則については packet0さん回答 に説明がありますが、大まかには次の通りです:

  • C言語コンパイラは、非互換な型へのポインタ同士は互いにaliasでないと仮定したコード生成を行ってよい。
  • ただし文字型へのポインタ型(char*)だけは、任意の型とのaliasを仮定する必要がある。

これらはalias解析に関するコンパイル時最適化を許可する規則であり、プログラマはStrict Aliasing Rulesを遵守したコードを記述する義務があります。restrictキーワードは、本来の規則ではaliasを仮定せざるをえないケース(互換型へのポインタやchar*)に対して、最適化を促すヒント情報「このポインタ値についてはaliasを考慮しなくてよい」をコンパイラに伝える仕組みです。


気になるのは、strtol(p, &p, 10)の部分です。
(中略)
この場合、restrict制約に違反していることになるのでしょうか?

いいえ。

  • 第1引数pは、char型オブジェクトの配列(文字列リテラル)を指す指すポインタ値です。
  • 第2引数&pは、char*型オブジェクト(変数p)を指すポインタ値です。

上記の2個のポインタ値は異なるオブジェクトを指します/互いにaliasではありませんから、関数呼び出しstrtol(p, &p, 10)はwell-definedです。

それとも型が違うのだから大丈夫?
型が違うから大丈夫なら、そもそも2つの引数にrestrict修飾する必要などないのでは?

Strict Aliasing Rulesでもchar*は特別扱いされますから、restrict修飾によって初めて「第1仮引数nptrと第2仮引数endptrは互いにaliasではない」ことを表明します。ただし、ここで関数strtolの引数がrestrict修飾されているのは、C標準ライブラリ実装者のため です。アプリケーションプログラマにとってはメリットもデメリットもほぼありません(詳細後述)。

ISO/IEC JTC1/SC22/WG14よりC99仕様策定時のRationale(論理的根拠)が公開されており、§7にてC標準ライブラリにおけるrestrictキーワード利用指針が触れられています。関数strtolのケースに適用できる説明を引用します:

Since the implementation costs are high if vendors are forced to cater to this extremely rare case, the restrict keyword is used to explicitly forbid situations like these.

Another library routine that uses restrict is:

char *fgets(char * restrict s, int n, FILE * restrict stream);

Again, since a character pointer can be a potential alias with other pointers, restrict is used to make it clear to the translator that parameter s is never an alias with parameter stream when the fgets function is called in a strictly conforming program.

C標準ライブラリのうちchar*型とそれ以外のポインタ型を引数にとる関数では、上記理由によりrestrict修飾がされています。

逆に、このケースでrestrict制約違反になる場合とはどんな場合?

下記のような偏執的コードを書いたときです。関数strtolのセマンティクス上このような処理は一般的でないため、アプリケーションプログラマにとって関数strtolでのrestrict修飾有無は影響がありません:

char a[] = “123456789”;
char** p = (char**)(&a[0]); // 配列aの一部をchar*オブジェクトとみなす
strtol(a, p, 10);

restrict制約に違反するようなプログラムを書いた場合、それはすなわち動作未定義ですか?

Strict Aliasing Rulesおよびrestrict修飾に関して、aliasに関する規則に違反したプログラムは未定義動作です。

コメントで、memcpy(p, p, 0)が動作未定義かどうか?

これはwell-definedです。関数memcpyの第3引数に値0を指定した場合、ゼロ文字のコピー動作つまり何もしないことが保証されます[C99 §7.21.1/2]。関数memcpyが未定義動作を引き起こすのは、2つのポインタ値引数が指す領域のうち実際にオーバーラップしている部分でコピーが行われたときです[C99 §7.21.2.1/2]。

回答者: Anonymous

Leave a Reply

Your email address will not be published. Required fields are marked *