©️

C言語で型のある定数を定義したい! => 処理系によるけど static const を使うと良いかも

2024/06/29に公開

対象読者

  • C言語で型のある定数を定義したい奇特な方
  • 処理系定義でも構わないという方

C言語で型のある定数を書きたい

現在、Cの規格でデファクトスタンダードとなっているC99においては、定数を定義するときはマクロ定数(#define)や列挙定数(enum)を使い定義するのが一般的ですが、#defineそのものはあくまでコードの展開という機能に過ぎず、enumは32bit符号付き整数型の定数しか定義できないため、実質的に型がありません。

C99にはconstがあるじゃん。あれは違うわけ?

「定数」をどのように定義しているかにもよりますが、少なくとも数学上においては、Cのconstは「定数」であるとは言えません。
なぜなら、Cのconstは定数式以外も格納できるからです。たとえば

#include <fcntl.h>

int main(void)
{
    const int fd = open("example.txt", O_RDONLY);
    /* 省略 */
    return 0;
}

において、fdは「定数」とは言えないでしょう。なぜなら関数openは、外部の影響によって何を返すか変わってくるわけで、そうなるとfdの値は一定であるとは言えないからです。
では、Cのconstとは何であると解釈すべきか?私は、比較的最近の言語でよく見られる不変変数と解釈し、その意味合いとしてよく使っております。
実際、昨今のCのコードでも、constは「定数」よりもこっちの意味合いで使われていることが多い印象です。

static constが定数の代わりとなるか

ANSI Cではstatic指定子を付けて宣言された変数は、プログラム開始前に初期化しなければならない都合上、初期値は定数式しか受け付けない、と規定されています(C99 6.7.8/p4)。

All the expressions in an initializer for an object that has static storage duration shall be constant expressions or string literals.

(静的記憶域期間を持つオブジェクトの初期化子内のすべての式は、定数式または文字列リテラルでなければならない。)

このため、static constとすることで、変数を事実上の定数にするという制約を課すことができると言えます

gccやclangでは#defineenumとほぼ同等に扱ってくれる

私のような静的型付き信者にとってグッドニュースがあります。それは、gccやclangだとstatic const#defineenumとほぼ同等に扱ってくれるということです。
ということで、早速clangで#definestatic constを比較してみましょう。

#define
  #include <stdio.h>

- static const int X = 42;
+ #define X   (42)

  int main(void)
  {
      printf("%d\n", X);

      return 0;
  }
static const
  #include <stdio.h>

- #define X   (42)
+ static const int X = 42;

  int main(void)
  {
      printf("%d\n", X);

      return 0;
  }

ここでCompiler Explorerというサイトを使って生成されたアセンブリコードを見てみると

https://godbolt.org/z/dEaKEMT55

#define
  main:                                   # @main
          push    rbp
          mov     rbp, rsp
          sub     rsp, 16
          mov     dword ptr [rbp - 4], 0
          lea     rdi, [rip + .L.str]
          mov     esi, 42
          mov     al, 0
          call    printf@PLT
          xor     eax, eax
          add     rsp, 16
          pop     rbp
          ret
  .L.str:
          .asciz  "%d\n"
static const
  main:                                   # @main
          push    rbp
          mov     rbp, rsp
          sub     rsp, 16
          mov     dword ptr [rbp - 4], 0
          lea     rdi, [rip + .L.str]
          mov     esi, 42
          mov     al, 0
          call    printf@PLT
          xor     eax, eax
          add     rsp, 16
          pop     rbp
          ret
  .L.str:
          .asciz  "%d\n"

まったく同じコードが生成されました。

めでたしめでたし・・・と言いたいところですが、gccだと若干バイナリサイズが大きくなってしまうようです。

https://godbolt.org/z/h1fvqY9bj

#define
  .LC0:
         .string "%d\n"
  main:
          push    rbp
          mov     rbp, rsp
-         mov     eax, 42
-         mov     esi, eax
+         mov     esi, 42
          mov     edi, OFFSET FLAT:.LC0
          mov     eax, 0
          call    printf
          mov     eax, 0
          pop     rbp
          ret
static const
  .LC0:
          .string "%d\n"
  main:
          push    rbp
          mov     rbp, rsp
-         mov     esi, 42
+         mov     eax, 42
+         mov     esi, eax
          mov     edi, OFFSET FLAT:.LC0
          mov     eax, 0
          call    printf
          mov     eax, 0
          pop     rbp
          ret

これは最適化することでまったく同一のコードになります。clangも同様です。

https://godbolt.org/z/8YxxqT1q6

#define
  .LC0:
          .string "%d\n"
  main:
          sub     rsp, 8
          mov     esi, 42
          mov     edi, OFFSET FLAT:.LC0
          xor     eax, eax
          call    printf
          xor     eax, eax
          add     rsp, 8
          ret
static const
  .LC0:
          .string "%d\n"
  main:
          sub     rsp, 8
          mov     esi, 42
          mov     edi, OFFSET FLAT:.LC0
          xor     eax, eax
          call    printf
          xor     eax, eax
          add     rsp, 8
          ret

とは言え、リリースする際は最適化コンパイルせずにリリースすることはまずないため、弊害があるとすればデバッグするときぐらいでしょう。

2024/6/30 補足

Qiitaにて指摘をいただきました。

https://qiita.com/Ukicode/items/a44a1e2dd070562c39b0#comment-d060843a415ea2c6922f

#define X   (42)

int a[X];

int main(void)
{
    extern int x;
    switch (x) {
    case X:
        break;
    }
}

はgccとclangで普通にコンパイルが通りますが

- #define X   (42)
+ static const int X = 42;

とすると

<source>:3:5: error: variably modified 'a' at file scope
    3 | int a[X];
      |     ^
<source>: In function 'main':
<source>:9:5: error: case label does not reduce to an integer constant
    9 |     case X:
      |     ^~~~

https://godbolt.org/z/6rTxh13Ed

gccではエラーとなってしまう

これは、static constが所詮変数でしかないため、gccでは配列の静的なサイズ、もしくはcaseとしては使えないという話ですね (それ以上にclangでは通ることに驚きですが)
実はこれに関しては、この記事自体がそこまで深堀りするつもりはなかったため、認知していたうえであえて書かなかったのですが、さすがにこの点を書かないのも酷かなとも思いましたので、コメントを引用させていただく形で補足いたします。 (コメントしてくださった方ありがとうございました!)

ここまで書いといて注意点というか

当然ながら(?)、gccやclang以外でstatic const#defineenumと同様にふるまうという保証は一切ない (ANSI Cで規定されているわけではない) ので、今回私が検証を行ったCコンパイラ以外でコンパイルすることを想定している場合は素直に#defineenumを使ったほうが良いでしょう。

それかC23でconstexprが追加されるので、良い子はそれがデファクトスタンダード化されるまで待ちましょう。

  • 2024/6/30追記
    C99の6.7.3/p3の脚注にて、以下の記載があるのを見つけました。この記載を見る限りではconst#defineenumと同様にふるまうというのは処理系定義であるということです。

The implementation may place a const object that is not volatile in a read-only region of storage. Moreover, the implementation need not allocate storage for such an object if its address is never used.

(処理系は、非volatileconstオブジェクトを読み取り専用領域に配置できる。さらに、処理系は、そのオブジェクトのメモリアドレスが使用されない場合、メモリ領域に割り当てる必要はない。)

Discussion