C90の規格票(「プログラム言語C JISX3010-1993 (ISO/IEC 9899:1990)」)を読んでいます。今回は、関係演算子と等価演算子ついて調べてみました。
##用語
まず始めに、本記事中の用語の定義を示しておきます。
- 関係演算子とは「>,<,>=,<=」のことです。
- 等価演算子とは「==,!=」のことです。
- オブジェクト型とは、関数型及び不完全型を除くすべての型(int型、ポインタ型、配列型、共用体型など)を指します。オブジェクト指向の「オブジェクト」とは関係無いです。
- 集成体型とは、配列型と構造体型の総称です。
- スカラ型とは、算術型とポインタ型の総称です。
##異なるオブジェクトへのポインタ、関数ポインタの比較は?
以下は、規格C90ではいずれも未定義の動作となります。
int n, m;
&n > &m; /* 未定義の動作:異なるオブジェクトへのポインタの比較 */
void f(void){}
int main(void){
f >= f; /* 未定義の動作:オペランドが関数型オブジェクトへのポインタ */
int a[10];
int b[10];
a > b; /* 未定義の動作:異なる配列の要素へのポインタの比較 */
return 0;
}
両オペランドがポインタ型オブジェクトである場合、以下が許容されます。
- 同じ構造体型オブジェクトまたは共用体型オブジェクトの任意のメンバへのポインタの比較。
- 同じ配列型オブジェクトの、任意の要素へのポインタまたは最後の要素を一つ超えた要素へのポインタの比較。
配列型配列(要素型が配列型の配列)の場合、同一オブジェクトの任意の要素へのポインタであっても要素の階層(?)が異なる場合、未定義の動作となります。T型配列型配列の先頭要素へのポインタ(T型配列型へのポインタ型)と、T型配列型オブジェクトの先頭要素へのポインタ(T型へのポインタ)では、型が適合しないためです。
以下の場合、同一配列の記憶領域内の要素を指していても、int型へのポインタ(第一オペランド)とint型配列型へのポインタ(第二オペランド)という適合しない型同士を比較しているため、未定義の動作となります。
int a[10][10];
&a[0][0] > &a[0];
また、関数型へのポインタは、関係演算子のオペランドに指定すること自体が許されません。ただし、等価演算子(==、=!)はオペランドに関数型へのポインタを指定可能です(そうでないと、空ポインタとの比較ができなくなってしまう)。
##奇妙なコードが必ずしも未定義の動作になるとは限らない?
にわかには信じがたいですが、以下の奇妙なコードは未定義の動作ではありません。
int n;
&n > &n+1; /* 正しい:関係演算子・等価演算子では、配列型以外へのポインタは「長さ1の配列」として考える */
規格C90の関係演算子及び等価演算子の意味規則の定義においては、int型オブジェクトへのポインタなど「配列型でないオブジェクトへのポインタ」は、要素数1の配列の先頭要素へのポインタと同じ動作をするものとして、演算子の意味規則を定義しています。それを踏まえると、スカラ型や共用体型へのポインタは「(要素数1の配列として考えた時に)任意の要素または最後の要素を一つ越えた要素へのポインタ」同士を比較することは許されるので、上記コードは正しいです(ちなみに、不完全型については陽に禁止されていませんが、配列型の説明において不完全型の配列は作ることはできないと書かれてあるので、不完全型については対象外と考えられます)。
と書いておきながら、正直、ここは半信半疑です。規格(「6.3.8 関係演算子」)を字面通りに読むと許容されるのですが、もし上の解釈で間違ってないのだとすれば、コンパイラ開発者向けに配列の要素型へのポインタと区別せず一括して構文解析できるように気を利かせたのかもしれません。
##評価結果の型
- 関係演算子・等価演算子の評価結果はint型です。
- 関係が真の場合は1、偽の場合は0を返します(「真の場合は0以外を返す」だと思っていたので、ここも意外でした)。