2
1

【C言語】アンダースコアではじまる10の予約語

Last updated at Posted at 2024-09-08

はじめに

どうも、y-tetsuです。

皆さん、C言語の学習お疲れ様です!筆者ものんびりですが、日々励んでおります。

最近は、本記事のタイトルにもしているアンダースコアではじまる予約語というものを学びました。

「アンダースコアではじまる予約語!?なんだそれ??」

……ってリアクションをいただけた方、大変ありがとうございます!とてもいい人!!

実のところ、調べてみるまでは筆者も「ハトが豆鉄砲を食ったような顔」してました…。ですので、これからはちょっとでも「知ってるよ」と言えるよう、備忘録を残したいと思います。

是非ぜひ、皆さんも読んでいってください。

予約語について

まず最初に、一口にC言語で予約語といっても、実はいろいろな種類が含まれています。

大まかに、予約語の種類を以下に示します。

種類 役割
キーワード 構文や文法で特別な意味を持つ単語 intreturnifwhile など
プリプロセッサディレクティブ コンパイル前にプリプロセッサが処理するコマンド #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_Imaginaryinlinerestrict
C11 _Alignas_Alignof_Atomic_Generic_Noreturn_Static_assert_Thread_local

という事で、ほとんどの新しいキーワードには _ が先頭に付いていました。バッティングする恐れがないので、C規格のバージョンアップ後も既存のコードはすんなり動くはずですよね。

理由は定かではないのですが、inlinerestrict だけは特別のようです。

ChatGPT(GPT-4o)は以下のように言っておりました。

inlinerestrictのようなアンダースコアの付いていないキーワードは、C言語の標準ライブラリや多くのコードベースで頻繁に使われることが想定されるため、アンダースコアなしの形が選ばれました。これらのキーワードは、高速化や最適化を目的として使われるため、コードの読みやすさと簡潔さが重要視されます。

さらに、これらのキーワードがC99で追加される以前、inlineは多くのコンパイラで既に拡張としてサポートされていたため、既存の使用法と整合性を保つためにアンダースコアなしのキーワードとして標準に組み込まれました。

なるほど!と思いましたが、誤っておりましたら申し訳ございません。

アンダースコアではじまるキーワードたち

さて、準備万端となりました。

いよいよ、アンダースコアではじまる10個のキーワードたちを見ていきましょう。

本記事の実行結果は、GCC 14.1.0のものとなっております。

型にまつわるシリーズ

まずは、取っつきやすいものから。

いずれも intdouble の同類となる、変数の「型」を指定するものたちです。

_Bool_Complex_Imaginary

_Bool

_Bool は、符号なしの整数型で、真(1)か偽(0)の2つの値をとる真理値を表すデータ型です。

bool.c
#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++と同様の、 booltruefalseが使えるようになります。

stdbool.c
#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 や、実部や虚部を取得する関数(crealcimag)などを扱うために、complex.h のインクルードがほぼ必須となりそうです。

使用例を以下に示します。

complex.c
#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;
}

複素数 z1z2 を加算して 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

プログラムの実行結果は、下図のようになります。

complex.png

複素数型には以下の3つがあります。

  • float _Complex
  • double _Complex
  • long double _Complex

単独の _Complexdouble _Complex と解釈されるとのこと。内部的には、実部と虚部の2要素の配列で実現されているそうです。

_Imaginary

_Imaginary は純虚数型とよばれ、先ほどの複素数型の虚数部分のみを表すためのデータ型です。

サンプルコードを示します。

imaginary.c
#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 型をサポートしていないとの事です。

ユニコーンか?はたまたツチノコか!?ってなくらい、名実ともに"想像上の"型のようですね:unicorn:

このざんねんさは、だめぽ さんが書いて下さっている記事にて、すでに紹介されておりますので続きは↓↓コチラ↓↓へ。

マルチスレッド処理にまつわるシリーズ

お次は、マルチスレッド処理(一つのプログラムで複数の作業を同時に行う仕組み)に関連するものたちです。

_Atomic_Thread_local

_Atomic

_Atomic は型修飾子や型指定子の形で、オブジェクトをアトミックと宣言するためのものです。

以下の2通りの使い方ができます。

_Atomic 型名 変数名  // 型修飾子
_Atomic(型名) 変数名  // 型指定子

アトミックとは、一連の操作が他の処理と干渉せず、一度に実行されることを保証する機能です。これにより、マルチスレッド環境でのデータ競合や不整合を防げます。

まずは、比較のため _Atomic を使わずに、1つのカウンタを2つのスレッドで加算する例を見てみます。

no_atmic.c
#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 をアトミックとして宣言してみるとどうなるか見てみましょう。

atmic.c
#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 を指定してその挙動を確認してみましょう。

thread_local.c
#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 型です。

実例を見てみましょう。

alignof.c
#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 なしとありのそれぞれの実例を見てみましょう。

いずれも、各型の変数のアライメントと配置されたアドレスを標準出力するものです。

(アライメント指定子なし)

no_alignas.c
#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

(アライメント指定子あり)

alignas.c
#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

実行結果の表示だけを見て、ピンとくるのは難しそうですね…。この様子を以下に図示します。

alignas.png

配置されるアドレス自体はプログラム実行の度に変わるのであまり気にしなくてOKです。(下2桁だけを番地として表示しています)

_Alignas なしの場合は、各型の境界の制約を守りつつ、なるべく詰めて配置されています。

一方、_Alignas を指定したもの(赤枠)は、指定のバイトで割り切れる番地に移動している事が分かります。(指定されていないものは、先と同様に制約を守りつつ、なるべく詰めています)

なお、この _Allignas を使って配置を指定する事により得られる、うれしい効果としては以下が挙げられます。

  • 異なるプラットフォームやコンパイラ間でのコードの一貫性と正確性を維持できる
  • メモリアクセス効率の向上やキャッシュ効率の最適化により、パフォーマンスの向上が期待できる

その他

最後は、どうにも分類できなかった、奇抜な!?ものたちです。

_Noreturn_Static_assert_Generic

_Noreturn

_Noreturn (関数指定子)は、添えることでその関数が呼び出された場合に、「そのまま制御が戻らない」という事を明示するためのものです。

どんなうれしさが?というと、主に以下が挙げられます。

  • 警告を抑制すること
  • コードの最適化を促すこと

関数が戻ることを想定しているのに実際のコードは戻らない、って場合は非常にマズいです。なので、戻らないと分かってるものはあらかじめ明示しておき、残りは警告で防ぎたいモチベーションがあるのかなと思います。(もちろん、100%警告できるわけではないですが)

_Noreturn の有無で警告がどう変わるかを見てみましょう。(コードの最適化については今回は割愛します)

サンプルとして、abort を呼ぶことでもう戻らない関数 goodby を用意しました。

まずはこの関数に、_Noreturn をつけなかった場合です。

no_noreturn.c
#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 を付けた結果を示します。

noreturn.c
#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 文がある場合にも警告が出ます。

return.c
#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バイトより大きいことを確かめる」など、コードの特定の条件が満たされていることの確認に使われるようです。

static_assert.c
#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 とは型総称マクロと呼ばれるもので、コンパイル時に型を判定し、指定された型に基づいて異なる式を選択する「ジェネリック選択」を実現するものです。

型に依存しない「一般的な」処理をする、という意味を込めて「ジェネリック」なのだそうです。(決して、お安いとかではなかったみたいですね)

……とはいえイメージが難しいので、さっさと具体例を見てみましょう。

generic.c
#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:unicorn:
_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個のキーワードをカミ砕いてみました。何かのお役に立てられましたら幸いです。

ganen.png

関連記事

C言語について、他の記事も書いております。良かったら読んでみてください。

  • これは知らなかった!これはけしからん!という筆者が選んだトピックをいくつかご紹介しております

  • C言語を書く上で絶対やってはいけない「未定義動作」をいくつかご紹介しております
2
1
0

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
2
1