enum/union/typedefの実務
enumとunion、typedefを明確なルールを設けずに、または場当たり的に混用しているコードは、意外と多いのではないでしょうか。明確な使い分けをおこなわないと、別の担当者が解読しにくくなり、保守性を大きく損なうおそれがあります。この記事ではJPCERT/CC のDCL06-C、N1570の該当節、MISRA C:2012、一般的なマイコン(マイクロコントローラ)ベンダーのHAL(Hardware Abstraction Layer)を軸に、判断の拠り所を整理します。
- マジックナンバー排除のためのenum活用
- enumの型安全性と暗黙変換の注意点
- unionの用途(レジスタ表現・可変フィールド)
- typedefで隠すべき型・隠さない型
- 命名規則と可読性(代表的なマイコン向けコアライブラリ/HALに見られる命名パターン)
- MISRA C:2012 のenum/union/typedef関連ルール
- C++のenum class/usingとの対比
マジックナンバー排除のためのenum活用
enumは、マジックナンバーを排除する目的で用いられる有効な方法の一つです。JPCERT/CC セキュアコーディングスタンダード DCL06-C「意味のある値を表現するのに、リテラルではなくシンボリック定数を使用する」の要求に応えられる方法です。
JPCERT/CC DCL06-C の要点
DCL06-C では、配列サイズや状態値、優先度など意味の固定された数値をシンボリック定数で表すよう求めています。ISO/IEC 9899:2011(C11)の最終ドラフト(WG14 N1570)§6.7.2.2「Enumeration specifiers」が、列挙子の定義を解説しています。列挙子を定義できるenumは、定数式が要求される配列宣言やswitch-caseラベルでも利用できるメリットがあります。
#defineとの違い(デバッガでのシンボル可視性)
#defineで定義できるのは数値だけではありません。N1570 §6.10.3のとおり、マクロ名は前処理の段階で置換リストのトークン列へ置き換えられ、コンパイル後のシンボルとしては残りにくい性質があります(ただしデバッグ情報として保持できる処理系もあります)。この点が、enumとの相違点です。
-gだけではマクロ情報が十分に参照できない場合があります。GCCでは-g3を指定すると、マクロ定義もデバッグ情報に含められます(デバッガ側の対応にも依存)。これに対して列挙子は識別子であり、DWARF仕様は列挙型と各列挙子を名前付きの項目として記録すると定めています。DWARFでは列挙型はDW_TAG_enumeration_type、列挙子はDW_TAG_enumeratorとして保持されます。
有効範囲の扱いも違います。§6.2.1はマクロ名を意味解析の前に置換されるものと位置づけ、有効範囲の議論には含めていません。一方の列挙定数は識別子として、宣言位置に応じた有効範囲を持ちます。同じ有効範囲での二重宣言はコンパイルエラーです。
enumの型安全性と暗黙変換の注意点
N1570 §6.7.2.2の記載により、列挙子はint型の定数とされます。一方で、列挙型そのものの表現(互換となる整数型やサイズ)は実装定義であり、コンパイラのオプションでも変化し得ます。enumをintと同じ感覚で扱うと、思わぬ不具合につながります。列挙外の値が混入した場合でも、コンパイラで検出できないケースがあるため注意が必要です。
C11 §6.7.2.2 の暗黙int変換
N1570 §6.7.2.2パラグラフ3は、列挙子リストの識別子を「int型を持つ定数」として宣言すると定めています。列挙定数(enumeration constant)がint型であることは、§6.4.4.3「Enumeration constants」でも規定されています。
列挙型は、char/符号付き整数型/符号なし整数型のいずれかと互換(実装定義)であり、式中では整数変換が起き得るため、§6.3.1.8の通常の算術変換が働きます。列挙型と整数型を混ぜた式が通るのはこのためです。
上記で示す暗黙の変換により、列挙外の整数が入った場合でも処理系によってはエラーや警告とならない場合があります。switchのdefaultで「起きないはず」の値をつかむ事故は、ここから生まれます。
-fshort-enumsの副作用
GCC・Clangなどのコンパイラは -fshort-enums オプションを備えています。コンパイラ公式ドキュメントのコード生成オプションの項目には、このオプションが列挙型に「宣言された値の範囲を表すのに必要なバイト数だけを割り当てる」と説明されています。このオプションにより、RAMやROMを節約できる場合があります。
ただし-fshort-enumsは、同一ABI/同一ビルドオプションの前提を崩します。コンパイル単位やライブラリが混在するとABI不一致になり、構造体レイアウトの差で通信データや永続化データも壊れるおそれがあります。バイナリ非互換になり得るため(コンパイラ公式ドキュメントでも警告されている)、使用する際は注意が必要です。
RAMの節約と構造体の隙間のずれは、同じ変更の表と裏です。オプションを付けた側と付けない側では、同じヘッダでも同じメンバが別のオフセットを指すおそれがあります。
unionの用途(レジスタ表現・可変フィールド)
unionは同じ記憶域を共有する
書いたメンバから読むのが原則
安全な逃げ道:char配列経由/
メモリコピーでバイト単位コピー
unionは、同じメモリ領域を複数のメンバで共有するデータ型です。N1570 §6.7.2.1「Structure and union specifiers」に定義されています。組み込みでの代表例に、レジスタのビットフィールド表現があげられます。
C11 §6.7.2.1 に基づくunionの基本
unionは複数のメンバが定義されるものの、各メンバは同じ記憶域を共有します。どのメンバを介して書き込むかは、開発者が選びます。N1570 §6.7.2.1の記述に沿えば、書いた側のメンバから読み出すのが原則です。
last-written活性メンバとタイプパニングの落とし穴
最後に書いたメンバ以外を読むタイプパニング(type-punning)は、C++では未定義動作となります。C標準はunionの別メンバ読みをタイプパニングとして脚注で言及していますが、結果は処理系依存になり得ます。移植性を重視する場合は、memcpyやunsigned char配列経由のバイト列操作を優先してください。JPCERT/CC EXP39-C「適合しない型のポインタを介してオブジェクトにアクセスしない」でも、同じ論点が扱われています。
typedefで隠すべき型・隠さない型
typedefは、データ型にエイリアス(別名)を付けるための機能です。抽象化に便利な方法ですが、コードを読みやすくするためには抽象化する項目と明示する項目の検討が必要です。
ハンドラ型・時刻型の抽象化
Handle_tやTime_tのように、実装詳細から離れた抽象名を持つ型はtypedefと相性が良好です。N1570 §6.7.8「Type definitions」で、定義方法が解説されています。typedefの活用により、データ型を変更した際の修正もしやすくなります。
ポインタ・配列のtypedef隠蔽の弊害
一方で、typedef PtrFn_t が関数ポインタと通常ポインタのどちらなのかを、名前だけで判別することは困難な場合があります。特にポインタと配列に対してtypedefを用いると、宣言の複雑さを隠すことと引き換えにコードの可読性を下げるおそれがあります。typedefで新しい名称を付与する際には、元のデータ型を推測できる名称にするなどの配慮が必要です。
命名規則と可読性(代表的なマイコン向けコアライブラリ/HALに見られる命名パターン)
命名を組織的に決めておくと、初見の読者でも構造体の役割が推測できます。
レジスタ構造体と初期化構造体の命名パターン
標準化されたコアライブラリのペリフェラルアクセス規約は、レジスタの並びを表す構造体の型名に _Type または _TypeDef を付けると定めています。一部のマイコンベンダーHALでは、この規約に重ねて、初期化パラメータを渡す構造体に_InitTypeDef、ハンドル構造体に_HandleTypeDefを付けます。
標準コアライブラリのヘッダでの構造体命名
CMSIS仕様書のCoreセクションでは、CMSIS対応の32ビットマイコンのSCBやNVICといったレジスタ構造体の命名がベンダー横断でそろえられています。標準化された命名は、移植コストを下げるうえで役立ちます。
MISRA C:2012 のenum/union/typedef関連ルール
MISRA C:2012 はセーフティクリティカル領域で参照されるガイドラインです。列挙型と整数の暗黙変換、typedef の再宣言、union の使用可否といった論点にルールを置いています。
Rule 5.6・5.7(typedef名とタグ名の一意性)
MISRA C:2012 Rule 5.6 は、typedef名を一意の識別子とすることを求めます。Rule 5.7 はタグ名について、すべての名前空間において一意であることを求めています。区分はどちらもRequired(必要)です。
Rule 10.3(列挙型と整数)と Rule 19.2(unionの制限)
MISRA C:2012 Rule 10.3 は、式の値を、より狭い本質型やカテゴリの異なる本質型のオブジェクトへ代入することを禁じます。列挙型は整数型と別のカテゴリのため、規格上は通る暗黙変換がここで塞がれます。区分は Required(必要) です。union については Rule 19.2 が「union キーワードを使用すべきでない」と述べており、こちらは Advisory(推奨) です。
C++のenum class/usingとの対比
C++ への移行ができる案件では、enumはenum classへ、typedefはusingへ置き換えるのが定石です。C++ Core Guidelines(Bjarne Stroustrup, Herb Sutter編)の Enum.3「Prefer class enums over “plain” enums」は、通常のenumよりenum classを優先することを示しています。
C++ Core Guidelines Enum.3の考え方
Enum.3で示されるとおり、enum classはスコープを持ち、暗黙的なint型への変換もありません。C言語のenumで問題になっていた暗黙のint変換や、-fshort-enumsによるレイアウト変化といった副作用が、そもそも発生しにくい設計になっています。
typedef から using エイリアス宣言への移行
C++ Core Guidelines T.43「Prefer using over typedef for defining aliases」でも推奨されるとおり、using はtypedefと同様にエイリアスを設定できます。typedefでは非対応のテンプレート化にも対応します。
C言語の枠内でも、命名と規約の工夫で保守性を確保できます。もしC++が使える環境ならば、enum classやusingの活用は正確で保守しやすいコードの作成に有効です。


