C言語では、プログラムに追加の情報を提供するために属性が使用されます。属性は、様々なソース構造に関連付けられることがあり、型、オブジェクト、識別子、またはブロックに指定されます。これらの属性は、コンパイラに追加の情報を提供し、特定の動作や最適化を促進するのに役立ちます。

標準属性と実装固有の属性

編集

属性には、標準属性と実装固有の属性の2種類があります。標準属性はC言語の仕様書で定義され、特定の動作を指定します。一方、実装固有の属性は特定のコンパイラや開発環境に固有の拡張であり、通常はその特定の環境でのみ有効です。

属性の構文

編集

属性は二重角括弧で囲まれたリスト([[属性名]])として表されます。属性は、属性リスト内にコンマで区切られた形式で指定されます。属性リストは、ソース構造に関連付けられた属性を指定するために使用されます。

標準属性の例

編集

C言語の仕様書ではいくつかの標準属性が定義されています。

標準属性とその意味
属性 意味
deprecated 指定された要素が非推奨であり、将来のバージョンで削除される可能性があることを示す
fallthrough switch文内のcaseラベルが他のcaseまたはdefaultラベルにフォールスルーすることを許可する
maybe_unused 警告なしで未使用のエンティティを指定する
nodiscard 関数の戻り値を無視してはいけないことを示す
noreturn 関数が呼び出し元に戻らないことを示す
_Noreturn 関数が呼び出し元に戻らないことを示す(互換性のための別名)
unsequenced 演算が規定されたシーケンスで実行されないことを示す
reproducible プログラムが再現可能であることを保証する

これらの属性は、C言語の仕様書で定義され、コンパイラや開発環境で特定の動作を指定するために使用されます。

属性の推奨される慣習

編集

属性の使用に関して、いくつかの推奨される慣習があります。例えば、すべての実装は、仕様書で定義された標準属性をサポートすることが推奨されます。また、実装固有の属性を定義する際には、その属性に固有の識別子を使用することが推奨されます。

意味論と制約

編集

属性の効果や適用条件に関する詳細な規則が仕様書で提供されています。これには、標準属性がどのように解釈されるか、属性リスト内の順序が重要であるかどうか、などが含まれます。これらの規則には、コンパイラが正確に属性を解釈し、プログラムの意味を適切に理解するためのガイドラインが含まれています。

以下は、これらの属性を使用したコードの例です:

#include <stdio.h>
#include <stdlib.h>

// [[deprecated]] 属性の使用例
[[deprecated]]
void old_function() {
    printf("This function is deprecated.\n");
}

// [[nodiscard]] 属性の使用例
[[nodiscard]]
int important_calculation() {
    return 42;
}

// [[noreturn]] 属性の使用例
[[noreturn]]
void fatal_error() {
    printf("A fatal error occurred.\n");
    exit(1);
}

int main() {
    old_function();  // 警告: deprecated

    int result = important_calculation();  // 正常
    // important_calculation(); // 警告: nodiscard

    // fatal_error(); // 正常: noreturn で終了

    return 0;
}

この例では、old_functionは非推奨であり、important_calculationの戻り値は無視すべきではなく、fatal_errorは決して戻らないことが示されています。

標準属性

編集

nodiscard 属性

編集

nodiscard属性は、関数やデータ型の定義に付与することができる属性です。この属性が付与された関数やデータ型のインスタンスが使用された際に、その返値や値が無視されていないかをチェックし、無視されている場合は診断メッセージを出力することができます。このように、想定外の振る舞いを防止するのが主な目的です。

構文

編集

nodiscard属性は、関数定義や構造体、共用体、列挙型の定義に対して適用することができます。属性引数句があれば、その形式は(文字列リテラル)となります。

意味論

編集

実装がnodiscard属性をサポートしている場合、__has_c_attribute条件付き組み込み式(6.10.2)にnodiscardを渡すと値202311Lが返されます。

属性が付与されていない名前や実体は後から再宣言してnodiscard属性を付与することができ、その逆も可能です。最初にnodiscard属性が付与された宣言以降、その実体はnodiscardマークされたとみなされます。

推奨される使用法

編集

nodiscard呼び出しとは、nodiscard属性が付与された関数を呼び出す関数呼び出し式、またはnodiscard属性が付与された構造体、共用体、列挙型の値を返す関数呼び出し式のことを指します。このようなnodiscard呼び出しをvoid式として評価することは、想定外の振る舞いを引き起こす可能性があるため、避けるべきです。実装はこのような場合に診断メッセージを出力することが推奨されています。

出力される診断メッセージには、nodiscard属性のある名前や実体に対して付与された属性引数句の文字列リテラルが含まれるべきです。

まとめ

編集

nodiscard属性は、関数やデータ型の返値や値が無視されないよう注意を促す機能です。適切に使用することで、予期せぬ動作を防ぎ、プログラムの安全性を高めることができます。しかし、過剰に使いすぎるとかえってノイズになる可能性もあるため、慎重に使用する必要があります。

maybe_unused 属性

編集

概要

編集

maybe_unused属性は、構造体、共用体、typedef名、オブジェクト、構造体や共用体のメンバー、関数、列挙型、列挙子、ラベルの宣言に対して付与することができる属性です。この属性が付与された実体が未使用であった場合でも、実装が診断を出力しないよう指示するのが目的です。

制約

編集

maybe_unused属性は、構造体、共用体、typedef名、オブジェクト、構造体や共用体のメンバー、関数、列挙型、列挙子、ラベルの宣言に対して適用することができます。この属性には属性引数句を付与してはいけません。

意味論

編集

maybe_unused属性は、その名前や実体が意図的に未使用である可能性があることを示します。

実装がmaybe_unused属性をサポートしている場合、__has_c_attribute条件付き組み込み式(6.10.2)にmaybe_unusedを渡すと値202311Lが返されます。

属性が付与されていない名前や実体は後から再宣言してmaybe_unused属性を付与することができ、その逆も可能です。最初にmaybe_unused属性が付与された宣言以降、その実体はmaybe_unusedマークされたとみなされます。

推奨される使用法

編集

maybe_unused属性が付与された実体に対しては、実装がその実体が未使用であることを診断しないことが推奨されています。また、属性が付与されているにもかかわらずその実体が使用されている場合も、診断を控えることが推奨されています。

[[maybe_unused]] void f([[maybe_unused]] int i) {
    [[maybe_unused]] int j = i + 100;
    assert(j);
}

上記の例では、関数fとその引数i、ローカル変数jにmaybe_unused属性が付与されています。NDEBUGが定義されていても、実装はjが未使用であることを診断しないことが推奨されています。

まとめ

編集

maybe_unused属性は、コード中で意図的に未使用の実体を残す必要がある場合に便利です。この属性を適切に使用することで、未使用の実体に対する無用な診断を防ぐことができます。

ただし、この属性が濫用されるとコードの可読性が低下する可能性があります。未使用の実体が本当に意図的なものなのか、単に不要なデッドコードなのかを見極める必要があります。

また、この属性はコンパイラ固有の機能である可能性があり、移植性にも留意する必要があります。異なる環境で同じコードをビルドする場合、maybe_unused属性がサポートされているかどうかを確認しておく必要があります。

deprecated 属性

編集

概要

編集

deprecated属性は、名前や実体の使用が許可されているものの、何らかの理由で使用が推奨されない場合に付与できる属性です。特に、名前や実体が時代遅れ、不安全、目的に適さないなどの場合に適しています。

制約

編集

deprecated属性は、構造体、共用体、typedef名、オブジェクト、構造体や共用体のメンバー、関数、列挙型、列挙子の宣言に対して適用することができます。属性引数句がある場合、その形式は(文字列リテラル)となります。

意味論

編集

deprecated属性は、使用が推奨されない名前や実体をマークするために使用できます。

実装がdeprecated属性をサポートしている場合、__has_c_attribute条件付き組み込み式(6.10.2)にdeprecatedを渡すと値202311Lが返されます。

属性が付与されていない名前や実体は後から再宣言してdeprecated属性を付与することができ、その逆も可能です。最初にdeprecated属性が付与された宣言以降、その実体はdeprecatedマークされたとみなされます。

推奨される使用法

編集

実装は、deprecated属性が指定された宣言の後で、その名前や実体が関連するdeprecated実体のコンテキスト外で参照された場合に、診断メッセージを出力することが推奨されています。この診断メッセージには、その名前や実体に適用されたdeprecated属性の属性引数句内の文字列リテラルが含まれるべきです。

deprecatedマークされた名前や実体への参照に対する診断は、その参照がdeprecatedマークされた関数やデータ型などの関連するコンテキスト内にある場合は抑制されることが推奨されています。

fallthrough 属性

編集

概要

編集

fallthrough属性は、switch文内で使用できる属性です。この属性を使用することで、実装が通常は警告を出すであろうケースラベルやデフォルトラベルへのフォールスルー(実行が次のケースに落ちること)に対して、警告を抑制することができます。

制約

編集

fallthrough属性トークンは、属性宣言(6.7)内でのみ現れることができます。そのような宣言はfallthrough宣言と呼ばれます。属性引数句は許可されていません。fallthrough宣言は、外側のswitch文(6.8.5.3)の内部でのみ出現できます。fallthrough宣言の後に出現する次のブロック項目(6.8.3)は、最も内側のswitch文に関連付けられたcaseラベルまたはdefaultラベルでなければなりません。そして、fallthrough宣言が反復文内に含まれている場合、次の文は最も内側の反復文のセカンダリブロックの同一実行の一部でなければなりません。

意味論

編集

実装がfallthrough属性をサポートしている場合、__has_c_attribute条件付き組み込み式(6.10.2)にfallthroughを渡すと値202311Lが返されます。

推奨される使用法

編集

fallthrough宣言の使用は、ある実行パスに沿って別のcaseラベルやdefaultラベルから到達可能なcaseラベルやdefaultラベルに対して、実装が発する可能性のある診断を抑制することを意図しています。fallthrough宣言が動的に到達不可能な場合、実装は診断を発することが推奨されています。

noreturn 属性 と _Noreturn 属性

編集

概要

編集

noreturn属性と_Noreturn属性は、関数が決して返らないことを示すために使用される属性です。この属性が関数に付与されている場合、関数が返ってきたらその振る舞いは未定義となります。

説明

編集

_Noreturnが属性トークンとして使用された場合(関数指定子としてではなく)、noreturn属性トークンと同じ制約とセマンティクスが適用されます。_Noreturnを属性トークンとして使用することは廃止される機能です。

制約

編集

noreturn属性は関数に対してのみ適用できます。属性引数句は許可されていません。

意味論

編集

ある関数の1つの宣言でnoreturn属性が指定されている場合、その関数の最初の宣言でもnoreturn属性を指定しなければなりません。あるトランスレーション単位でnoreturn属性付きで宣言された関数が、別のトランスレーション単位でnoreturn属性なしで宣言された場合、その振る舞いは未定義となります。

noreturn属性が付与されていた関数fが呼び出されたが、最終的にfが返ってきた場合、その振る舞いは未定義となります。

実装がnoreturn属性をサポートしている場合、__has_c_attribute条件付き組み込み式(6.10.2)にnoreturnを渡すと値202311Lが返されます。

推奨される使用法

編集

実装は、noreturn属性が付与された関数が呼び出し元に返ってくる可能性がある場合、診断メッセージを出力するべきです。

まとめ

編集

noreturn属性と_Noreturn属性は、関数が決して返らないことを示すために使用されます。この属性が付与された関数が実際に返ってきた場合、その振る舞いは未定義となります。この属性は関数の宣言時に指定する必要があり、トランスレーション単位間で一貫性がなければなりません。 実装は、noreturn関数が返る可能性がある場合に適切に診断を出力するべきです。

関数型に関する属性

編集

一般

編集
概要
編集

この節で定義される関数型のプロパティと属性の主な目的は、関数呼び出し時のオブジェクトアクセスに関する情報を翻訳者に提供し、関数呼び出しの特性を推定できるようにすることです。これらのプロパティは、読み取り操作(statelessかつindependent)、書き込み操作(effectless、idempotent、reproducible)、またはその組み合わせ(unsequenced)を区別します。

制約
編集

標準の関数型属性として許可される識別子は、unsequencedとreproducibleのみです。

関数型属性は、関数宣言子または関数型を持つ型指定子に対して適用されます。対応する属性は関数型のプロパティとなります。属性引数句は許可されていません。

説明
編集

これらの属性は、関数型には意味論的に付加されますが、そのような関数のプロトタイプの一部ではありません。したがって、再宣言や変換でこの属性が落とされても、型は互換となります。逆に、この属性を持つ関数宣言やポインタから、その特性を持たない定義にアクセスした場合、振る舞いは未定義となります。

これらの属性で記述された関数呼び出しを並べ替えるために、呼び出し前に開始し呼び出し後に終了するオブジェクトの寿命へのアクセスを制限する必要があります。関数呼び出し中のオブジェクトへの効果は、その呼び出しと同じスレッドに制限されます。ポインタパラメータとlvalueの間のbased-on関係は、オブジェクトが呼び出し中に意図せず変更されないことをモデル化しています。

関数呼び出しの開始後、終了前に実行される操作は、その関数呼び出し中に順次実行されると言えます。オブジェクトXの定義がある関数fからXへのアクセスがfの呼び出しが活性化していない間に発生する場合、Xはfからエスケープしたと言えます。オブジェクトの寿命がある呼び出しの間に開始し終了する場合、またはそのオブジェクトがfによって定義されて

いてエスケープしない場合、そのオブジェクトはfの呼び出しにローカルであると言えます。関数呼び出しとオブジェクトXが同期している場合、Xへの呼び出し中に順次実行されていないアクセスはすべて呼び出しの前後に発生します。

実行状態(浮動小数点環境、ロケール、入出力ストリームなど)はこれらの属性の目的上オブジェクトとみなされ、この状態にアクセスする操作はlvalue変換、この状態を変更する操作はストア操作とみなされます。

関数の特性
編集

stateless関数は、静的またはスレッド記憶期間を持つオブジェクトの定義が関数内またはその関数から呼び出される関数内で、const指定されており volatile指定されていない場合です。

オブジェクトXが関数呼び出しによって観測されるのは、両者が同期し、Xがその呼び出しにローカルでなく、Xの寿命が関数呼び出しの前に開始し、Xへのアクセスがその呼び出し中に順次実行される場合です。呼び出し前に最後に格納されたXの値が、その呼び出しによって観測される値と呼ばれます。

関数ポインタ値fがindependentである場合、fのいずれかの呼び出しを通してパラメータに基づかないlvalueから観測されるオブジェクトXについて、プログラム実行中のfの全ての呼び出しでXへのアクセスによって同じ値が観測されます。そうでない場合(アクセスがポインタパラメータに基づく場合)、Xへのアクセスはすべて一意のポインタパラメータPに基づくlvalueに対して行われなければなりません。関数定義がindependentである場合、派生した関数ポインタ値はindependentです。

関数呼び出し中に順次実行されるオブジェクトXへの格納操作が、両者が同期し、Xがその呼び出しにローカルでなく、Xの寿命が呼び出し後に終了し、格納された値が呼び出しによって観測された値(もしあれば)と異なり、呼び出しの終了前に最後に書き込まれた値である場合、その格納操作は観測可能と呼ばれます。関数呼び出しの評価がeffectlessである場合、呼び出し中に順次実行されるストア操作はその呼び出しと同期するオブジェクトの変更のみです。さらに、その操作が観測可能である場合、関数にはXへのアクセスがすべて一意のポインタパラメータPに基づくlvalueに対して行われるようなポインタパラメータPが1つだけ存在しなければなりません。

関数ポインタ値fがeffectlessである場合、fを呼び出す関数呼び出しの評価がすべてeffectlessです。関数定義がeffectlessである場合、派生した関数ポインタ値はeffectlessです。

評価Eがidempotentである場合、Eを2回評価しても、結果の値(もしあれば)や実行の観測可能な状態を変更しません。関数ポインタ値fがidempotentである場合、fを呼び出す関数呼び出しの評価がすべてidempotentです。関数定義がidempotentである場合、派生した関数ポインタ値はidempotentです。

関数がreproducibleである場合、effectlessかつidempotentです。関数がunsequencedである場合、stateless、effectless、idempotent、independentです。

注釈
編集

関数のindependenceに関する同期要件は、プログラムの意味論を変更せずに関数呼び出しを安全に並べ替えできる境界を提供します。アクセスするオブジェクトXがconst指定されていてvolatile指定されていない場合、並べ替えには制約がありません。Xが初期化フェーズで条件付けられたオブジェクトである場合、単一スレッドプログラムでは順序付け前の関係による同期が提供され、原則として呼び出しを初期化の直後に移動できます。マルチスレッドプログラムでは、<threads.h>ヘッダーの同期関数の呼び出しや、初期化フェーズの終了時に適切なatomic_thread_fenceの呼び出しによって同期保証を与えることができます。

関数がindependentまたはeffectlessであることがわかっている場合、すべてのポインタパラメータの宣言にrestrictを付けても、呼び出しの意味論は変わりません。同様に、そのような関数の呼び出し中のすべての原子操作のメモリ順序をmemory_order_relaxedに変更しても意味論は保たれます。

<math.h>ヘッダーで提供されている関数の多くは、この副節で前述したプロパティを持っていません。多くの関数はエラー時に浮動小数点状態やerrnoを変更するため(観測可能な副作用があるため)、ほとんどの結果は丸め方向モードなどの実行ワイド状態に依存するためです(independentではないため)。特定のCライブラリ関数がreproducibleまたはunsequencedであるかどうかは、実装の特性(特定のエラー条件に対する実装定義の振る舞いなど)にも依存することがあります。

推奨される使用法
編集

可能であれば、実装はこれらの属性が対応するプロパティを持たない関数定義に適用された場合に診断することが推奨されます。アプリケーションがindependentまたはeffectlessなプロパティを主張する関数については、ポインタパラメータにrestrictを付与することが推奨されます。

reproducible 型属性

編集
説明
編集

reproducible型属性は、その型の関数またはその型を指すポインタが指す関数がreproducibleであることを主張します。

意味論
編集

実装がこの属性をサポートしている場合、__has_c_attribute条件付き組み込み式(6.10.2)にreproducibleを渡すと値202311Lが返されます。

size_t hash(char const[static 32]) [[reproducible]];

上記の関数宣言では、この属性は連続した2回の呼び出しで同じ戻り値が得られることを主張しています。呼び出し中の抽象状態の変更は可能ですが、それが観測可能でない限り、他の副作用は発生しません。したがって、この関数定義では、ローカルな静的またはスレッド記憶期間を持つオブジェクトを使って呼び出し時の引数を追跡し、計算された戻り値をキャッシュすることができます。

unsequenced 型属性

編集
説明
編集

unsequenced型属性は、その型の関数またはその型を指すポインタが指す関数がunsequencedであることを主張します。

意味論
編集

実装がこの属性をサポートしている場合、__has_c_attribute条件付き組み込み式(6.10.2)にunsequencedを渡すと値202311Lが返されます。

注釈1
編集

unsequenced型属性は、そのような関数に対して強力なプロパティを主張します。特に、関数呼び出しの特定の順序付け要件を緩和しても、抽象マシンの状態に影響を与えないことを意味します。したがって、そのような関数の呼び出しは共通部分式除去、局所的メモ化、遅延評価などの最適化手法の候補となります。

注釈2
編集

関数型にunsequenced属性を付与することの妥当性の証明は、派生した関数ポインタがトランスレーション単位からエスケープするかどうかの性質に依存する可能性があります。内部リンケージを持ち、関数ポインタがトランスレーション単位からエスケープしない関数の場合、すべての呼び出しコンテキストが既知であり、例外条件を引き起こす引数でライブラリ関数が呼び出されるような制御フローが存在しないことを原理的に証明することが可能です。外部リンケージを持つ関数の場合、そのような証明ができない可能性があり、その場合は提供された引数で例外条件が発生しないよう確認する必要があります。

注釈3
編集

unsequencedプロパティは必ずしも、その関数が再入可能であったり、呼び出しが並行して実行できることを意味するわけではありません。これは、unsequenced関数が静的記憶期間のオブジェクトから読み取りや書き込みを行える一方で、呼び出しが終了した後に変更が観測可能でなければならないためです。

bool tendency(signed char) [[unsequenced]];

上記の関数宣言では、この属性はその関数が抽象マシンの変更可能な状態に依存しないことを主張しています。したがって、与えられた引数値に対する呼び出しは1回だけ実行する必要があり、返された値は適切なタイミングで再利用できます。例えば、すべての可能な引数値に対する呼び出しをプログラム起動時に実行してテーブル化しておくことができます。

typedef struct toto toto;
toto const* toto_zero(void) [[unsequenced]];

上記の関数宣言では、この属性はその関数が抽象マシンの変更可能な状態に依存しないことを主張しています。同一スレッド内では、この関数の呼び出しは戻り値が必要になる前に順不同で実行でき、2回の呼び出しでは同じポインタ戻り値が得られます。したがって、そのような呼び出しは特定のスレッドで1回だけ実行する必要があり、返されたポインタ値とその指すtoto const型のオブジェクトの値をキャッシュできます。

inline double distance(double const x[static 2]) [[reproducible]] {
  #pragma STDC FP_CONTRACT OFF 
  #pragma STDC FENV_ROUND FE_TONEAREST
  // sqrtは無効な引数で呼ばれず、結果は引数値のみに依存すると主張する
  extern typeof(sqrt) [[unsequenced]] sqrt;
  return sqrt(x[0]*x[0] + x[1]*x[1]);
}

関数fのunsequencedプロパティは、fを使う別の関数g内で局所的に主張できます。例えばライブラリ関数sqrtは一般的にはunsequencedではありません。負の引数に対してはドメインエラーを引き起こし、結果は丸め方向モードに依存するためです。しかし、上記のような関数コンテキストでは、ユーザーが無効な引数でsqrtが呼ばれないこと、浮動小数点環境のデフォルト値が全ての呼び出しで同じであることを証明できます。

そのため、distanceの中でsqrtにunsequenced属性を付与できます。distanceには浮動小数点環境を変更する副作用がある可能性がありますが、その状態はスレッドローカルなので、関数外部での変更は関数内部での変更と順序付けられ、さらに観測される値は関数が戻る際に復元されます。したがって、この副作用は呼び出し側から観測できません。全体としてdistanceはstateless、effectless、idempotentであり、その属性が示すようreproducibleです。ただし、浮動小数点環境の異なる状態でdistanceが呼び出される可能性があるため、independentではありませんし、unsequencedでもありません。しかし、正当化できる場合にunsequenced属性を付与すれば、最適化の機会が生まれる可能性があります。

 
double g(double y[static 1], double const x[static 2]) {
  // distanceは浮動小数点環境の異なる状態を観測しないと主張する
  extern double distance(double const x[static 2]) [[unsequenced]];
  y[0] = distance(x);
  ...
  return distance(x); // y[0]に置き換え可能
}

まとめ

編集

属性は、C言語プログラムに追加の情報を提供するための重要なツールです。標準属性と実装固有の属性を使用することで、プログラマはコンパイラに対して特定の動作を指示し、コードの品質や効率を向上させることができます。属性の使用に関する仕様書の規則に従うことで、プログラマはコンパイラが正確にプログラムを解釈し、予想どおりに動作することを保証することができます。