C言語/データ型と変数の高度な話題

ここでは、ISO/IEC 9899:2023(通称 C23)の 6.7 Declarations で定義されている構文と意味を読み解き、データ型と変数への理解を深めようと思います。

宣言

編集

N3096 working draft — April 1, 2023 ISO/IEC 9899:2023 (E) §6.7 Declarations:宣言[1].

構文
declaration ::= 
    declaration-specifiers init-declarator-list? ';' |
    attribute-specifier-sequence declaration-specifiers init-declarator-list? ';' |
    static_assert-declaration |
    attribute-declaration

declaration-specifiers ::=
    declaration-specifier attribute-specifier-sequence? |
    declaration-specifier declaration-specifiers

declaration-specifier ::=
    storage-class-specifier |
    type-specifier-qualifier |
    function-specifier

init-declarator-list ::= 
    init-declarator |
    init-declarator-list ',' init-declarator

init-declarator ::=
    declarator |
    declarator '=' initializer

attribute-declaration ::=
    attribute-specifier-sequence ';'

記憶域クラス指定子

編集

記憶域クラス指定子( storage-class specifiers )は7種類あり、いずれもキーワードです[2]

構文
storage-class-specifier ::= "auto"
                          | "constexpr"
                          | "extern"
                          | "register"
                          | "static"
                          | "thread_local"
                          | "typedef"

記憶域クラス指定子(storage-class specifiers)は、変数や関数の宣言時に、以下の性質を指定するために使用されます。

  1. 記憶域の持続期間 (storage duration)
    auto
    自動記憶域期間 (ブロックスコープ内で有効)
    static
    スタティック記憶域期間 (プログラム実行時から終了までの期間)
    thread_local
    スレッドローカル記憶域期間 (スレッド内で有効)
    register
    自動記憶域期間で、レジスタ実装を提案(実装依存)
  2. リンケージ (linkage)
    extern
    外部リンケージを持つ
    static
    内部リンケージを持つ (同一翻訳単位内でのみ参照可能)
  3. 値の固定化
    constexpr
    定数式として値を固定する
  4. 型定義
    typedef
    型の別名を定義する

主な制約は以下の通りです。

  • 1つの宣言で記憶域クラス指定子は1つしか使用できない(例外あり)
  • thread_localはstaticまたはexternと共に使う
  • auto は typedef 以外の指定子と共に使える
  • constexprはauto、register、staticと共に使える
  • 関数宣言ではthread_localは使えない
  • 記憶域クラス指定子の性質は構造体・共用体メンバにも適用される

constexprで宣言された変数は、定数式として使え、その値は翻訳時に固定される。制約として、アトミック型、可変長型、volatile、restrictな型は使えず、初期化子は定数式や文字列リテラルでなければならない。

autoはCでは主に次の2つの用途があります。

  1. ブロックスコープ変数の記憶域クラスの指定
    int main() {
        auto int x = 0; // auto は省略可能
        {
            auto int y = 1; // ブロックスコープ変数
        }
        // yは到達範囲外
        return 0;
    }
    
  2. * ブロックスコープ内で宣言された変数には、デフォルトでauto記憶域クラスが割り当てられます。
  3. * auto変数は、そのブロックを抜けると寿命が尽きます。
  4. * 基本的にautoキーワードは省略可能です。
  5. 変数宣言における型推論
    auto x = 0; // xはint型
    auto y = 1.0; // yはdouble型
    auto z = &y; // zはdouble*型
    
  6. * 型の位置に書かれたautoに初期化式を与えると、その型が推論されます。
  7. * 配列型や関数ポインタ型は推論されず、エラーになります。
  8. * autoを使うと、移植性の高いコードが書けます。

C23から導入されたautoの型推論機能は、冗長なマクロ構文を避け、コードの簡潔さと可読性を高める役割があります。適切に使うと生産性が上がります。

static

編集

staticはCにおいて、記憶域の持続期間とリンケージを指定するキーワードです。

  1. 記憶域の持続期間
    • static変数はスタティック記憶域期間を持ち、プログラムの実行時から終了までその領域が確保されます。
    • static変数は、その変数が宣言されたブロックを抜けても値が保持されます。
    • グローバル変数と同様の振る舞いをしますが、スコープがそのソースファイル内部に制限されます。
    void func() {
        static int count = 0; // 初期化は1回のみ実行される
        count++;
        printf("count = %d\n", count); // 1, 2, 3, ...と増えていく
    }
    
    int main() {
        func(); // 出力: count = 1
        func(); // 出力: count = 2
        func(); // 出力: count = 3
        return 0;
    }
    
  2. リンケージ
    • ファイルスコープでstaticを付けると、その変数や関数に内部リンケージが与えられます。
    • 内部リンケージの識別子は、そのソースファイル内でのみ参照できます。
    // foo.c
    static int x = 1; // 内部リンケージ
    
    int main() {
        return 0;
    }
    
  1. // bar.c
    extern int x; // エラー、xは他のファイルから参照できない
    
    int main() {
        return 0;
    }
    
  2. static関数
    • staticを関数に付けると、その関数に内部リンケージが与えられます。
    • 内部リンケージの関数は同じソースファイル内でのみ参照できます。
    // util.c
    static int private_func() { // 内部リンケージ
        return 42;
    }
    
    int public_func() {
        return private_func(); // OKこのファイル内なら呼べる  
    }
    

staticを適切に使うことで、グローバル変数の名前空間の汚染を防ぎ、変数やデータの有効範囲を制限できます。また、内部に隠蔽したい関数に内部リンケージを与えることで、モジュール性の高いコードを書くことができます。

thread_local

編集

thread_localはC11から導入された記憶域クラス指定子で、スレッドローカル記憶域期間を持つオブジェクトを宣言するために使用します。

スレッドローカル記憶域期間を持つオブジェクトは、各スレッドごとに別々のインスタンスが作られ、他のスレッドからはアクセスできません。つまり、スレッドセーフな変数を実現できます。

使用例
#include <threads.h>
#include <stdio.h>

thread_local int value = 0; // スレッドローカル変数

int increment_value(void* data) {
    value++; // 各スレッドごとに別々のvalueが使用される
    return 0;
}

int main() {
    thrd_t threads[4];
    
    for (int i = 0; i < 4; i++) {
        thrd_create(&threads[i], increment_value, NULL);
    }
    
    for (int i = 0; i < 4; i++) {
        thrd_join(threads[i], NULL);
    }
    
    printf("Final value: %d\n", value); // 値は1(各スレッドの値は別イン スタンス)
    return 0;
}

上記の例では、valueがスレッドローカル変数として宣言されています。increment_value関数内でvalueがインクリメントされますが、それぞれのスレッドで別々のインスタンスが使用されるため、最終的なvalueの値は1になります。

スレッドローカル変数を使うメリットは以下の通りです:

  • データ競合が発生しないので、ミューテックスなどの排他制御が不要
  • スレッド間でデータをコピーする必要がないので、効率的
  • グローバル変数を使うよりスレッドセーフ

一方で、デメリットとしては:

  • スレッドローカル変数はスレッド終了時に自動的に破棄されるため、動的に確保したメモリ領域などは明示的に解放する必要がある
  • 過剰に使うと、メモリ使用量が大きくなる可能性がある

thread_localはスレッド間で変数を共有したくない場合に便利です。しかし、グローバル変数を乱用するよりはマシではあるものの、過剰な使用は避けた方が良いでしょう。

register

編集

registerはCにおける記憶域クラス指定子の1つです。コンパイラにレジスタ割り当てを提案する役割があります。

register int x;

register変数の特徴は以下の通りです:

  • コンパイラに対して、その変数をCPUレジスタに割り当ててパフォーマンスを改善することを示唆します。
  • ただし、実際にレジスタに割り当てるかはコンパイラ次第です。
  • レジスタ割り当ての際、アドレス演算子&を使うことはできません。
  • 静的記憶域期間を持つことはできません。つまりグローバル変数やスタティック変数には指定できません。
  • 配列型の変数に対してregisterを指定することはできません。

レジスタに格納された変数にはメモリアクセスが発生しないので、高速にアクセスできます。しかし現代のコンパイラは高度な最適化を行うため、registerの効果は限定的で、あまり利用されなくなってきました。

使用例:

int func(int a, int b) {
    register int result = a + b; // resultをレジスタに割り当てることを示唆
    // resultに何らかの計算を行う
    return result;
}

このような小さな関数内のスコープ変数に対してregisterを指定すると、レジスタ割り当てによる高速化が期待できます。

しかし、registerの利点は限定的で、現代の最適化コンパイラではその効果は不確実です。むしろregisterを濫用すると、コンパイラがレジスタ割り当ての最適化を阻害する可能性があります。そのため、現代のCプログラミングではregisterはほとんど利用されていません。

register変数の利用は避け、コンパイラの最適化能力に委ねる方が無難です。レジスタ割り当ての最適化は、コンパイラの進化によって自動的に行われるようになっています。

extern

編集

externはCにおいて、変数や関数の外部リンケージを指定するためのキーワードです。外部リンケージとは、その識別子が翻訳単位(ソースファイル)を越えて参照できることを意味します。

externの主な使い方は以下の2つです。

  1. 関数の外部リンケージの指定
    関数を別のソースファイルから利用できるようにするには、externキーワードを使って外部リンケージを指定する必要があります。
    foo.c
    extern int bar(int x);  // barの外部リンケージを指定
    
    int main() {
        int y = bar(3);  // barを呼び出せる
        return 0;
    }
    
    bar.c
    int bar(int x) {
        return x * 2;
    }
    
  2. グローバル変数の外部リンケージの指定
    外部リンケージを持つグローバル変数は、プログラム内の他の翻訳単位からアクセスできます。
    foo.c
     
    extern int global_var;  // external linkage
    
    int main() {
        global_var = 42;  // OK
        return 0;
    }
    
    bar.c
    int global_var = 0;  // 定義と初期化
    
    externの宣言は、外部で定義された変数や関数を参照するためだけに使われ、実際の定義や初期化は行いません。一方、externなしで変数や関数を宣言すると、その翻訳単位内で定義されることを意味します。
    foo.c
    int bar(int x);  // bar関数の定義を翻訳単位内で探す
    
    int x;  // xはこの翻訳単位内で定義される
    

externキーワードは、複数のソースファイルから成るプログラムにおいて、変数や関数を適切にリンクするために欠かせない構文です。外部リンケージを適切に制御することで、名前空間の衝突を防ぎ、モジュール性の高いコードを書くことができます。


異なるスコープで宣言された識別子や、同じスコープで2回以上宣言された識別子は、結合(Linkages[3])と呼ばれるプロセスによって、同じオブジェクトや関数を参照するようにすることができます[4]

結合には、外部結合( external linkage )、内部結合( internal linkage )、および無結合( none linkage )の3種類があります。

プログラム全体を構成する翻訳単位やライブラリの集合の中では、外部結合された特定の識別子の宣言は、それぞれ同じオブジェクトや関数を意味します。

1つの翻訳単位( translation-unit )の中で、内部結合を持つ識別子の各宣言は、同じオブジェクトまたは関数を表します。無結合な識別子の各宣言は一意の実体を表します。

オブジェクトまたは関数のファイルスコープ識別子の宣言に、記憶域クラス指定子 static が含まれている場合、その識別子は内部リンクを持ちます。

識別子の事前宣言が可視化されているスコープにおいて、記憶域クラス指定子 extern で宣言された識別子について、事前宣言が内部または外部結合を指定している場合、後の宣言における識別子の結合は、事前宣言で指定された結合と同じである。先行宣言が見えない場合、または先行宣言が結合を指定していない場合、その識別子は外部結合を持つ。

関数の識別子の宣言に記憶域クラス指定子がない場合、その結合は、記憶域クラス指定子 extern で宣言された場合と全く同じように決定されます。 オブジェクトの識別子の宣言にファイルスコープがあり、記憶域クラス指定子がない場合、そのリンク先は外部となります。

オブジェクトまたは関数以外と宣言された識別子、関数のパラメータと宣言された識別子、記憶域クラス指定子 extern を持たずに宣言されたオブジェクトのブロックスコープ識別子は、結合されません。

1つの翻訳単位の中で、同じ識別子が内部リンクと外部リンクの両方で現れた場合、その動作は未定義です。


関数宣言に記憶域クラス指定子 static を含めることができるのは、ファイルスコープにある場合のみです[5]

constexpr

編集

constexprは、C23で導入された記憶域クラス指定子の一種です。これは、スカラーオブジェクトを定数として宣言するために使用されます。スカラーオブジェクトは、宣言時に完全かつ明示的に初期化されなければなりません。静的初期化のルールに従います。それでも、宣言に適切なリンケージを持ち、ランタイムでアドレスを取得できます。ただし、ランタイムでのいかなる方法でも変更できません。つまり、コンパイラはオブジェクトの固定値に関する知識を他の定数式で使用できます。

また、このような定数の初期化子に使用される定数式は、コンパイル時にチェックされます。

浮動小数点型の初期化子は、翻訳時の浮動小数点環境で評価されなければなりません。

constexprで宣言できるオブジェクトの型にはいくつかの制限があります。具体的には、以下の構造はconstexprとして宣言することができません。

  • ポインター(ただし、nullポインターはconstexprであることができます)
  • 可変修飾型
  • アトミック型
  • volatile型
  • restrictポインター

typedef

編集

typedefは新しい型の別名(エイリアス)を定義するための構文です。既存の型に新しい名前を付けることができ、可読性の向上や型の抽象化に役立ちます。

構文は以下の通りです:

typedef 既存の型 新しい型名;

使用例:

  1. 構造体型の別名
    typedef struct {
        int x;
        int y;
    } Point;
    
    int main() {
        Point p1 = {1, 2}; // structキーワードなしで構造体を宣言できる
        return 0:
    }
    
  2. ポインタ型の別名
    typedef int* IntPtr;
    
    int main() {
        int x = 10;
        IntPtr ptr = &x; // int* ptr = &x; と同じ
        return 0;
    }
    
  3. 型修飾子付きの型の別名
    typedef unsigned int WORD;
    typedef const char* StringPtr;
    
  4. 関数ポインタ型の別名
    typedef int (*FuncPtr)(int, int); // 関数ポインタ型の別名
    
    int add(int a, int b) {
        return a + b;
    }
    
    int main() {
        FuncPtr fptr = &add; // int (*fptr)(int, int) = &add; と同じ
        int res = fptr(3, 4); // add関数を呼び出す
        return 0;
    }
    

typedefを使うメリットは以下の通りです:

  • 複雑な型を簡潔に表現できる
  • コードの可読性が向上する
  • 型のセマンティクスを表現できる (e.g. UserID, HostNameなど)
  • 移植性が高まる (コンパイラごとに基本型サイズが変わるため)

一方で、typedefの乱用は可読性を下げる可能性があるため、適切に利用することが重要です。型の別名を定義する際は、その型の意味や用途が明確になるような分かりやすい名前をつけることをお勧めします。

型指定子

編集

N3096 working draft — April 1, 2023 ISO/IEC 9899:2023 (E) §6.7.2 Type specifiers:型指定子[6]

構文
type-specifier ::= "void"
                    | "char"
                    | "short"
                    | "int"
                    | "long"
                    | "float"
                    | "double"
                    | "signed"
                    | "unsigned"
                    | "_BitInt" "(" constant-expression ")"
                    | "bool"
                    | "_Complex"
                    | "_Decimal32"
                    | "_Decimal64"
                    | "_Decimal128"
                    | atomic-type-specifier
                    | struct-or-union-specifier
                    | enum-specifier
                    | typedef-name
                    | typeof-specifier

型指定子(Type specifiers)は、C言語において変数や関数の型を指定するためのキーワードです。型指定子には以下のようなものがあります。

  • 基本型
    • void
    • 整数型(char, short, int, long, long long)
    • 浮動小数点型(float, double, long double)
    • ビット精度型(_BitInt)
    • 複素数型(_Complex)
    • 論理型(bool)
    • 小数(decimal)浮動小数点型(_Decimal32, _Decimal64, _Decimal128)
  • 派生型
    • 構造体(struct)
    • 共用体(union)
    • 列挙型(enum)
    • 型定義(typedef)

型指定子の主な制約は以下の通りです:

  • 宣言時には少なくとも1つの型指定子が必要(型推論の場合を除く)
  • 複素数型や小数浮動小数点型は、実装がサポートしている場合にのみ使用可能
  • ビット精度整数型(_BitInt)では、ビット幅を指定する整数定数式が必要

型指定子を使って、変数や関数の型を明示的に指定することができます。

int x;          // int型
double y;       // double型 
struct Point {
    int x, y;
};               // 構造体型
typedef int INTEGER; // INTEGER は int型の別名

適切な型指定を行うことで、プログラムの振る舞いを明確にし、誤った演算を防ぐことができます。また、型情報を基に最適化が行われるので、パフォーマンスにも影響します。

型指定子を使いこなすことは、C言語を学ぶ上で非常に重要です。利用可能な型と、それぞれの特性を理解しておくことをお勧めします。


固定幅の整数型

編集

C99では、プログラムの移植性を高めるために、いくつかの新しい整数型が定義されています[7]。 すでに利用可能な基本的な整数型では、実際のサイズが実装によって定義され。システムによって異なる可能性があるため、(移植性を確保する上で)不十分であると考えられました。 新しい型は、ハードウェアが通常いくつかの型しかサポートしておらず、そのサポートが環境によって異なる組み込み環境において特に有用です。すべての新しい型は、<inttypes.h> ヘッダー(及び、<stdint.h> ヘッダー)で定義されています。 型は、以下のカテゴリーに分類されます。

正確な幅の整数型
すべての実装で同じビット数 n を持つことが保証されている整数型(実装で利用可能な場合にのみ含まれます)。
最小幅の整数型
少なくとも指定されたビット数 n を持つ、実装で利用可能な最小の型であることが保証されています。少なくとも N=8,16,32,64 で指定されていることが保証されます。
最速の整数型
少なくとも指定されたビット数 n を持つ,実装で利用可能な最速の整数型であることが保証されています。少なくとも N=8,16,32,64 で指定されていることが保証されます。
ポインター整数型
ポインター保持できることが保証されている整数型(実装で利用可能な場合にのみ含まれます)。
最大幅の整数型
実装上最大の整数型であることが保証されている整数型。

以下の表は、型と実装の詳細を取得するためのインターフェースをまとめたものです(nはビット数を意味します)。

型と実装の詳細を取得するためのインターフェース
型カテゴリ 符号付き型 符号なし型
最小値 最大値 最小値 最大値
正確な幅 intn_t INTn_MIN INTn_MAX uintn_t 0 UINTn_MAX
最小幅 int_leastn_t INT_LEASTn_MIN INT_LEASTn_MAX uint_leastn_t 0 UINT_LEASTN_MAX
最速 int_fastn_t INT_FASTn_MIN INT_FASTn_MAX uint_fastn_t 0 UINT_FASTn_MAX
ポインター intptr_t INTPTR_MIN INTPTR_MAX uintptr_t 0 UINTPTR_MAX
最大幅 intmax_t INTMAX_MIN INTMAX_MAX uintmax_t 0 UINTMAX_MAX

C言語における共用体(Union)のよくあるユースケースは以下のとおりです。

  1. メモリの効率的な使用
    共用体を使うと、あるデータを格納するのに必要なメモリサイズが異なる場合でも、最大サイズのメンバーに合わせてメモリを確保できます。メモリの無駄を最小限に抑えられます。
  2. 異種混合データの表現
    1つのデータに複数の意味や解釈を持たせたい場合に便利です。例えば、数値データと文字データを同時に扱う必要がある場合などです。
  3. ビットフィールド操作
    共用体のメンバーを適切にサイズ指定することで、ビット単位でデータを扱えます。フラグや状態を表すビットマップなどの実装に役立ちます。
  4. システムデータ構造の実装
    OSやネットワークプロトコルなどで使用されるデータ構造を表現するのに共用体が使われることがあります。異なる解釈ができるデータ表現に適しています。
  5. 可変長データ構造
    共用体とポインタ、malloc/freeを組み合わせることで、可変長のデータ構造を実装できます。
  6. データの効率的な入出力
    共用体を使えば、異なるデータ型を同じメモリ領域にマップできるので、入出力のパフォーマンスが向上する可能性があります。
  7. レガシーコードとの互換性確保
    既存のデータ構造を変更せずに新しいメンバーを追加したい場合などに、共用体が役立つことがあります。

共用体の利用には注意が必要ですが、メモリ効率の観点や、異種混合データの扱いという点で、有用な機能となっています。

列挙体( enumeration )は、名前の付いた整数定数値( integer constant values )の集合で構成されています。それぞれの列挙は、異なる列挙型( enumerated type )を構成します[8]

ISO/IEC 9899:2023(通称 C23)の §6.7.2.2 Enumeration specifiers(列挙型指定子)を抄訳/引用します[9]

構文
enum-specifier
enum identifieropt { enumerator-list }
enum identifieropt { enumerator-list , }
enum identifier
enumerator-list
enumerator
enumerator-list , enumerator
enumerator
enumeration-constant
enumeration-constant = constant-expression
enumeration-constant[10]
identifier
制約条件
列挙定数の値を定義する式は、intとして表現可能な値を持つ整数定数式でなければならない。
セマンティクス
列挙子リスト( enumerator-list )の識別子( identifier )は、int型の定数として宣言されており、許容される範囲内であればどこにでも現れることができます。
=を伴った持つ列挙子( enumerator )は、その列挙定数( enumeration-constant )を定数式( constant-expression )の値として定義します。
最初の列挙子に=がない場合、その列挙定数の値は 0 です。
=がない後続の各列挙子は、前の列挙定数の値に1を加えた定数式の値としてその列挙定数を定義します。
(=付きの列挙子を使用すると、同じ列挙内の他の値と重複する値を持つ列挙定数が生成されることがあります)。
列挙体の列挙子は、そのメンバーとしても知られています。

構文中で、列挙子リスト( enumerator-list )の後ろに , がないケースとあるケースの両方が書かれているので、どちらの書き方も許されます。

列挙体の使用例
#include <stdio.h>

enum Day_of_the_Week {
  dwSUNDAY,
  dwMONDAY,
  dwTUESDAY,
  dwWEDNESDAY,
  dwTHURSDAY,
  dwFRIDAY,
  dwSATURDAY,
};

int weekend(enum Day_of_the_Week day) {
  switch (day) {
  case dwSUNDAY:
  case dwSATURDAY:
    return 1;
  case dwMONDAY:
  case dwTUESDAY:
  case dwWEDNESDAY:
  case dwTHURSDAY:
  case dwFRIDAY:
    return 0;
  }
}
int main(void) {
  const char * names[] = { // 順位を使った配列変数の初期化
    [dwSUNDAY] = "日曜日",
    [dwMONDAY] = "月曜日",
    [dwTUESDAY] = "火曜日",
    [dwWEDNESDAY] = "水曜日",
    [dwTHURSDAY] = "木曜日",
    [dwFRIDAY] = "金曜日",
    [dwSATURDAY] = "土曜日",
  };
  for (enum Day_of_the_Week dw = dwSUNDAY; dw <= dwSATURDAY; dw++) {
    printf("%i: %s(%s)\n", dw, names[dw], weekend(dw) ? "週末" : "平日");
  }
}
実行結果
0: 日曜日(週末)
1: 月曜日(平日)
2: 火曜日(平日)
3: 水曜日(平日)
4: 木曜日(平日)
5: 金曜日(平日) 
6: 土曜日(週末)
列挙子(enumで定義する定数;しばしば列挙メンバーと呼ばれます)は、同じスコープでは別の列挙体でも同じ名前空間を使うので、衝突を避けるため列挙子に列挙体のニーモニックを前置するなどの対策が必要です。
ここでは、列挙子の名前が dw で始まるのはタグ Day_of_the_Week の列挙子であることを示しています。
列挙型の変数、dw はもちろん enum Day_of_the_Week 型であることを示しています。

この命名ルールは一例ですが、実際のプログラミングでは多くの識別子の名前を考える必要があるので、一貫性のある命名ルールを作ってコードを書くことで可読性・保守性が向上します。

Cのenumには、列挙子を集合で返したり、反復する機能も、列挙子の数を返す機能もないので、ループを廻すときは最初の列挙子から最後の列挙子までインクリメントしながらループすることになります(列挙子に初期値を与えている場合は、この手段も使えません)。

また、C++のコードとしてコンパイルすると、dw++ がエラーになります[11]

タグ

編集

ISO/IEC 9899:2023(通称 C23)の §6.7.2.3 Tags(タグ)を抄訳/引用します[12]

制約事項
特定の型は、その内容が最大で一度だけ定義されなければならない。
以下の形式の型指定子は
enum identifier
それが指定する型が完成した後にのみ現れるものとする。
セマンティクス
同じスコープで同じタグを使用する structunion、または enum の宣言はすべて同じスコープを持ち、同じタグを使用する structunionenum のすべての宣言は、同じ型を宣言する。
型は、内容を定義するリストの閉じ括弧までは不完全で、それ以降は完全な型となります。
structunionenum の宣言のうち、スコープが異なるものや、異なるタグを使用しているものが2つあると、異なる型を宣言することになります。
異なるスコープまたは異なるタグを使用する2つの structunionenum の宣言は、異なる型を宣言します。
structunion、または enum の各宣言でタグを含まない structunionenum の宣言は、それぞれ個別の型を宣言します。
次のような形式の型指定子
struct-or-union identifieropt { struct-declaration-list }。
struct-or-union identifieropt { member-declaration-list }
または
enum identifier { enumerator-list }
の形式の型指定子。または
enum identifier { enumerator-list , }
は、 structunion、または enum を宣言します。
リストは、struct のコンテンツ、union のコンテンツ、または enum のコンテンツを定義します。
識別子が提供されている場合、型指定子は識別子をその型のタグであるとも宣言する。

型修飾子

編集

型修飾子( Type qualifiers )は次のいずれかの組み合わせです[13].。

構文
type-qualifier
const
restrict
volatile
_Atomic
制約事項
参照される型がオブジェクト型であるポインター型以外の型は、制限修飾してはならない。
実装がアトミック型をサポートしていない場合、_Atomic修飾子は使用してはならない(6.10.8.3 Conditional feature macros 参照)。
_Atomic修飾子によって変更される型は、配列型や関数型であってはならない。
型修飾子の一覧
型修飾子 意味
const 初期化以降の代入を禁止します。
restrict aliasが存在しないと仮定した最適化を許します。
volatile 未知の方法でインスタンスが書き換えられうる事を示し、最適化を抑制します。
_Atomic 不可分操作を提供します。

const修飾型をもったオブジェクトは初期化以外で変更不可能となります[13]。 つまり、そのオブジェクトを不変値( constant value )として扱うことができ[14]、言語処理系に畳み込みなどの最適化の機会を与えます。 const修飾型をもったオブジェクトは初期化が必須になります。


例、次の宣言のペアは、「不変値への可変ポインター」と「可変値への不変ポインター」の違いを示しています。

const int *ptr̲to̲constant;   
int *const constant̲ptr;

ptr_to_constant が指すオブジェクトの内容は、そのポインターを介して変更してはならないが、ptr_to_constant 自体は別のオブジェクトを指すように変更することができる。 同様に、constant_ptr が指すintの内容は変更されても構いませんが、constant_ptr 自体は常に同じ場所を指すものとします。

constの誤った使用例
int main(void) {
  const int i = 1;
  i++; // iの値は変更不可能なので、この箇所でコンパイルエラーとなります。
}
コンパイル結果
Main.c:3:4: error: cannot assign to variable 'i' with const-qualified type 'const int'
  i++; // iの値は変更不可能なので、この箇所でコンパイルエラーとなります。
  ~^
Main.c:2:13: note: variable 'i' declared const here
  const int i = 1;
  ~~~~~~~~~~^~~~~ 
1 error generated.

restrict

編集

restrictとは、ポインターに用いる型修飾子であり、そのポインターが指す先を、同一関数・ブロック内の別のポインターが指さないという情報をコンパイラーに伝え、コンパイラーの最適化を促進することができます。 restrict型修飾子を付けても付けなくても、目に見える動作は変わりません。 [15]

restrictの例として、標準ライブラリstring.hのmemcpy関数とmemmove関数が挙げられる。

#include<string.h>

void *memcpy(void * restrict s1, const void * restrict s2, size_t n);
void *memmove(void *s1, const void *s2, size_t n);

これら2つの関数は、どちらもs2をs1にn文字コピーしますが、memcpyは領域の重なり合わないオブジェクト間でコピーする必要があり、 そのためrestrict型修飾子を用いることができます。

volatile

編集

volatileとは、変数がコンパイラーに未知の方法で変更され、又はその他の未知の副作用を持つことをコンパイラーに伝え、コンパイラーの最適化を抑制する型修飾子です。 volatileで型修飾する動機として:

  • メモリーマップドI/O
  • 異なるスレッド間で共有されるオブジェクト
  • 非同期シグナルのハンドラーによりモディファイされるオブジェクト
  • setjmp が呼ばれてから longjmp が呼ばれるまでに変更され得るスタック上のオブジェクト

などがあります。 これらのオブジェクトの参照が共通部分式削除定数畳み込みなどの言語処理系による畳込みでループの最初に一度だけ評価されたり、あるいは全く評価されなくなることを避ける狙いがあります。

ただし、

  • volatileで型修飾されたオブジェクトとそうでないオブジェクトとの間の実行順序は保証されない
  • volatileで型修飾されたオブジェクトへのアクセスが分割されないことは保証されない(アトミックであることは保証されない)

など、volatileで型修飾する動機になったニーズを完全に充足しているかは検討の余地があります。

_Atomic

編集

原子型指定子( Atomic type specifiers )は、スレッドとともにC11で導入されました[16]

構文
atomic-type-specifier
_Atomic ( type-name )
制約事項
実装が原子型をサポートしていない場合、原子型指定子( Atomic type specifiers )を使用してはならない(6.10.8.3 Conditional feature macros 参照)。
原子型指定子の型名は、配列型、関数型、原子型、または修飾型を参照してはならない。
セマンティクス
原子型に関連するプロパティは、lvalues(左辺値)である式に対してのみ意味を持ちます。
キーワード_Atomicの直後に左括弧がある場合は、型修飾子ではなく、(型名を持つ)型指定子として解釈されます。

脚註

編集
  1. ^ N3096 working draft — April 1, 2023 ISO/IEC 9899:2023 (E). ISO/IEC JTC1/SC22/WG14. p. 96, §6.7 Declarations. https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3096.pdf. 
  2. ^ N3096 working draft — April 1, 2023 ISO/IEC 9899:2023 (E). ISO/IEC JTC1/SC22/WG14. p. 97, §6.7.1 Storage-class specifiers. https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3096.pdf. 
  3. ^ 演算子の結合性は associativity of operators。JISは、2つの術語に1つの訳を与えてしまっている
  4. ^ N3096 working draft — April 1, 2023 ISO/IEC 9899:2023 (E). ISO/IEC JTC1/SC22/WG14. p. 189, §6.11.2 Linkages of identifiers. https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3096.pdf. 
  5. ^ 6.11.5 storage-class specifiers
  6. ^ N3096 working draft — April 1, 2023 ISO/IEC 9899:2023 (E). ISO/IEC JTC1/SC22/WG14. p. 79, §6.7.2 Type specifiers. https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3096.pdf. 
  7. ^ 引用エラー: 無効な <ref> タグです。「c99」という名前の注釈に対するテキストが指定されていません
  8. ^ N3096 working draft — April 1, 2023 ISO/IEC 9899:2023 (E). ISO/IEC JTC1/SC22/WG14. p. 31, §6.2.5 Types. https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3096.pdf. 
  9. ^ N3096 working draft — April 1, 2023 ISO/IEC 9899:2023 (E). ISO/IEC JTC1/SC22/WG14. p. 84, §6.7.2.2 Enumeration specifiers. https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3096.pdf. 
  10. ^ §6.4.4.3 Enumeration constants
  11. ^ static_cast を使えばdw = static_cast<Day_of_the_Week>(dw + 1)コンパイルできますが、static_castは、コンパイラーの型チェックを骨抜きにしてしまうので、乱用は避けましょう(operator ++ をオーバーロードするにしても、その中で static_cast が必要)。
  12. ^ N3096 working draft — April 1, 2023 ISO/IEC 9899:2023 (E). ISO/IEC JTC1/SC22/WG14. p. 85, §6.7.2.3 Tags. https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3096.pdf. 
  13. ^ 13.0 13.1 N3096 working draft — April 1, 2023 ISO/IEC 9899:2023 (E). ISO/IEC JTC1/SC22/WG14. p. 87, §6.7.3 Type qualifiers. https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3096.pdf. 
  14. ^ N3096 working draft — April 1, 2023 ISO/IEC 9899:2023 (E). ISO/IEC JTC1/SC22/WG14. p. 93, §6.7.6.1 Pointer declarators. https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3096.pdf. 
  15. ^ 引用エラー: 無効な <ref> タグです。「型修飾子」という名前の注釈に対するテキストが指定されていません
  16. ^ N3096 working draft — April 1, 2023 ISO/IEC 9899:2023 (E). ISO/IEC JTC1/SC22/WG14. p. 87, §6.7.2.4 Atomic type specifiers. https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3096.pdf. 

参考文献

編集