【C言語】
未定義動作 vs 未規定動作 vs 処理系依存動作の違いを解説

warning: comparison is always false due to limited range of data type

警告:データ型の範囲制限ため比較は常に偽になります。
[-Wtype-limits]


この記事の概要

この記事では、C言語の「未定義動作」「未規定動作」「処理系定義動作」について解説しています。

具体例を交えて、コンパイラ依存の動作やビットフィールドの定義方法による挙動の違いを説明し、バグの原因を回避するためのベストプラクティスも紹介しています。

初心者に役立つ知識を提供し、コードの信頼性を向上させるための情報を網羅しています。

処理系定義の動作

#include    <stdio.h>
struct  tag {
    int bit:1;//処理系定義の動作
};
int main(void){
    struct tag  x;
    x.bit = 1;
    if(x.bit == 1){
        printf("gccはここに来ない");
    }else{
        printf("gccはここに来る bit = %d\n",x.bit);
    }
}

ビットフィールドを「単なる」int 型で定義してはいけない
signed/unsigned が明示されていない単なる int 型のビットフィールドのsigned/unsignedはコンパイラに依存します。
これを処理系定義の動作といいます。

gcc/clangの場合はsigned になるため
1bitしかないビットフィールドは
【0/-1】しか表現できません

1は表現できないので
     if(x.bit == 1){ 
は成立しません。


●ビットフィールドは unsigned int 型で定義する(推奨修正例)

#include    <stdio.h>
struct  tag {
    unsigned int bit:1;
};
int main(void){
    struct tag  x;
    x.bit = 1;
    if(x.bit == 1){
        printf("gccはここに来る bit = %d\n",x.bit);
    }else{
        printf("gccはここに来ない");
    }
}

signed/unsigned が明示されたビットフィールドは
コンパイラに依存しません

ビットフィールドは【0/1】を表現できるので
     if(x.bit == 1){ 
は成立します。

■未規定の動作

#include <stdio.h>
int a(void){    puts("A");  return  1;  }
int b(void){    puts("B");  return  2;  }
int c(void){    puts("C");  return  3;  }
int     main(void){
    printf("%d:%d:%d\n",a(),b(),c());
}

関数引数の評価順序は未規定の動作
このコードは
未規定の動作と言って
未定義の動作と違いバグではないのですが、
コンパイラによって動作が異なるので
避けて下さい。

ここではgccとclangで動作結果が違う例を紹介します。


gccの結果

./a.out
C
B
A
1:2:3

clangの結果

./a.out
A
B
C
1:2:3

参考:

EXP30-C. 副作用が発生する式の評価順序に依存しない

https://www.jpcert.or.jp/sc-rules/c-exp30-c.html

INT12-C. 式中で使用される単なるintのビットフィールドの型について勝手な想定をしない

https://www.jpcert.or.jp/sc-rules/c-int12-c.html