はじめに
どうも、y-tetsuです。
皆さん、C言語の学習お疲れ様です!筆者ものんびりですが、日々励んでおります。
最近は、本記事のタイトルにもしているアンダースコアではじまる予約語というものを学びました。
「アンダースコアではじまる予約語!?なんだそれ??」
……ってリアクションをいただけた方、大変ありがとうございます!とてもいい人!!
実のところ、調べてみるまでは筆者も「ハトが豆鉄砲を食ったような顔」してました…。ですので、これからはちょっとでも「知ってるよ」と言えるよう、備忘録を残したいと思います。
是非ぜひ、皆さんも読んでいってください。
予約語について
まず最初に、一口にC言語で予約語といっても、実はいろいろな種類が含まれています。
大まかに、予約語の種類を以下に示します。
種類 | 役割 | 例 |
---|---|---|
キーワード | 構文や文法で特別な意味を持つ単語 |
int 、return 、if 、while など |
プリプロセッサディレクティブ | コンパイル前にプリプロセッサが処理するコマンド |
#define 、#include など |
その他識別子 | 標準ライブラリやコンパイラ実装で使用されるもの |
__FILE__ 、__LINE__ など |
今回ご紹介するものはその内の1つ、キーワードになります。
C言語(C11)で予約されているキーワードは44個あるそうです。その内、アンダースコアではじまるものは10個あります。当然ですが、予約されているものは識別子(変数名、関数名、型名など)として使ってはいけません。
今回の主役の面々をざっと並べてみます。(アルファベット順)
_Alignas
、_Alignof
、_Atomic
、_Bool
、_Complex
、_Generic
、_Imaginary
、_Noreturn
、_Static_assert
、_Thread_local
いかがでしょうか?ご存じのものはありましたかね。
ところで _
って、アンダースコア、アンダーバー、アンダーライン、と色々な呼び名がありますが、一体どれが最適なのでしょうか。
試しにC11の最終ドラフトであるN1570を見てみると、underscore
との記載がされておりました。やはり、"アンダースコア"と呼ぶのが良さそうかなと思います。
アンダースコアの意味
つづいて本題へ入る前に、キーワードの先頭にアンダースコア _
を付ける意味について触れておきます。
N1570には、以下のように記載されています。
(「7.1.3 Reservedidentifiers 1」より抜粋)
・All identifiers that begin with an underscore and either an uppercase letter or another
underscore are always reserved for anyuse.
・All identifiers that begin with an underscore are always reserved for use as identifiers
with file scope in both the ordinary and tag name spaces.
・アンダースコアで始まり、その後に大文字のアルファベットまたは別のアンダースコアが続く識別子は、常にあらゆる用途に予約されています
・アンダースコアで始まるすべての識別子は、通常の名前空間およびタグ名空間において、ファイルスコープの識別子として常に使用するために予約されています。
ここから、以下がわかりますね。
-
_Some_identifier
のような、「アンダースコア + 大文字」は予約されてるので使っちゃダメ -
__SOME_IDENTIFIER__
のような、「アンダースコア + アンダースコア」も駄目 -
_some_identifier
のような、「アンダースコア」もだめ(関数やif
文などブロック内のローカル変数は一応OK)
これは主に、アンダースコアで始まる識別子は、コンパイラや標準ライブラリの実装で内部的に使用される意図だと思われます。
ちなみに、C99やC11でそれぞれ追加されたキーワードは以下のようになっています。
C標準 | 追加されたキーワード |
---|---|
C99 |
_Bool 、_Complex 、_Imaginary 、inline 、restrict
|
C11 |
_Alignas 、_Alignof 、_Atomic 、_Generic 、_Noreturn 、_Static_assert 、_Thread_local
|
という事で、ほとんどの新しいキーワードには _
が先頭に付いていました。バッティングする恐れがないので、C規格のバージョンアップ後も既存のコードはすんなり動くはずですよね。
理由は定かではないのですが、inline
と restrict
だけは特別のようです。
ChatGPT(GPT-4o)は以下のように言っておりました。
inline
やrestrict
のようなアンダースコアの付いていないキーワードは、C言語の標準ライブラリや多くのコードベースで頻繁に使われることが想定されるため、アンダースコアなしの形が選ばれました。これらのキーワードは、高速化や最適化を目的として使われるため、コードの読みやすさと簡潔さが重要視されます。
さらに、これらのキーワードがC99で追加される以前、inline
は多くのコンパイラで既に拡張としてサポートされていたため、既存の使用法と整合性を保つためにアンダースコアなしのキーワードとして標準に組み込まれました。
なるほど!と思いましたが、誤っておりましたら申し訳ございません。
アンダースコアではじまるキーワードたち
さて、準備万端となりました。
いよいよ、アンダースコアではじまる10個のキーワードたちを見ていきましょう。
本記事の実行結果は、GCC 14.1.0のものとなっております。
型にまつわるシリーズ
まずは、取っつきやすいものから。
いずれも int
や double
の同類となる、変数の「型」を指定するものたちです。
_Bool
、_Complex
、_Imaginary
_Bool
_Bool
は、符号なしの整数型で、真(1)か偽(0)の2つの値をとる真理値を表すデータ型です。
#include <stdio.h>
int main()
{
_Bool a;
int i;
for (i=0; i<5; i++)
{
a = i;
printf("a = %d -> %d\n", i, a);
}
return 0;
}
_Bool
型の変数 a
に 0 から 4 までの、5つの値を代入するとどうなるか見てみます。
$ gcc ./bool.c -o bool
$ ./bool
a = 0 -> 0
a = 1 -> 1
a = 2 -> 1
a = 3 -> 1
a = 4 -> 1
0 は "0" それ以外は "1" となっており、確かに2値となっていることが分かりますね。
stdbool.h
をインクルードすると、C++と同様の、 bool
、true
、false
が使えるようになります。
#include <stdio.h>
#include <stdbool.h>
int main()
{
bool a;
a = true;
printf("true = %d\n", a);
a = false;
printf("false = %d\n", a);
a = 5;
printf("5 = %d\n", a);
return 0;
}
$ gcc ./stdbool.c -o stdbool
$ ./stdbool
true = 1
false = 0
5 = 1
bool
は _Bool
の同義語で、true
は1、false
は0に等しい記号定数です。
_Complex
_Complex
は複素数型とよばれ、複素数を表すためのデータ型です。
複素数は実部と虚部で構成されており、複素数 $z$ は、$z = x + y × i$ で表現されます。($i$は虚数単位)
_Complex
を使う場合、プログラム中で虚数単位を扱うマクロ I
や、実部や虚部を取得する関数(creal
や cimag
)などを扱うために、complex.h
のインクルードがほぼ必須となりそうです。
使用例を以下に示します。
#include <stdio.h>
#include <complex.h>
int main()
{
double _Complex z1 = 1.0 + 2.0 * I; // 複素数 1 + 2i
double _Complex z2 = 1.0 - 1.0 * I; // 複素数 1 - i
printf("z1 : %.1f + %.1fi\n", creal(z1), cimag(z1));
printf("z2 : %.1f + %.1fi\n", creal(z2), cimag(z2));
// 加算
double _Complex z3 = z1 + z2;
printf("z3 : %.1f + %.1fi\n", creal(z3), cimag(z3));
// z3を原点を中心に 90°反時計回りに回転
double _Complex z4 = z3 * I;
printf("z4 : %.1f + %.1fi\n", creal(z4), cimag(z4));
return 0;
}
複素数 z1
と z2
を加算して z3
を求め、さらにそれに I
を掛けて-90°回転させた z4
を求める計算をしています。
$ gcc ./complex.c -o complex
$ ./complex
z1 : 1.0 + 2.0i
z2 : 1.0 + -1.0i
z3 : 2.0 + 1.0i
z4 : -1.0 + 2.0i
プログラムの実行結果は、下図のようになります。
複素数型には以下の3つがあります。
float _Complex
double _Complex
long double _Complex
単独の _Complex
は double _Complex
と解釈されるとのこと。内部的には、実部と虚部の2要素の配列で実現されているそうです。
_Imaginary
_Imaginary
は純虚数型とよばれ、先ほどの複素数型の虚数部分のみを表すためのデータ型です。
サンプルコードを示します。
#include <stdio.h>
#include <complex.h>
int main()
{
double _Imaginary z = 3.0 * I;
printf("%.1fi\n", cimag(z));
return 0;
}
……と、コードを示してはみたものの、これを実行できる処理系は、筆者には見つけられませんでした。
$ gcc ./imaginary.c -o imaginary
imaginary.c: In function 'main':
imaginary.c:6:12: error: expected identifier or '(' before '_Imaginary'
6 | double _Imaginary z = 3.0 * I;
| ^~~~~~~~~~
imaginary.c:8:29: error: 'z' undeclared (first use in this function)
8 | printf("%.1fi\n", cimag(z));
| ^
imaginary.c:8:29: note: each undeclared identifier is reported only once for each function it appears in
なんと、GCCもclangも _Imaginary
型をサポートしていないとの事です。
ユニコーンか?はたまたツチノコか!?ってなくらい、名実ともに"想像上の"型のようですね
このざんねんさは、だめぽ さんが書いて下さっている記事にて、すでに紹介されておりますので続きは↓↓コチラ↓↓へ。
マルチスレッド処理にまつわるシリーズ
お次は、マルチスレッド処理(一つのプログラムで複数の作業を同時に行う仕組み)に関連するものたちです。
_Atomic
、_Thread_local
_Atomic
_Atomic
は型修飾子や型指定子の形で、オブジェクトをアトミックと宣言するためのものです。
以下の2通りの使い方ができます。
_Atomic 型名 変数名 // 型修飾子
_Atomic(型名) 変数名 // 型指定子
アトミックとは、一連の操作が他の処理と干渉せず、一度に実行されることを保証する機能です。これにより、マルチスレッド環境でのデータ競合や不整合を防げます。
まずは、比較のため _Atomic
を使わずに、1つのカウンタを2つのスレッドで加算する例を見てみます。
#include <stdio.h>
#include <pthread.h>
int counter = 0;
void *thread_func(void* arg)
{
for (int i=0; i<1000000; i++)
{
counter++; // カウンタを加算(2つのスレッドで共有)
}
return NULL;
}
int main()
{
pthread_t thread1, thread2;
// 2つのスレッドを作成
pthread_create(&thread1, NULL, thread_func, NULL);
pthread_create(&thread2, NULL, thread_func, NULL);
// スレッドの終了を待つ
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
// 結果を表示
printf("counter: %d\n", counter);
return 0;
}
上記は2つのスレッドを作成し、それぞれのスレッドから counter
へアクセスして加算を行います。
1000000回のカウントアップを2つのスレッドから行っているため、理想は 2000000回が結果として表示されて欲しいですが、……実はそうはなりません。
$ gcc ./no_atomic.c -o no_atomic
$ ./no_atomic
counter: 1438044
上記結果より、予想より回数が減ってしまっていますね。
counter
を加算する際には、大きく「読み出し」→「加算」→「書き込み」の3ステップを踏みます。
例えば、2つのスレッドのタイミングによっては、同時に同じ値を読み出して加算する場合が発生します。この場合、2つのスレッドで2回分の加算を行いましたが、最終結果は1回分の加算となります。
このようなタイミングが何度も発生することで、意図した回数よりどんどん少ない結果になっていきます。
それもこれも、アトミックではない操作をしているからに他なりません。
これを解決する1つの方法として、_Atomic
が使えます。
counter
をアトミックとして宣言してみるとどうなるか見てみましょう。
#include <stdio.h>
#include <pthread.h>
#include <stdatomic.h>
_Atomic int counter = 0;
void *thread_func(void* arg)
{
for (int i=0; i<1000000; i++)
{
atomic_fetch_add(&counter, 1); // カウンタをアトミックにインクリメント
}
return NULL;
}
int main()
{
pthread_t thread1, thread2;
// 2つのスレッドを作成
pthread_create(&thread1, NULL, thread_func, NULL);
pthread_create(&thread2, NULL, thread_func, NULL);
// スレッドの終了を待つ
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
// 結果を表示
printf("counter: %d\n", counter);
return 0;
}
先ほどのサンプルコードから大きく2か所変えました。
_Atomic int counter = 0;
上記で counter
をアトミック宣言しました。
#include <stdatomic.h>
atomic_fetch_add(&counter, 1); // カウンタをアトミックにインクリメント
続いて加算を、専用のアトミックな加算操作に変えました。
結果がどうなるか見てみましょう。
$ gcc ./atomic.c -o atomic
$ ./atomic
counter: 2000000
はい。見事、加算における「読み出し」→「加算」→「書き込み」の3ステップの一貫性が保たれ、期待通りの結果となりましたね!
アトミック操作は、今回ご紹介した加算以外にもいくつかあります。他の例を少しだけ紹介しておきます。
演算関数名 | 内容・説明 | 用途・特徴 |
---|---|---|
atomic_fetch_add | アトミック変数の値に指定された値を加算し、元の値を返す。 | インクリメントや合計計算に使用。 |
atomic_fetch_sub | アトミック変数の値から指定された値を減算し、元の値を返す | デクリメントや減算操作に使用。 |
atomic_fetch_and | アトミック変数の値と指定された値のAND演算を行い、元の値を返す。 | ビットマスクの操作やフラグ管理に使用。 |
atomic_fetch_or | アトミック変数の値と指定された値のOR演算を行い、元の値を返す。 | ビット設定操作に使用。 |
_Thread_local
_Thread_local
は記憶域クラス指定子の1つで、その名の通り、スレッド毎のローカルなオブジェクトを確保するためのものです。
言い換えると、 _Thread_local
が指定された変数は、そのスレッドでのみ操作されるテンポラリ変数のような挙動となるイメージです。
先ほどの、_Atomic
の章で使ったサンプルの counter
に _Thread_local
を指定してその挙動を確認してみましょう。
#include <stdio.h>
#include <pthread.h>
_Thread_local int counter = 0;
void *thread_func(void* arg)
{
for (int i=0; i<1000000; i++)
{
counter++; // カウンタを加算(2つのスレッドで固有)
}
printf("thread_func counter : %d\n", counter);
return NULL;
}
int main()
{
pthread_t thread1, thread2;
// 2つのスレッドを作成
pthread_create(&thread1, NULL, thread_func, NULL);
pthread_create(&thread2, NULL, thread_func, NULL);
// スレッドの終了を待つ
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
// 結果を表示
printf("counter: %d\n", counter);
return 0;
}
変更箇所は大きく2つです。
_Thread_local int counter = 0;
counter
に _Thread_local
を付けました。
void *thread_func(void* arg)
{
for (int i=0; i<1000000; i++)
{
counter++; // カウンタを加算(2つのスレッドで固有)
}
printf("thread_func counter : %d\n", counter);
return NULL;
}
上記には printf
文を追加し、各スレッドの加算終了後に、その時のカウンタ値を表示させるようにしました。
$ gcc ./thread_local.c -o thread_local
$ ./thread_local
thread_func counter : 1000000
thread_func counter : 1000000
counter: 0
実行の結果、まず各スレッドで加算した counter
の結果は 1000000がそれぞれで見られます。
また、最終結果の counter
は 0と初期値のままです。ローカルで演算した結果は、元の値には何の影響も及ぼさないようですね。
_Thread_local
のメリットとしては、スレッド毎で固有に操作したいデータについては、管理を簡素化し、データ競合などを防ぐことが挙げられます。
アライメントにまつわるシリーズ
続いて、アライメント(データを効率よくメモリに配置するための境界揃えのルール)に関連するものたちです。
_Alignof
、_Alignas
_Alignof
_Alignof
はアライメント演算子とよばれ、引数の型が「何バイト境界に配置されなけらばならないか」の値を返します。戻り値の型は、型のバイト数を返す sizeof
と同じ size_t
型です。
実例を見てみましょう。
#include <stdio.h>
int main()
{
char a;
short b;
int c;
long d;
long long e;
float f;
double g;
long double h;
printf("char : %2zu byte alignment\n", _Alignof(a));
printf("short : %2zu byte alignment\n", _Alignof(b));
printf("int : %2zu byte alignment\n", _Alignof(c));
printf("long : %2zu byte alignment\n", _Alignof(d));
printf("long long : %2zu byte alignment\n", _Alignof(e));
printf("float : %2zu byte alignment\n", _Alignof(f));
printf("double : %2zu byte alignment\n", _Alignof(g));
printf("long double : %2zu byte alignment\n", _Alignof(h));
return 0;
}
$ gcc ./alignof.c -o alignof
$ ./alignof
char : 1 byte alignment
short : 2 byte alignment
int : 4 byte alignment
long : 4 byte alignment
long long : 8 byte alignment
float : 4 byte alignment
double : 8 byte alignment
long double : 16 byte alignment
実行結果から、各型をメモリ上に配置する際のアドレスの制約が分かります。
たとえば、 int
の結果は4バイトでした。これは、int
を正しく格納するためには、4で割り切れるアドレスに配置しなければならない事を意味しています。(もちろん、8なら8、16なら16で割り切れるアドレスへの配置が必要です)
書式指定子 %z
は、size_t
型の変数を表示する際に、適切なサイズで出力するために使用されます。
-
%zu
:size_t
型の変数を符号なしの 10 進数で表示します -
%zx
:size_t
型の変数を符号なしの 16 進数で表示します
_Alignas
_Alignas
はアライメント指定子と呼ばれ、指定された変数や型のメモリ上の配置を指定するために使用されます。
_Alignas
の構文は以下となっております。
_Alignas(alignment) 型 変数名;
alignment
には必要なアライメントを指定する整数または型名を指定します。
_Alignas
の効果を見るために、_Alignas
なしとありのそれぞれの実例を見てみましょう。
いずれも、各型の変数のアライメントと配置されたアドレスを標準出力するものです。
(アライメント指定子なし)
#include <stdio.h>
int main()
{
char a;
char b;
char c;
short d;
short e;
int f;
int g;
int h;
printf("a : alignment = %2zu, address = 0x%p\n", _Alignof(a), (void *)&a);
printf("b : alignment = %2zu, address = 0x%p\n", _Alignof(b), (void *)&b);
printf("c : alignment = %2zu, address = 0x%p\n", _Alignof(c), (void *)&c);
printf("d : alignment = %2zu, address = 0x%p\n", _Alignof(d), (void *)&d);
printf("e : alignment = %2zu, address = 0x%p\n", _Alignof(e), (void *)&e);
printf("f : alignment = %2zu, address = 0x%p\n", _Alignof(f), (void *)&f);
printf("g : alignment = %2zu, address = 0x%p\n", _Alignof(g), (void *)&g);
printf("h : alignment = %2zu, address = 0x%p\n", _Alignof(h), (void *)&h);
return 0;
}
$ gcc ./no_alignas.c -o no_alignas
$ ./no_alignas
a : alignment = 1, address = 0x0000001B899FF87F
b : alignment = 1, address = 0x0000001B899FF87E
c : alignment = 1, address = 0x0000001B899FF87D
d : alignment = 2, address = 0x0000001B899FF87A
e : alignment = 2, address = 0x0000001B899FF878
f : alignment = 4, address = 0x0000001B899FF874
g : alignment = 4, address = 0x0000001B899FF870
h : alignment = 4, address = 0x0000001B899FF86C
(アライメント指定子あり)
#include <stdio.h>
int main()
{
char a;
char b;
char c;
_Alignas(int) short d;
short e;
int f;
_Alignas(8) int g;
_Alignas(16) int h;
printf("a : alignment = %2zu, address = 0x%p\n", _Alignof(a), (void *)&a);
printf("b : alignment = %2zu, address = 0x%p\n", _Alignof(b), (void *)&b);
printf("c : alignment = %2zu, address = 0x%p\n", _Alignof(c), (void *)&c);
printf("d : alignment = %2zu, address = 0x%p\n", _Alignof(d), (void *)&d);
printf("e : alignment = %2zu, address = 0x%p\n", _Alignof(e), (void *)&e);
printf("f : alignment = %2zu, address = 0x%p\n", _Alignof(f), (void *)&f);
printf("g : alignment = %2zu, address = 0x%p\n", _Alignof(g), (void *)&g);
printf("h : alignment = %2zu, address = 0x%p\n", _Alignof(h), (void *)&h);
return 0;
}
$ gcc ./alignas.c -o alignas
$ ./alignas
a : alignment = 1, address = 0x000000B744DFF82F
b : alignment = 1, address = 0x000000B744DFF82E
c : alignment = 1, address = 0x000000B744DFF82D
d : alignment = 4, address = 0x000000B744DFF828
e : alignment = 2, address = 0x000000B744DFF826
f : alignment = 4, address = 0x000000B744DFF820
g : alignment = 8, address = 0x000000B744DFF818
h : alignment = 16, address = 0x000000B744DFF810
実行結果の表示だけを見て、ピンとくるのは難しそうですね…。この様子を以下に図示します。
配置されるアドレス自体はプログラム実行の度に変わるのであまり気にしなくてOKです。(下2桁だけを番地として表示しています)
_Alignas
なしの場合は、各型の境界の制約を守りつつ、なるべく詰めて配置されています。
一方、_Alignas
を指定したもの(赤枠)は、指定のバイトで割り切れる番地に移動している事が分かります。(指定されていないものは、先と同様に制約を守りつつ、なるべく詰めています)
なお、この _Allignas
を使って配置を指定する事により得られる、うれしい効果としては以下が挙げられます。
- 異なるプラットフォームやコンパイラ間でのコードの一貫性と正確性を維持できる
- メモリアクセス効率の向上やキャッシュ効率の最適化により、パフォーマンスの向上が期待できる
その他
最後は、どうにも分類できなかった、奇抜な!?ものたちです。
_Noreturn
、_Static_assert
、_Generic
_Noreturn
_Noreturn
(関数指定子)は、添えることでその関数が呼び出された場合に、「そのまま制御が戻らない」という事を明示するためのものです。
どんなうれしさが?というと、主に以下が挙げられます。
- 警告を抑制すること
- コードの最適化を促すこと
関数が戻ることを想定しているのに実際のコードは戻らない、って場合は非常にマズいです。なので、戻らないと分かってるものはあらかじめ明示しておき、残りは警告で防ぎたいモチベーションがあるのかなと思います。(もちろん、100%警告できるわけではないですが)
_Noreturn
の有無で警告がどう変わるかを見てみましょう。(コードの最適化については今回は割愛します)
サンプルとして、abort
を呼ぶことでもう戻らない関数 goodby
を用意しました。
まずはこの関数に、_Noreturn
をつけなかった場合です。
#include <stdio.h>
#include <stdlib.h>
void goodbye()
{
printf("good-bye here.\n");
abort();
printf("hello again. (but...)\n");
}
int main()
{
goodbye();
printf("never come back here.\n");
return 0;
}
$ gcc ./no_noreturn.c -o no_noreturn -Wmissing-noreturn
no_noreturn.c: In function 'goodbye':
no_noreturn.c:4:6: warning: function might be candidate for attribute 'noreturn' [-Wsuggest-attribute=noreturn]
4 | void goodbye()
| ^~~~~~~
$ ./no_noreturn
good-bye here.
あらかじめ _Noreturn
が必要となりそうな箇所を警告するため -Wmissing-noreturn
オプションを有効にしています。
案の定、もう戻ってこない goodbye
関数がコンパイラに怒られましたね。
次に、先ほどのコードの goodbye
関数に _Noreturn
を付けた結果を示します。
#include <stdio.h>
#include <stdlib.h>
_Noreturn void goodbye()
{
printf("good-bye here.\n");
abort();
printf("hello again. (but...)\n");
}
int main()
{
goodbye();
printf("never come back here.\n");
return 0;
}
$ gcc ./noreturn.c -o noreturn -Wmissing-noreturn
$ ./noreturn
good-bye here.
_Noreturn
であることを明示する事で、無事に警告が消えました。
ちなみに、Noreturn
を付けた関数に return
文がある場合にも警告が出ます。
#include <stdio.h>
#include <stdlib.h>
_Noreturn void goodbye()
{
printf("good-bye here.\n");
abort();
printf("hello again. (but...)\n");
return;
}
int main()
{
goodbye();
printf("never come back here.\n");
return 0;
}
$ gcc ./noreturn.c -o noreturn
return.c: In function 'goodbye':
return.c:12:5: warning: function declared 'noreturn' has a 'return' statement
12 | return;
| ^~~~~~
$ ./noreturn
abort here.
戻らないはずの関数に return
入っちゃってますよ!どっちなんだい?と教えてくれていますね。
実はこの _Noreturn
も、先にご紹介したC言語のざんねんなしよう事典にとりあげらていますので、参照してみて下さい。(ざんねんなところがあります)
_Static_assert
_Static_assert
はコンパイル時の静的アサーション(コンパイル時チェック)を行うための宣言です。
他のチェック機構に、assert
や #error
がありますが、それぞれの違いもあわせて簡単に示します。
チェック機構 | 実行タイミング | 主な用途 | 特徴 |
---|---|---|---|
#error 指令 |
プリプロセッサ | プリプロセッサ時のエラーチェック | コンパイルを停止し、メッセージを表示 |
_Static_assert 宣言 |
コンパイル時 | 定数式のコンパイル時チェック | 実行時オーバーヘッドがなく、条件が満たされないとコンパイルエラー |
assert マクロ |
実行時 | 実行時の条件チェック | デバッグ用で条件が満たされないとアボート、NDEBUG を定義することで無効化可能 |
_Static_assert
は、例えば以下のように「int
型が 2バイトより大きいことを確かめる」など、コードの特定の条件が満たされていることの確認に使われるようです。
#include <stdio.h>
_Static_assert(sizeof(int) > 2, "16bit code not supported.");
int main()
{
printf("OK.\n");
return 0;
}
$ gcc ./static_assert.c -o static_assert
$ ./static_assert
OK.
筆者の環境では、OKとなりましたが、例えば int
が2バイトの環境では以下のエラーが出力されるはずです。
$ gcc ./static_assert.c -o static_assert
static_assert.c:3:1: error: static assertion failed: "16bit code not supported."
3 | _Static_assert(sizeof(int) > 2, "16bit code not supported.");
| ^~~~~~~~~~~~~~
その他、以下の用途での使用も考えられます。
- 配列サイズのチェック
- プラットフォームに依存するコードのチェック
_Generic
いよいよラストです。筆者的にはこれが一番の「ハトが豆鉄砲!」なキーワードでした。
_Generic
とは型総称マクロと呼ばれるもので、コンパイル時に型を判定し、指定された型に基づいて異なる式を選択する「ジェネリック選択」を実現するものです。
型に依存しない「一般的な」処理をする、という意味を込めて「ジェネリック」なのだそうです。(決して、お安いとかではなかったみたいですね)
……とはいえイメージが難しいので、さっさと具体例を見てみましょう。
#include <stdio.h>
#define type_to_string(x) _Generic((x), \
int: "int", \
double: "double", \
char *: "string", \
default: "unknown")
int main()
{
int a = 1;
double b = 3.14;
char *c = "generic";
float d = 5.69;
printf("type of a is \"%s\"\n", type_to_string(a));
printf("type of b is \"%s\"\n", type_to_string(b));
printf("type of c is \"%s\"\n", type_to_string(c));
printf("type of d is \"%s\"\n", type_to_string(d));
return 0;
}
マクロ定義のところで _Generic
が使われています。
まず、サンプルコードが何をしようとしているかというと、type_to_string
マクロの引数 x
の「型」が何なのかに応じて、返す文字列を切り替えて表示させようとしています。
$ gcc ./generic.c -o generic
$ ./generic
type of a is "int"
type of b is "double"
type of c is "string"
type of d is "unknown"
実行結果より、type_to_string(a)
は引数の a
が int
型なので、対応する文字列 "int"
が返されています。
また、type_to_string(d)
は引数の d
が float
型で合致するものがありません。その場合は、default
に対応する文字列 "unknown"
が返されています。
ここで、分かりやすさのために語弊を恐れず言うと、_Generic
は、型名で分岐する switch
文のようなものです。
_Generic
の構文は以下になります。
_Generic( `expression`, `type1`: `selection1`, `type2`: `selection2`, ..., `default`: `default_selection` )
コンパイルは通りませんが、type_to_string
マクロを switch
文を使った関数に書き直したイメージを、以下に示します。(引数 x
に型名を与えられないので正しくはないですが)
char* type_to_string(type x)
{
switch (x)
{
case int:
return "int";
case double:
return "double";
case string:
return "string";
default:
return "unknown";
}
return NULL;
}
この場合、_Generic
の構文と switc
分の対応付けは以下のようになります。
_Generic の構文 |
switch 文 |
---|---|
expression |
x |
type |
case 文の各条件 |
selection |
case 文の各処理 |
default |
default 文 |
default_selection |
default 文の処理 |
シンタクスハイライトが、一部ダメになっちゃってますね。うまくイメージが伝わるといいのですが…。
この _Generic
の機能の主な用途には以下があります。
- 型に基づく操作の最適化
- 関数オーバーロードの代用
以上を持ちまして、10個のアンダースコアではじまるキーワードの紹介は終わりです。
まとめ
あまり馴染みがないものもあったかと思いますが、いかがでしたでしょうか。簡単にでも触れてみると、「なんだ、どうってこと無かったな…」って気もしますね。
最後に、本記事で紹介したキーワードについて、別途標準ライブラリのヘッダをインクルードすることにより、アンダースコア _
なしの定義が使えるものについて、一覧に残しておきます。
キーワード | 標準ヘッダ | 代替マクロ定義 |
---|---|---|
_Bool |
stdbool.h |
bool |
_Complex |
complex.h |
complex |
_Imaginary |
complex.h |
imaginary
|
_Atomic |
なし | なし |
_Thread_local |
threads.h |
thread_local |
_Alignof |
stdalign.h |
alignof |
_Alignas |
stdalign.h |
alignas |
_Noreturn |
stdnoreturn.h |
noreturn |
_Static_assert |
assert.h |
static_assert |
_Generic |
なし | なし |
互換性に問題がない場合は、やはりなるべくアンダースコアのない方がコードの可読性にとっては良さそうですね。
実は、C言語の"最新"の標準であるC23におきましては、本記事掲載の10個以外にも、アンダースコアではじまるキーワードが、いくつか追加されております。
_Decimal32
、_Decimal64
、_Decimal128
、_BitInt
……が、本記事では紙面の都合で割愛させていただきました。(キリが悪そうだったもので、すいませーん🙇♂️)
ここまで、お読みいただきありがとうございました。大変お疲れ様でした!!
おわりに
筆者なりに「実例」に触れながら、10個のキーワードをカミ砕いてみました。何かのお役に立てられましたら幸いです。
関連記事
C言語について、他の記事も書いております。良かったら読んでみてください。
- これは知らなかった!これはけしからん!という筆者が選んだトピックをいくつかご紹介しております
- C言語を書く上で絶対やってはいけない「未定義動作」をいくつかご紹介しております