はじめに
組み込みのコードを読んでいると、#define だらけで最初は面食らうことがあります。
#define T_TICK 250
#define H8P4DR 0xffffd3
#define sil_reb_mem(mem) (*((volatile UB *)(mem)))
定数の定義もあれば、アドレスの定義もある。さらに関数のように見えるものまで。「全部 #define でいいのか?」と疑問に思うかもしれませんが、それぞれ使い所と意図が違います。
今回はマクロの使い方を整理しつつ、似た役割を持つインライン関数との使い分けまで見ていきます。前回登場した sil_reb_mem のようなパターンも、ここで腑に落ちるはずです。
定数の定義 ― マジックナンバーを追放する
コードの中に突然現れる数値、いわゆるマジックナンバーは保守性の敵です。
/* マジックナンバーだらけのコード */
base_time += 250;
if (base_time >= 1000) {
base_time = 0;
}
この 250 や 1000 が何を意味するのか、書いた本人以外にはわかりません。マクロで名前を付けることで意図が明確になります。
#define T_TICK 250 /* 実行周期 250ms */
#define T_1SEC 1000 /* タイマー上の1秒 */
base_time += T_TICK;
if (base_time >= T_1SEC) {
base_time = 0;
}
マクロはプリプロセッサによってコンパイル前に文字列置換されるので、実行時のオーバーヘッドはゼロです。また、値を変更したいときも #define の一か所を直すだけで済みます。
const 変数でも同じことはできますが、組み込みでは const がROMに配置されるかどうかがコンパイラや設定に依存するケースがあります。マクロであればコンパイル時に展開されるため、そういった不確実性がありません。プロジェクトの規約に従いつつ、どちらを使うか判断してください。
ハードウェア依存定数(アドレス)の定義 ― HW依存をヘッダに閉じ込める
組み込みではレジスタやI/Oポートのアドレスを直接扱うことがあります。これもマクロで名前を付けておくのが定石です。
/* port4: D0 - D7 */
#define H8P4DDR 0xfee003 /* データディレクションレジスタ */
#define H8P4DR 0xffffd3 /* データレジスタ */
#define H8P4PCR 0xfee03e /* ポートコントロールレジスタ */
生のアドレスをコード中に直接書くと、ハードウェアが変わったときに全ファイルを検索して直す羽目になります。マクロでアドレスに名前を付けてヘッダファイルに集約しておけば、変更箇所が一か所に収まります。
また、名前が付いていることでコードの意図も読み取りやすくなります。
/* 悪い例 */
*((volatile UB *)0xffffd3) = 0x80;
/* 良い例 */
*((volatile UB *)H8P4DR) = 0x80;
ハードウェア依存の定数はできる限りヘッダファイルに集め、ソースコードからは名前でしかアクセスしない、という設計にしておくと移植性が格段に上がります。
引数付きマクロ ― 関数のように書けるが関数ではない
マクロには引数を持たせることができます。前回も登場した sil_reb_mem はその典型例です。
#define sil_reb_mem(mem) (*((volatile UB *)(mem)))
#define sil_wrb_mem(mem, data) (*((volatile UB *)(mem)) = (data))
使う側はこう書けます。
UB p4 = sil_reb_mem(H8P4DR);
sil_wrb_mem(H8P4DR, p4);
プリプロセッサによって展開されると、こうなります。
UB p4 = (*((volatile UB *)(H8P4DR)));
(*((volatile UB *)(H8P4DR))) = p4;
毎回キャストを書かずに済み、コードが読みやすくなります。また関数呼び出しと違い、プリプロセッサによるテキスト置換なので実行時のオーバーヘッドがありません。
ただし引数付きマクロには落とし穴があります。
副作用のある式を引数に渡すと意図しない動作になる
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int x = 3, y = 5;
int m = MAX(x++, y++); /* x++ と y++ がそれぞれ2回評価される */
MAX(x++, y++) は ((x++) > (y++) ? (x++) : (y++)) に展開され、勝った方の引数が2回インクリメントされます。引数に副作用のある式(インクリメントや関数呼び出しなど)を渡すのは避けてください。
括弧を省略するとオペレータの優先順位で意図しない結果になる
/* 悪い例 */
#define DOUBLE(x) x * 2
int b = DOUBLE(1 + 2); /* 1 + 2 * 2 = 5 になってしまう */
/* 良い例 */
#define DOUBLE(x) ((x) * 2)
int b = DOUBLE(1 + 2); /* (1 + 2) * 2 = 6 */
引数と式全体を必ず括弧で囲む習慣をつけてください。
インライン関数 ― 引数付きマクロの代替と使い分け
引数付きマクロの落とし穴を避けつつ、同じようにオーバーヘッドなしで使えるのがインライン関数です。
Inline UB sil_reb_mem(VP mem)
{
return(*((volatile UB *) mem));
}
Inline void sil_wrb_mem(VP mem, UB data)
{
*((volatile UB *) mem) = data;
}
Inline を付けて宣言すると、関数呼び出しのところに関数の中身が展開されます。引数付きマクロと同じ動作ですが、通常の関数と同じ文法で書けるので副作用や括弧の問題が起きません。型チェックもコンパイラが行ってくれます。
引数付きマクロとインライン関数の使い分けは次のように考えると整理しやすいです。
| 引数付きマクロ | インライン関数 | |
|---|---|---|
| 型チェック | なし | あり |
| 副作用の危険 | あり | なし |
| 複数行の処理 | 書きにくい | 書きやすい |
stdint.h不要環境 |
使える | コンパイラ依存 |
シンプルな1行の置換であれば引数付きマクロでも問題ありませんが、少し複雑な処理や型安全性を重視する場面ではインライン関数を選ぶのが無難です。なお、ここで使っている Inline(大文字)はTOPPERS RTOSのシステムヘッダで定義された環境固有のマクロ(static __inline 等に展開)であり、標準Cの inline キーワードそのものではありません。標準C99では static inline と書きます。プロジェクトの環境を確認してください。
実務での注意点・よくある落とし穴
マクロのデバッグはしにくい
マクロはプリプロセッサによる単純な文字列置換なので、デバッガでステップ実行しても展開前のマクロ名しか見えないことがあります。バグの原因を追うときに苦労しやすいので、複雑なロジックをマクロに詰め込むのは避けた方が無難です。
引数付きマクロの名前は関数と区別しにくい
使う側のコードだけ見ると、引数付きマクロか関数かの区別がつきません。プロジェクトによっては大文字で書く、名前にプレフィックスを付けるなどの命名規則を設けているところもあります。チームの規約に従ってください。
インライン展開されるかどうかはコンパイラ次第
inline キーワードはあくまでコンパイラへのヒントであり、必ずインライン展開されるとは限りません。最適化レベルや関数の複雑さによっては通常の関数呼び出しになることもあります。パフォーマンスがクリティカルな箇所では、コンパイラの出力(アセンブリ)を確認する習慣をつけておくとよいです。
まとめ
マクロは組み込みCで非常に多用されますが、用途によって使い方が異なります。
- 定数の定義 ― マジックナンバーを追放し、変更箇所を一か所に集約する
- アドレスの定義 ― HW依存をヘッダに閉じ込め、移植性を上げる
- 引数付きマクロ ― 関数のように書けるが、副作用と括弧には注意
- インライン関数 ― 型安全で副作用の心配がない、引数付きマクロの代替
次回はヘッダファイルの設計と条件付きコンパイルを取り上げます。今回登場したマクロやインライン関数をどのファイルに書くべきか、という話にもつながってきます。