C言語で型のある定数を定義したい! => 処理系によるけど static const を使うと良いかも
対象読者
- C言語で型のある定数を定義したい奇特な方
- 処理系定義でも構わないという方
C言語で型のある定数を書きたい
現在、Cの規格でデファクトスタンダードとなっているC99においては、定数を定義するときはマクロ定数(#define
)や列挙定数(enum
)を使い定義するのが一般的ですが、#define
そのものはあくまでコードの展開という機能に過ぎず、enum
は32bit符号付き整数型の定数しか定義できないため、実質的に型がありません。
const
があるじゃん。あれは違うわけ?
C99には「定数」をどのように定義しているかにもよりますが、少なくとも数学上においては、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
とすることで、変数を事実上の定数にするという制約を課すことができると言えます。
#define
やenum
とほぼ同等に扱ってくれる
gccやclangでは私のような静的型付き信者にとってグッドニュースがあります。それは、gccやclangだとstatic const
を#define
やenum
とほぼ同等に扱ってくれるということです。
ということで、早速clangで#define
とstatic const
を比較してみましょう。
#include <stdio.h>
- static const int X = 42;
+ #define X (42)
int main(void)
{
printf("%d\n", X);
return 0;
}
#include <stdio.h>
- #define X (42)
+ static const int X = 42;
int main(void)
{
printf("%d\n", X);
return 0;
}
ここでCompiler Explorerというサイトを使って生成されたアセンブリコードを見てみると
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"
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だと若干バイナリサイズが大きくなってしまうようです。
.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
.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も同様です。
.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
.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にて指摘をいただきました。
#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
が#define
やenum
と同様にふるまうという保証は一切ない (ANSI Cで規定されているわけではない) ので、今回私が検証を行ったCコンパイラ以外でコンパイルすることを想定している場合は素直に#define
やenum
を使ったほうが良いでしょう。
それかC23でconstexpr
が追加されるので、良い子はそれがデファクトスタンダード化されるまで待ちましょう。
-
2024/6/30追記
C99の6.7.3/p3の脚注にて、以下の記載があるのを見つけました。この記載を見る限りではconst
が#define
やenum
と同様にふるまうというのは処理系定義であるということです。
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.
(処理系は、非
volatile
なconst
オブジェクトを読み取り専用領域に配置できる。さらに、処理系は、そのオブジェクトのメモリアドレスが使用されない場合、メモリ領域に割り当てる必要はない。)
Discussion