組み込みC言語における定数の作り方について(後半)

さて、前回の続きです。

#define vs const

ここからは、定義の方法によってどういうメリット・デメリットがあるかをまとめてみます。
まずは#defineとconstです。
#defineとconstを比べるという状況は、つまり定数を定義したいときです。
原則としては、やはりconstを使うべきでしょう。

#define const
型チェック ×
スコープ ×
予期せぬ演算防止 ×
プリプロセッサでの使用 ×
メモリ節約 ×
定数の確実性

#defineにできることのほとんどはconstでできますし、constは#defineではできないことをやってくれます。
#defineのマイナス面をconstは補っていますし、多くの面でconstが勝っています。
メモリの消費量は普通は気にしませんし、定数の確実性もよっぽど変なコーディングをしなければ問題になりません。
唯一、プリプロセッサでは#defineしか使えないので、そのときだけ#defineを使うようにすればいいのです。

但し、組み込みC言語の場合は、単純にそうは行きません。
組み込みC言語では、メモリ使用量が制限されています。変数1つ1つのサイズを気にしながらコーディングを行います。
そのような環境下では、手放しに「定数にはconstを使え」とも言えないのです。
とは言え、よっぽど定数の数が多くない限りは、組み込みC言語と言えどもやはりconstで十分です。
マイコンでメモリ使用量が逼迫するのは主にプログラム領域ですが、constなどの変数が記録されるのはデータ領域です。
マイコンの多くが、プログラム領域とデータ領域を分離しているハーバード・アーキテクチャーを採用しているので、
constを#defineに置き換えたところで、書けるプログラムが多くなるわけではありません。

結論としては、
組み込みC言語であっても、プリプロセッサでのみ#defineを使い、そうでない定数はconstを使うのが良い、となります。
(2017/2/20修正ここから)
マイコンの種類によって、このあたりは異なるようです。
また記事を改めて検証します。
(2017/2/20修正ここまで)

#define vs enum

#defineとenumではどうでしょう。
#defineとenumで比較する状況は、項目を列挙したい時です。
こちらでも、やはりenumに軍配が挙がることになります。

#define enum
項目への数値の割り振り ×
型チェック ×
スコープ ×
予期せぬ演算防止
プリプロセッサでの使用 ×
メモリ節約
定数の確実性

enumの便利なところは、項目にわざわざ数値を割り振る必要がないところです。
#defineで項目を列挙するには、全てに数値を割り振らなければなりません。

#define red 0
#define yellow 1
#define green 2
#define blue 3

これの欠点は主に2つあります。
ひとつは、この項目に順序がある場合、途中に項目を入れると数値を振り直さないといけないのです。
例えば上記の例で、redとyellowの間にorangeを入れようとしたら、

#define red 0
#define orange 1
#define yellow 2
#define green 3
#define blue 4

と、yellow以降の数字を振り直す必要があります。
これが、enumの場合は

enum color_e{
    red,
    orange,
    yellow,
    green,
    blue
}

と、数字を振る必要がないので、好きに項目を追加・削除することが出来ます。
もうひとつは、#defineで項目を列挙すると、それが項目を列挙しているのか、定数の定義をしているのかわからないという欠点です。

/* ホテル予約管理システム */
#define cheap 0        /* 安い */
#define normal 1       /* 普通 */
#define expensive 2  /* 高い */
int type;     /* 部屋のタイプ */
int point;    /* ポイント */

と書かれたコードがあるとします。
さて、#defineで定義された3つの定数は、どちらの変数のために使う定数なのでしょう。
もしかしたら、予約した部屋のタイプに使う項目として定義しているのかもしれません。その場合、数値には意味はありません。
でももしかしたら、予約に伴い加算されるポイントの点数として定義しているのかもしれません。その場合、数値には意味があることになります。
このように、#defineで定義された定数は、それが定数なのか項目なのか、そこだけ見てもわからないという欠点があります。
このような場合、#defineで宣言する定数の名前を工夫することで対処するという方法もありますが、それをチェックするのはコーディングしている人間なので、間違う可能性があります。
この点、enumの場合は、それ専用の型として定義してしまうので、うっかり間違うことは避けられます。

enum room_type_e {
    cheap,
    normal,
    expensive
}
enum room_type_e room_type; /* 部屋のタイプ */
int point;    /* ポイント */

と書かれていれば、enumで定義されたcheap, normal, expensiveは、部屋のタイプのための定数であることが明白になります。
規格上は、cheap=0、normal=1, expensive=2なので、pointに代入することもできますが、
少なくとも、コードを見て迷ったり勘違いをすることはなくなります。

結論は先程と似ていて、
プリプロセッサでのみ#defineを使い、そうでない列挙はenumを使うのが良い、となります。

const vs enum

残る組み合わせはconstとenumですが、
これはそもそも用途が異なるので、比較するべきものではないというのが結論です。
勿論、先に紹介したリンク先のように、enumをconstの様に使うことはできます。
できますが、これはコーディングした人以外には誤解しか与えない記法です。
やはり、
定数にはconstを、
列挙にはenumを、
それぞれ使うべきです。

結論

タイトルに「組み込みC言語の〜」なんて書きましたが、結論は組み込みに限らず最新のC言語での結論と同じになってしまいました。

(2017/2/20修正ここから)
但し、プログラム領域が逼迫している場合に定数を定義する場合は、#defineも検討対象となる。
(2017/2/20修正ここまで)

が結論です。

組み込みC言語業界特有の文化

まぁ、以下は駄話ですが、
組み込みC言語業界って、C言語業界の中でも結構保守的で、
それは、PCソフトと違って、一度世の中に出してしまうと組み込みソフトは簡単にバグ修正ができないので、
(最悪製品を回収することになり、会社にとっては年度の利益が吹っ飛んだり、最悪倒産したりします)
過去に大丈夫だったコードを使って新しいプログラムを作っていることが多いのです。
(こういうのを「実績があるコードを流用する」って言ったりします)
過去に使ったコードを使うので、中々開発環境を最新にすることが出来ず、
結局、21世紀になった今でも、コーディングルールはC89準拠とかだったりするわけですね。

ここまでで紹介してきたように、
最初期のC言語では、定数の定義方法は#defineしかありませんでした。
なので、今でも組み込みC言語業界では、「伝統的に」定数を#defineで定義する人が多いのです。
しかし、#defineには色々は欠点があります。
ですので、コーディングルールで禁止されていない限りは、
やはりconstやenumを使うようにしていきましょう。

僕はこっそり#defineをconstやenumリファクタリングしたりしています。
そういう行動が、やがて組み込みC言語コードから危険な#defineを駆逐してくれると信じて。