LoginSignup
0
0

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

Last updated at Posted at 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というサイトを使って生成されたアセンブリコードを見てみると

#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だと若干バイナリサイズが大きくなってしまうようです。

#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も同様です。

#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補足 @fujitanozomu 様より

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

0
0
18

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0