読者です 読者をやめる 読者になる 読者になる

C言語(標準)にM_PIは無い

C言語で円周率πを使うには M_PI を使う、と経験で知っている人は多いが、あれは実はC言語の規格には含まれていない。むしろ、処理系がM_PIを定義してはいけない事情(c - Using M_PI with C89 standard - Stack Overflow)がある。


ここでは、C言語の最新規格であるC11と等価なN1570 (http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf) を参考にする。これより古いC言語の規格でも同様だと思われる。

C言語の規格では、まず「標準に準拠したプログラム」は何かということを定義し、続いて「標準に準拠した処理系」は何かということを定義する、という手順を踏んでいる。

Strictly conforming program

Strictly conforming programとは、最も移植性が高いプログラムである。あるプログラムがstrictly conformingであるかどうかは、この規格書の中で厳密に定義されている。大まかに言うと、

  • 出力が不定動作に依存してはいけない。
  • 出力が未定義動作に依存してはいけない。
  • 出力が処理系定義の動作に依存してはいけない。
  • 出力がminimum implementation limitsを超過してはいけない。(例えば、intは-32767以上32767以下の数値を表現できることは仮定してよいが、それよりも大きいとは仮定してはいけない。)

「不定動作」「未定義動作」「処理系定義の動作」「implementation limitとその最小値」なども、それぞれ厳密に定義されている。

Strictly conforming programの例

#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("Hello, world!\n");
    return 0;
}

Strictly conformingではない例(mainの引数が非標準、GCC拡張であるStatements and Declarations in Expressionsを使っている)

#include <stdio.h>

int main(int argc, char *argv[], char *envp[]) {
    printf(({"Hello, world!\n";}));
    return 0;
}

(参考: N1570 p.8)

Conforming implementation

Conforming implementationとは、全てのstrictly conforming programを正しく動作させる処理系のことである。(その他、#errorでエラーになることや__cplusplusをマクロで定義しないことなど、いくつかの制約がある)

したがって、conforming implementationは、strictly conformingでないプログラムに関してはどう料理してもよい。

  • Conformingだと思われる処理系の例 : gcc -std=c11 (いくつかのGCC拡張は有効だが、C標準と矛盾する機能は無効化される)
    • ただし、GCCで未実装のC11機能があるので、その点ではconformingではなさそう
  • Conformingではない処理系の例 : gcc -std=gnu11

(参考: N1570 p.8)

Conforming program

あるConforming implementationが受理するプログラムはConforming programと呼ばれる。この意味では、上記のCプログラムも「正しい」Cプログラムだが、移植性は保証されない。

(参考: N1570 p.8)

識別子

次のような識別子は予約されている:

  • アンダースコア+大文字、またはアンダースコア2個、で始まる識別子
  • アンダースコアで始まる識別子 (ファイルスコープ、通常の名前空間とタグ名前空間のみ制限)
  • 規格書内で定義されているマクロ名
  • 規格書内で定義されている外部リンク名とerrno (外部リンク名としての使用のみ制限)
  • 規格書内で定義されているファイルスコープの名前 (関連するヘッダーがインクルードされている場合のみ、マクロ名としておよび同一名前空間でのファイルスコープでの使用のみ制限)

つまり、

  • Strictly conforming programは、予約されている識別子を独自に定義・宣言・undefしてはいけない。
  • Strictly conforming programは、予約されていない識別子は自由に使ってよい。
  • Conforming implementationは、予約されていない識別子を独自に定義・宣言してはいけない。 (strictly conforming programの挙動が邪魔されるので)
  • Conforming implementationは、予約されている識別子をある程度自由に使える。

例えば、ヘッダーによくある

#ifndef _HOGE_INCLUDED
#define _HOGE_INCLUDED

some_declarations

#endif

は、

  • ユーザー側のプログラムとしては: アンダースコアで始まっているので、strictly conformingではない。
  • ライブラリとしては: strictly conforming programが使える空間を汚していないので良い。むしろ、そうしないとstrictly conforming programの使える名前空間を汚していることになる。

(参考: N1570 p.182)

math.h

math.hは、規格で存在が要請されているヘッダーの一つである。

規格では、math.hで宣言されているべき識別子の一覧も指定されている。

例えば、

  • 型 : float_t, double_t が定義されている。
  • マクロ : HUGE_VAL, INFINITY, NAN(NaNが使える場合だけ), ... などが定義されている。
  • 関数マクロ : fpclassifyなどのマクロが定義されている。
  • 関数 : cos, sin, log, ...などの関数が宣言されている。

したがって、これらの識別子は予約されていることになる。つまり、strictly conforming programは、

  • cos, sin, logなどを外部リンク名として独自に定義してはいけない。
  • math.hをインクルードしている場合は、logやHUGE_VALなどの識別子を独自にマクロ名やファイルスコープでの名前として使ってはいけない。

一方、問題のM_PIはここでは言及されていない。したがってこの識別子は予約されていない。

したがって、以下のようなプログラムはstrictly conformingではない。(存在しないはずのM_PIを使っているので)

#include <math.h>
#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("PI = %f\n", M_PI);
    return 0;
}

一方、以下のようなプログラムはstrictly conformingである

#include <math.h>
#include <stdio.h>

int M_PI = 3;

int main(int argc, char *argv[]) {
    printf("PI = %d\n", M_PI);
    return 0;
}

(参考: N1570 p.231)

M_PIを定義してはいけない理由

もし、conforming implementationがmath.hでM_PIを定義していたとすると、上記のStrictly conforming program

#include <math.h>
#include <stdio.h>

int M_PI = 3;

int main(int argc, char *argv[]) {
    printf("PI = %d\n", M_PI);
    return 0;
}

が正しく動作しなければいけないという制約に反する。

したがって、conforming implementationではM_PIを定義してはいけない。

実際のコンパイラ

  • gccは-std=c11のもとではM_PIを定義しない。
  • gccは-std=gnu11のもとではM_PIを定義する。 (つまりこのgccはconforming implementationではない)
  • VC++はM_PIを定義しないらしい。

gccについては、gcc (Ubuntu 4.8.2-19ubuntu1) 4.8.2で確認した。(参考
math.h の M_PI などは仕様外だった - Gust Notch? Diary)

_USE_MATH_DEFINES

処理系によっては、

#include <math.h>
#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("PI = %f\n", M_PI);
    return 0;
}

コンパイルできないが、

#define _USE_MATH_DEFINES
#include <math.h>
#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("PI = %f\n", M_PI);
    return 0;
}

のように_USE_MATH_DEFINESを指定すると、正しく動作することがある。

これは、予約された識別子を定義している

#define _USE_MATH_DEFINES

の一行によって、このプログラムがStrictly conforming programではなくなることに由来している。したがって、

#define _USE_MATH_DEFINES
#include <math.h>
#include <stdio.h>

int M_PI = 3;

int main(int argc, char *argv[]) {
    printf("PI = %d\n", M_PI);
    return 0;
}

のように、わざわざM_PIを独自に定義して使おうという危なげなプログラムがあったとしても、これは最初の1行のせいでStrictly conformingではなくなり、処理系はこれを正しく動かす義務がなくなる。

まとめ

  • 厳密には、処理系はM_PIを定義してはいけない。コンパイラーは実際にそれを上手に守っている。
  • 困る