Hatena::ブログ(Diary)

stacked tip that high

2017-02-24

定数定義にenumを使いたいが躊躇っている方へ

マクロを使って見た目を整えれば何とか。

#define DEFINE_CONST_INT(name,val) enum{name=val}

void test1( void )
{
    DEFINE_CONST_INT(foo,3);
}

こっちの方がまともかな?

#define DEFINE_CONST_INT(def) enum{def}

void test2( void )
{
    DEFINE_CONST_INT(bar=5);
}

流石にこっちは厳しいよね。

#define CONST_INT enum{
#define CONST_INT_END }

void test3( void )
{
    CONST_INT baz = 7 CONST_INT_END;
}

まぁ個人コーディングしている人ならいいかも知れんが、
流石にチームでコーディングしている中でこれを使うのは難しいかもね。

2017-02-11

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

さて、前回の続きです。

#define vs const

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

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

#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に軍配が挙がることになります。

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

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

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

結論

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

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

が結論です。

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

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

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

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

2017-02-05

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

C言語の定数の作り方

組み込みC言語を書いていると、定数をどう作ろうかと迷うことがあります。

最新のC言語では、定数の作り方は3つある*1。えっ?2つじゃないの?僕もそう思っていましたが、以下のサイトでは3つと紹介されています。言われてみれば、まぁ確かに。


  1. #define
  2. const
  3. enum

具体的には以下のように使います。

/* #defineを用いる場合 */
#define VAR 10

/* constを用いる場合 */
const int var = 10;

/* enumを用いる場合 */
enum{var = 10};

で、上記のサイトでは、プログラミング質問サイト「スタックオーバーフロー」のリンク先の書き込みを紹介しています。
c - "static const" vs "#define" vs "enum" - Stack Overflow

結論から言えば、

  • 多くの場合でenumによる定義が良い
  • アドレスを得たいのならconstしかない
  • プリプロセッサ(#ifなど)では#defineしか使えない

ということですが、
それでも先に紹介したサイトでは、これを知ってても、そのようにenumを使うことは無いだろうと締めくくっています。

僕の意見も全く同じで、
個人コーディングしているならまだよいが、チームでコーディングしている時に、

enum{var = 10};

なんてコードを見たら、
「なんだこれ?作りかけの列挙型かな?
 でもタグ名が無いから他で使用できないし、タグ名も考え中ってこと?
 それとも変数名を考え中?」
とか考えちゃうでしょう。余りにも標準的な書き方ではありません。
チームでのコーディングは、チームメンバーにわかってもらえないといけないので、
「動けばいい」ではダメなのです。

それぞれの特徴

さて、まだタイトルの「組み込み」に触れられていませんが、もう少しご辛抱を。
まずは、そもそもこの3つの定義について、その特徴を挙げていきます。
具体的には、それぞれがコンパイルの過程でどのように処理されるのかを挙げ、
そこから、各々が出来ることと出来ないことを挙げていきます。

#define

#defineはプリプロセッサで処理されるので、コンパイルの時点では既に具体的な値に置換されています。

#define VAR 10

main(){
    int var = VAR;
}

というコードは、コンパイルの時点では

main(){
    int var = 10;
}

となって、VARはコード上から消えています。
コンパイルの時点で消えているので、

  • シンボルが生成されず、デバッグの時に追いかけることが出来ない。
  • メモリ上に配置されないので、容量を食わない
  • メモリ上に配置しないので、勿論アドレスはない

という特徴があります。
また、プリプロセッサでは単純に置換が行われるだけなので、

  • C言語として正しいか否かはチェックされない
  • 演算順序が予期したものと異なるものになることがある

という弱点もあります。
前者でエラーが発生しても、先の通りデバッグで追いかけることが出来ないので、
エラーの解決に時間がかかることがあります(慣れてくるとエラーメッセージで何となく察することができますが)し、
後者の場合はエラーも吐いてくれないので、挙動がおかしい時にその原因を突き止めるのに苦労することになります。
また、

  • スコープの概念がない

ので、一度#defineで定義された定数は、関数を越えて有効です。
万一同名の定数が異なる関数で定義されていると、エラーになります。

そんな理由もあり、プリプロセッサに対する処理(#ifの条件式に使用するなど)以外では、
#defineは使わないようにしましょう
というのが、半ばC言語業界の常識になっていると言っても過言ではないと僕は思っています。

const

(無用な混乱を避けるため、ここではポインタに対するconstのお話はしません。)
さて、一方でこのconstですが、
C言語においてconstは、
「この値は書き換えられない前提で、この修飾子が付いた変数は定義と同時に値を代入しなければいけないもの」
という意味を、コンパイラに(そして、コードを読む他人にも)伝える意味があります。
なので、その意味に反するコーディングされていると、コンパイラは「親切に」エラーを吐いてくれます。
「親切に」と言ったのは、コンパイラとしては、別にそのコーディングで困っているわけではないからです。

int i;
for(i=0;i<10){
    printf("hello!\n");
}

というコードをコンパイラが見たら、
「for文の式が足りないけど、どうするの?困るんだけど?」
となりエラーを吐きます。
一方、

const int var = 5;
var = 10;

というコードをコンパイラが見ると、
「この変数constがついているんだけど、定義の後で代入してるよ。間違ってない?」
となりエラーを吐いてくれます。
実はC言語では、ポインタ経由で操作することでconstの値を定義後に変えることが出来ます。

const int var = 5;
int *p;
printf("%d\n", var);
p = &var;
*p = 10;
printf("%d\n", var);

とすると、最初の出力では5が出てきますが、2回目の出力では10が出てきます。
このように、constはあくまでも「これは定数(のつもり)ですよ」ということをコンパイラに知らせているだけなので、

const int var1 = 5;
int var2 = var1;

int var1 = 5;
int var2 = var1;

コンパイル結果としては同じなのです。

つまり、const intで定義された変数は、intで定義された変数と(原則的には)同じように扱われます。

  • コンパイル時にシンボルが生成されるので、デバッグ時に定数名が使用できる
  • メモリ上に配置されるので、定数のアドレスが存在する
  • メモリ上に配置されるので、勿論メモリの容量を食う
  • C言語の修飾子なので、文法チェックや型のチェックもしてくれる

但し、constで定義された定数がコード上で一切アドレスを参照されていない場合は、
コンパイラが気を利かせて、#defineで書いたときのように定数を具体値に置換してくれることもあります。
(これはコンパイラ最適化レベルの設定次第なので、必ず置換してくれるわけではないことに注意して下さい)

ところで、const intはいわば「定数のふりをしたint」なので、昔は

という弱点がありました。ですので、配列定義時は必ず#defineを使う必要がありました。

#define LEN 5
int array[LEN];

C99規格で「配列の要素数変数を使用することができる」と変更されたので、最新のC言語では

const int len = 5;
int array[len];

と書くことが出来ますし、

#include <stdio.h>
 
size_t fsize3(int n) {
  char b[n + 3];
  // ……
  return sizeof b;
}
 
int main(int argc, char **argv) {
  printf("%ld\n", fsize3(10));
}    

ということも出来るようになりました。
(コード出典:C言語の最新事情を知る: C99の仕様 - Build Insider

enum

列挙型、所謂enum型は、文字通り「項目を列挙するため」の型です。
列挙型は初期のC言語規格には存在せず、C89で初めて規格化されました。
それまでは#defineを使って列挙型を代用していました。

#define ALICE 0
#define BOB 1

void main(){
    int name = ALICE;
    if(name==ALICE){
        printf("My name is Alice!");
    }else{
        printf("My name is Bob!");
    }
}

前回の記事で#defineについて書いたとおり、このコードはプリプロセッサによって以下のように変換されます。

void main(){
    int name = 0;
    if(name==0){
        printf("My name is Alice!");
    }else{
        printf("My name is Bob!");
    }
}

一方、enumを用いるとコードはこうなります。

void main(){
    enum{Alice, Bob} name = Alice;
    if(name==Alice){
        printf("My name is Alice!");
    }else{
        printf("My name is Bob!");

    }
}

enumは#defineとは異なり、プリプロセッサで置き換えられる事はありません。
プリプロセッサでは置き換えられませんが、コンパイラで置き換えられることになります。
なので、enumの特徴は#defineとconstを足して2で割ったような感じになります。

  • コンパイル時にシンボルが生成されるので、デバッグ時に定数名が使用できる
  • メモリ上には配置されないので、定数のアドレスは存在しない
  • メモリ上に配置されないので、メモリの容量は食わない
  • スコープの概念がある
  • C言語の修飾子なので、文法チェックや型のチェックもしてくれる

但し、最後の項目については、コンパイラによってはエラーを吐いてくれないことがあります。

また、const intは「定数のふりをした変数」でしたが、enumは「れっきとした定数」なので、
switch-case文に使用することができます。

void main(){
    enum {east, west, south, north} direction;
    direction = east;
    switch( direction ){
        case east:
            printf("East!\n");
            break;
        case west:
            printf("West!\n");
            break;
        case south:
            printf("South!\n");
            break;
        case north:
            printf("North!\n");
            break;
    }
}

更に、enumを使うと連想配列みたいなものを作ることも出来ます。

void main(){
    enum {east, west, south, north} direction;    /* 方角 */
    int wind_power[4];    /* 風の強さ */
   wind_power[east] = 3;     /* 東風の強さ */
   wind_power[west] = 2;     /* 西風の強さ */
   wind_power[south] = 4;    /* 南風の強さ */
   wind_power[north] = 1;    /* 北風の強さ */
}

配列インデックス整数で指定しますが、
enum型はint型と互換性があるので、上記のように使うことも出来るのです。

(記事が長くなってしまったので、続きはまた今度)

*1:規格としては、1978年のK&Rでは#defineしかなかったが、1989年のC89でconst修飾子とenum型が導入されました。

2016-05-16

PIC用nop関数

#define JOIN(x, y) x ## y
#define nop() asm("nop")
#define NOP1 nop()
#define NOP2 NOP1;nop()
#define NOP3 NOP2;nop()

/* 中略 */

#define NOP100 NOP99;nop()

#define NOP(n) JOIN(NOP, n)

NOP(10); /* これでnop10回分になる */

このコードの良いところは、__delay()関数シリーズより正確(本当にnopを並べているだけだから)という点です。
いちいちNOP1から1つずつ定義する必要がありますが
マクロ関数なので再帰関数は作れない)、
必要なものだけ作ってもいいので、その辺は臨機応変に。
例えば、NOP30とNOP40だけほしいのなら、

#define JOIN(x, y) x ## y
#define nop() asm("nop")
#define NOP5 nop();nop();nop();nop();nop()
#define NOP10 NOP5;NOP5
#define NOP30 NOP10;NOP10;NOP10;
#define NOP40 NOP30;NOP10

#define NOP(n) JOIN(NOP, n)

とやればOKです。