スポンサーサイト

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

printfとtypedefの微妙な関係 - 無視されがち?なformt warning

型ができていない者が芝居をすると型なしになる。メチャクチャだ。
型がしっかりした奴がオリジナリティを押し出せば型破りになれる。どうだ、わかるか?
立川談志 - 『赤めだか』より

ひさびさにコーディングの話を書きたいと思います。組み込みの現場なんかで結構よく見かける話かと思いますが、以下のようなコードに対してでるwarningのお話です。

printfとtypdefの微妙な関係

ケースとしては、int等の組み込み型が環境依存なのを嫌って、typedefでint32_tやINT32という型におきかえることがあります。例えば以下のコード。
typedef long INT32;
int
main(int argc, char* argv[])
{
  INT32 val = 1;
  printf("val=%d\n", val);
  return 0;
}
さて、これをコンパイルすると、例えばgccなら以下のようなwarningがでます。(-Wallや-Wformatがある場合)
$ gcc -o test.c
test.c:9:22: warning: format specifies type 'int' but the argument has type
      'INT32' (aka 'long') [-Wformat]
  printf("val=%d\n", val);
-Werrorがついていればエラーになりますが、そうでない場合は、 実害なし、というか、別にあってるよね、くらいで無視されがちな気がしています。

実際のところ一般的な32bit処理系であれば、intとlongはどちらも符号付きの32bit整数型なので、 数値計算等で使っている範囲では全く同じなのですが、それでもwarningは出ます。なぜでしょうか?

INT32のようなtypedefを定義する理由は処理系依存の型を直接使わずに、 移植性を高めるために使われるものです。
(上の例なら、longが64bitになる64bitの処理系では、intへのtypedefに変えるなどする) 一方で、printfの%dは、そもそも処理系依存の型であるintを期待するものなので、 そこはintでなければ、処理系によっては不一致を起こすので、32bit系であろうと%dにはlongを渡す必要があるのです。

では、どうですればいいのでしょう?
上の例であれば、%dの代わりに%ld(signed long指定)を用いればwarningはなくなります。あるいは、引数側でintへのcastをしてやれば同じく問題はなくなります。
typedef long INT32;
int
main(int argc, char* argv[])
{
  INT32 val = 1;
  printf("val=%d\n", (int)val);
  return 0;
}
個人的には、printfのようなログ出力は処理系依存なので、引数の型の方にcastする上記やり方がスマートかと思います。 もちろんログなどではなくてファイルフォーマットにかかわるもので、移植時の型サイズ維持が重要なものはformat string側をかえるのが筋がいいものもあるとは思います。

「移植性をあげる」という目的で、処理系依存型を非依存型に変換するのはよく見かけるコードですが、一方で、非依存型から処理系依存型への変換というのは結構ないがしろにされがちです。strcmp等のstring系の関数でchar型を期待している関数を使う時も同じようなことがままあります。

なお、size_t, ssize_tについては、C99から専用のformat指定子zが定義されていますのでそれを使いましょう。(size_tから%zu、ssize_tなら%zd)

無視して良いwarning?

unsed-variablesとformatのwarningは無視されがちなwarningの2大巨頭ではないかと思います。どちらも大抵の場合は、確かに実害はないのですが、ごくまれにバグのものもあります。例えば以下のコード。
const char* name="taro";
printf("%d: name=%s\n", name, strlen(name)); 
引数順間違って%sに数値渡しているのでbuffer overrunをひきおこします。
ここで言いたいのは、「実害なし」という名目でwarningを残したり、 あえて、-Wno-formatのようにwarning抑制したりすることは、こういう本当にバグを埋もれさせかねないので、 基本的にはwarningは全て除去するというのは基本ポリシーとして実施すべきかと思います。

なお、デバッグ用にprintfを自作のマクロや関数でラップすることがありますが、 何もしないとprintfのformatチェックが機能しないことになり、 warning無視と同じくバグに気づくチャンスを逃すことになりかねません。 対策としては、例えばgccでは、独自定義マクロでもformatチェックするようにattributeしていることで コンパイラに指定する機構がありますので、あわせて活用することをおすすめします。
以下はgccの例です。
void debug_printf(const char* format, ...) __attribute__ ((format(printf, 1, 2)\
));

void debug_printf(const char* format, ...)
{
  va_list arg;
  va_start(arg, format);
  vprintf(format, arg);
  vfprintf(stderr, format, arg);
  va_end(arg);
}
上の例だと、__attribute__ formatという属性のを関数に付与することでformatチェックが入るようになります。引数の意味は、関数タイプ(この例だとprintf)、format文字列の位置(この例では1)、最初の引数の位置(この例では2)になっています。詳細は、以下gccのmanualが詳しいです。 ようするに、warningは正しく活用しましょうということですね。

なお、デバッグ出力用の可変個引数マクロやwarningの話は随分前にもとりあげていたので、よろしければご参考までに。

【関連記事】
可変個引数マクロを使う
warningに気を配る
使用しない仮引数

スポンサーサイト

バグは誰のものか - エラー処理の責務分担

まかせてはいるけれども、たえず頭の中で気になっている。そこでときに報告を求め、問題がある場合には、適切な助言や指示をしていく。それが経営者のあるべき姿だと思います。これは言いかえますと“まかせてまかせず”ということになると思います。まかせてまかせずというのは、文字どおり“まかせた”のであって、決して放り出したのではないということです。

前回単体テストに関する話を書いたのでその続きで。

単体テストという言葉の定義はプロジェクトによって様々ですが、もっとも多いのは”関数”を単位とするものかと思います。関数単位のテストでは正常系のテスト(関数が適切に使われた時のテスト)以外に、異常値を与えられた時のテストも行われます。例えば以下の例。
const char*
month_name(int month)
{
  const char* name[] = {"Jan", "Feb", "Mar", "Apl", "May", "Jun",
                        "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};

  if (month < 1 || month >= 12} 
    return NULL; /* invalid month */
  return name[month - 1];
}
月を文字列に変換する関数ですが、1〜12以外はエラーとしてNULL(0)を返しています。
このような関数の単体テストでは、1〜12の値以外に、エラー値として、0や-1, 13等も異常値としてテストすることになると思います。

このように、多くの関数では、引数として取りうる値や条件が限られている場合が多く、それを満たさない場合は、エラーを返すというのはよく見られることです。しかし、このようなエラー処理は、毎回必要なことでしょうか。

例えば、この関数が以下のような関数の内部で使われている場合を考えます。
int
print_date(int month, int day)
{
  if (month < 1 || month >= 12} 
    return -1;
  .....
  printf("%s-%d\n", month_name(month), day); 
}
先ほどと同じ範囲チェックがここでもされています。もし、month_name()がここでしか使われない、つまり、この関数の専用内部関数なのであれば、month_name()内の範囲チェックは冗長です。言い換えると、month_name()内のエラーチェックは内部のバグでもない限り発生することはありません。そもそも、print_date()では、month_name()でエラー、つまり、NULLがかえってくることを想定していないので、万が一NULLが返ってきたらNULL pointerアクセスでsegmentation fault等を引き起こします。

非常に単純な例ではありますが、このようなケースでは、month_name()でのエラー処理は不要で、必要なのはアサーションになります。つまり、以下のような形です。
const char*
month_name(int month)
{
  const char* name[] = {"Jan", "Feb", "Mar", "Apl", "May", "Jun",
                        "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
  assert(month > 0 && month < 12)
  return name[month - 1];
}
こう書くことで、この関数は前提条件として、引数に1から12しかとらないということが表現されます。範囲チェックをさぼっているのではなく「上位にまかせている」ということがこれではっきりします。アサーションであれば、処理されないようなエラーを返すコードもないので、そもそも単体テストも必要ありません。万が一、上位関数のロジックが間違っていれば、assertにひっかけて早期検出も可能です。
よく混同されがちですが、アサーションはエラー処理の一種ではないのです。

じゃあ、どうやって使い分けるの?という話になりますが、必ずしも一概には言いきれないものの、一般論として言えば、外部要因によって値が不正になるような場合(例えば、ライブラリの外部公開関数や、データが外部ファイルやネットワーク、ユーザー入力に由来するなど)には、エラー処理が必要になりますが、内部関数の場合は、大抵はエラー処理ではなく、アサーションが適当な場合が多くなります。

よく、エラー処理とアサーションが混同されているコードに以下のようなものがあります。
int
print_date(int month, int day)
{
  if (month < 1 || month >= 12) {
    assert(0);
    return -1;
  }
  .....
  printf("%s-%d\n", month_name(month), day); 
}
エラー処理の中に、assert(0);が書かれており、どっちつかずになってしまっています。但し、この関数が通常外部関数で使われているものであり、デバッグ用にエラー発生次第abortするという機能として、コンパイルオプション等で有効無効が設定できるようなアサーションなのであれば、それはデバッグを効率化するひとつの方法だと思います。

ここまでの例では、引数チェックの話を例にとりましたが、戻り値のチェックでも同じことが言えます。
  ....
  ercd = loc_mtx(MTX1); /* lock mutex */
  assert(ercd == E_OK);
  ....
mutexの取得失敗をassertで確認しています。通常、mutexの取得はパラメータエラーや、同一タスクでの二重取得等の内部ロジックの不具合でしか起こらないことが多く、ここでエラーを返しても上位関数ではどうすることもできないので、このような場合は、アサーションが適当です。(失敗するはずはないという表明になる)

もし、これを以下のように書こともできますが、結果としての処理は同じでも、コードとしては通りもしないエラー処理を書くことになり、C0やC1カバレッジを必要とするような場合には、困ることになります (通らないはずのコードなのだからパスを通しようがない)。
  ....
  ercd = loc_mtx(MTX1); /* lock mutex */
  if (ercd != E_OK) {
    printf("loc_mtx failed : %d\n", ercd);
    abort();
    ....

非常に単純な話をくどくどと書いてしまいましたが、各モジュールや関数の責務をはっきりさせ、効率的にテスト、デバッグする上で結構重要な観点だと思います。
B00JEYPPOE
【関連記事】
【関連書籍】

[C言語] 配列の添字に負の値は使えるか

今は終わりではない。これは終わりの始まりですらない。しかしあるいは、始まりの終わりかもしれない。

C言語で配列の添字に負の値を使うのは有りでしょうか。つまり以下のようなコードです。
  a[-1] = 0;
C言語では、配列int a[n]は、*((int*)(((int*)a + n))と等しいということなので、aが配列名、つまり、
  int a[10]
のように定義されたものなら、それは範囲外アクセスになってしまいます。
しかし、
  int b[10]
  int* a = &b[1];
のようなものであれば、*(a-1) = b[0]なので、問題なく動作します。

ということで、一応、言語的には使えなくもない負の添字ですが、不用意に使わない方が良いです。
例えば、以下のように有効な配列の範囲外を指すような使い方は、たとえ値参照しなくても使うべきではありません。
  int b[10]
  int* c = &b[-1];
C言語的には、ポインター 演算は、一度に割り振られた領域と、仮想的な"終端"を越えた1つめ の要素にだけ定義されていて、それ以外では未定義になります。よって、aが有効なアドレス空間の先頭に位置していた場合などに、a[-1]が有効なアドレスになるとは限らないということがありえます。
また、負の添字以前に、配列の添字に符号付き(signed)の型を使うのは、気づきにくい不具合を生む可能性があるという話もあります。例えば、次のようなコード。
#define ARRAY_SIZE 200
int my_array[ARRAY_SIZE];

int array(int n)
{
    if (n >= ARRAY_SIZE) {
        return -1;
    }
    return my_array[n];
}

単純なエラーチェック付きアクセサですが、これまた単純な配慮漏れがあります。
そう、nが負(マイナス値)の時の配慮が入っていないのです。

そのため、nがマイナスで渡されると、エラーチェックをすり抜けて、範囲外アクセスが発生します。この例では参照のみですが、値更新するような関数なら、任意の場所が書き換えられてしまうようなバグです。

さらに、あぶないのは以下のようなコード。
#define ARRAY_SIZE 200
int my_array[ARRAY_SIZE];

int array(int n)
{
    if (n >= ARRAY_SIZE) {
        return -1;
    }
    return my_array[n];
}

int func()
{
    char n = 150;
    printf("%d\n", array(n));
}
今後は、nがchar型になっています。char型は処理系によって、符号付きの場合と、符号なしの場合がありますが、符号ありの場合、150という値は保持されず、負の値になってしまいます。よって、負数を意図して使おうとしているわけではないものの、結果的に負数を使ってしまっています。
gccでは配列添字にcharを使っていると警告されるオプション(-Wchar-subscripts)があります。
charの暗黙の型変換は、いろんなバグの温床になるので、そもそもサイズのような変数の型にcharを使うのは、配列の添字に限らずやめるべきです。この手の変数については、符号なし型、できればsize_t(C言語ならstdlib.hで定義)を使うのが適切です。最新のC言語仕様であるC11の処理系であれば、オブジェクトの最大値を表す型としてrsize_tも使えます。


以上、C言語における配列の添字に負の値、あるいは、符号型を使った時の動作とリスクのお話でした。
この手の話は制約を理解して使えばいいという見方もあるかもしれませんが、ソフトウェア開発の現場等では、あとで別の人がメンテナンスする時の罠になりかねません。実際の現場では、あまりトリッキーなコードを書くのは控えておくのが望ましいですね。

4048919873 【関連記事】
【関連書籍】

静的な型チェックを強化する - 命名規則の活用

間違ったコードが間違って見えるコーディング規則を探すこと。
Joel Spolsky
プログラミングにおける変数や関数、クラス設計の命名というのは、そのプログラムの可読性を左右する最も重要な要素のひとつです。変数名や関数名が適切であれば、プログラムの流れも読みやすいですし、バグがあっても見つけやすいものです。
さて、このような命名規則(命名規約)といったものは、大抵それ読む人間のために設定されます。コンパイラにとっては、変数名がoldUserNameだろうがounだろうがxだろうが、関係ありません。変数名は単なる記号です。

しかし、考えて見るとこれは非常にもったいない。人間が、変数名からエラーを発見できるということは、このルールを(広い意味での)コンパイラに教えてあげれば、同じようにエラーを発見してもらえるのではないでしょうか。

例えば以前に次のようなバグコードを見たことがあります。
  ....
  len = strlen(name);
  strncpy(buf, name, sizeof(len));
  ....
ひとめで何がおかしいか分かるでしょうか。答えは、strncpyにsizeof(len)を渡しているのが間違い。sizeof(buf)のような与え方をすることも多いので、うっかり間違えたのでしょう。このようなコードは人間がよく見ればわかりますが、コンパイラにとっては何が間違いかはわかりません。C言語上の型チェックとしては、どちらもsize_tが渡るので問題ないと解釈されてしまいます。よって、この問題をコンパイラがコンパイル時に(静的に)発見することはできません。

ということで対策案。上の例でいえば、いたって単純な方法が考えられます。まず、lengthのようなものをsizeofでサイズ取得するというのは、あまりなさそうです。よって、sizeof(len.*)というパターンをソースでgrepしてみると。ものすごくピンポイントな対応ですが、このパターンでみつけられバグは結構ありそうに思います。

4894716860 ようは、命名規則がきまっていれば、単なる記号論理だけでなく、意味論までふみこんだ静的解析ができるんじゃなかろうかと。アプリケーションハンガリアンと呼ばれる変数の意味的な側面に注目してプレフィックス等の命名ルール呼ばれる命名規則を決める手法(参考:間違ったコードは間違って見えるようにする - Joel on Software)がありますが、これも人間に臭うコードが見えるようにするというだけではもったいない。ある程度ルールを決めれば、コンパイル時点でCPUに検出させることもできるのではないかと思うのです。

例えば、次のようなものもうまくやれば検出できそうです。
/* database中のoldをnewに置き換え */
void replace(DB* db, const char* old, const char* new);
...
  replace(db, new, old);
  ....
プロトタイプでは、newもoldも同じ型ですが、プロトタイプの変数名を見ればold->newの順番だとわかります。ところが、通常のコンパイラでは、上のようにnewとoldがいれかわっていても検出することはできません。おそらく人間でも直感的には間違いに気が付きにくいと思いますが、これもせっかくの仮引数名を利用してやれば、何とか検出できそうです。

意味論の解釈というのは難しいとはいえ、発想自体は単純な話ではあるので、すでにいろいろツールありそうに思って探したのですが、見つかりません。CheckStyleのような命名規則をチェックしてくれるものがあるので、これをカスタマイズすればできるのでしょうか。単なる規約以上は無理なのか・・

ご存知の方おられたらぜひ教えてください m(_ _)m。

【関連記事】
変数の命名規則
テキストとしてのコーディング規約
コンパイル時の静的チェック(STATIC_ASSERT)
【関連書籍】
Joel on Software Joel Spolsky 青木 靖
組込みソフトウェア開発向けコーディング作法ガイド[C言語版] SEC
C++ Coding Standards―101のルール、ガイドライン、ベストプラクティス ハーブ・サッター他
リファクタリング―プログラムの体質改善テクニック
プログラミング作法

【関連リンク】
間違ったコードは間違って見えるようにする - Joel on Software
ハンガリアン記法 - 悪態のプログラマ

テキストとしてのコーディング規約

愚者が賢者から学ぶことよりも、賢者が愚者から学ぶことのほうが多い。
モンテーニュ
今回の記事のテーマは「コーディング規約」です。コーディング規約とに含まれる、変数や関数の命名規則やら、コメントの書き方等の話題はここでも何度か取り上げましたが、今回は、コーディング規約の「テキスト」としての側面に焦点を当てたいと思います。

4798111899「コーディング規約」や「コーディングルール」というと、何だか型にはめられるという気がして、拒否反応を示す方もおられるでしょう。確かに「規約」とか「ルール」とかいう単語が入っているので、そのようなイメージは強いでしょう。しかし、中には「制約」というよりは「ガイドライン」と呼ぶべきものも多く含まれています。

ここで、コーディング規約導入の必要性について改めて整理してみます。細かく分類することも可能でしょうが、大きくは次の二つに大別できるかと思います。
  • スタイルを統一することで可読性を改善する。
  • 危険なコーディングを禁止し、品質を確保する。
前者は、複数人での作業を効率化するために存在します。変数や関数の命名規則やら、インデントの統一、一関数の最大行数の設定等があげられるでしょう。 それに対し後者は、移植性がない、可読性が悪い等の問題を排除するために、コーディング方法にルールを作るというものです。 オーバーラップする点も大いにあるとは思いますが、前者は「人間(他の担当者や保守担当者)のため」、後者は「プログラム自身のため」とも言えると思います。

さて、この後者の方ですが、内容的には規約というよりはガイドラインといった方が良いかもしれません。この点を積極的に利用すると、コーディング規約をプログラミングのテキスト・教科書としてとらえることができます。
以下の表に組込みソフトウェア開発力強化推進委員会による「コーディング作法ガイド」の一部を抜粋してあげさせて頂いています。それぞれの項目は、入門書でもよく言われるものから、意外と見落とされがちなもの、複数人の開発において重要となるルール的なもの、はたまた「実行時の利用スタックサイズが予測できないため再帰呼び出し関数は使用しない」といったような組込み特有のものまで、広範囲にわたってまとめられています。

構造体や共用体の比較にmemcmp を使用しない。

構造体や共用体のメモリには,未使用の領域が含まれる可能性があり,その領域に何が入っているか分からないので,memcmp は使用すべきでない。比較をするなら,メンバ同士で行う必要がある。また,構造体のメンバの詰め物,境界調整は処理系定義である。
<適合例>
struct {
  char c;
  long l;
} var1, var2;
void func() {
  if (s1.a == s2.a && s1.b == s2.b) {
/* …*/
<不適合例>
struct {
  char c;
  long l;
} var1, var2;
void func() {
  if (memcmp(var1, var2, sizeof(var1)) == 0) {
    /* … */
浮動小数点型変数は厳密な等式,非等式の比較はしない。【MISRA 13.3】

浮動小数点型は,見た目の値と実装された値は完全に一致していないので,比較は許容誤差を考慮して判定する必要がある。
<適合例>
#define LIMIT 1.0e-4
void func(double d1, double d2) {
  double diff = d1-d2;
  if (-LIMIT <= diff && diff <= LIMIT) {
    /* … */
<不適合例>
void func(double d1, double d2) {
  if (d1 == d2) {
    /* … */
下線で始まる名前(変数)は定義しない。

言語規格では,次の名前を予約済みとしている。
(1) 下線に続き英大文字1 字または下線に続き,もう1 つの下線で始まる全ての識別子は,いかなる使用に対しても常に予約済みである。
例: _Abc, __abc
(2) 1 つの下線で始まるすべての識別子は,通常の識別子及びタグ名前空間予約済みの名前を再定義した場合の動作は未定義である。
1 つの下線で始まり,小文字が続く名前は,ファイル有効範囲以外の部分では予約されていないが,覚えやすい規則とするため,下線で始まる名前すべてを使用しないという規則としている。
<不適合例>
int _Max1; /* 予約されている */
int __max2; /* 予約されている */
int _max3; /* 予約されている */
struct S {
  int _mem1; /* 予約されていないが使用しない */
};
実行時の利用スタックサイズが予測できな いため再帰呼び出し関数は使用しない。 【MISRA 16.2】  

内容は非常に充実しており、C言語プログラミングの初心者の方はもちろん、中級者以上の方にとっても、実践的な注意点が体系にまとめられており、非常に参考になると思います。例を見て頂けると分かるかと思いますが、危険なコーディングを禁止というよりは、「こんなことをするとこういうことになります。知ってますか?」というような失敗例・アンチパターンのカタログのようなものになっています。実際これらの多くは、言語仕様の勉強やサンプルコードだけではなかなか見えてこないもので、バグの定番のようなものが多いです。とはいえ、実際にバグを生めて苦い経験をしなくとも、事前にこのような失敗例を知っていることで、バグは回避できます。また、なぜそのようなコーディングがNGなのかを考えることで、言語仕様やプログラミングについての理解は深まります。

上の表のオリジナルはIPAのHPでPDFが無料配布(コーディング作法ガイド 0.8)されており、書籍化されたもの(組込みソフトウェア開発向けコーディング作法ガイド[C言語版])もあります。
無料で手に入るものですので、初心者以外の方も一度目を通してみられることをおすすめします。
(初出「組込みプレス Vol.3」を元に改稿)

【関連記事】
変数の命名規則
ビットシフトの落とし穴 - 算術シフトと論理シフト

【関連書籍】
組込みソフトウェア開発向けコーディング作法ガイド[C言語版] SEC
C++ Coding Standards―101のルール、ガイドライン、ベストプラクティス ハーブ・サッター他
リファクタリング―プログラムの体質改善テクニック
プログラミング作法
組込みプレス Vol.3

大規模ファイルを扱うには - 2Gの壁とLSF仕様

誰もが自分自身の視野の限界を世界の限界だと思い込んでいる。
アーサー・ショーペンハウエル
32bitの符号付き整数の最大値は2G、符号無しなら4G。それ以上は表せません。そのため、いわゆる2Gの壁、4Gの壁と呼ばれるものがあります。代表的なものはファイルサイズ、そして、メモリサイズです。まだメモリが2GB以上ということはそれほど多くはありませんが、ファイルサイズに関しては2GB以上のものも少なくありません。例えば、TV番組を録画した動画ファイル、DVDのイメージ等々。今回は、2GB以上の大規模ファイルを扱うためLSFについて説明します。

最近のLinuxやBSDだと、デフォルトでカーネルは2GB以上のファイルにLSFとは対応していますし、プログラムも対応しています。NFS(Server/Client)等のデーモン、Appach等のサーバ、cp、cmp等のfileを扱うプログラム等々。そのため、Linuxをデフォルトのままデスクトップとして使っている間は、あまり大規模ファイルのサポートについてはあまり意識する必要はないかもしれません。

しかし、後からプログラムをソースからmakeしてinstallする場合や、自ら開発する上ではこの2GBの壁について意識する必要があります。これを意識せずにコンパイルを行うと、2GB以上のファイルアクセスができなかったり、時には思わぬバグを引き起こしたりする危険性があります。

大規模ファイルアクセスのための仕様はLFS仕様(Large File Summit specification)として規定されたものがあり、Linux(glibc)等もこれに従っています。まずこのLFS仕様を説明します。

現行APIの拡張

従来のファイルアクセスのインタフェースでは、各パラメータは32bit値になっているため、2GB以上の大規模ファイルを正しく扱えません。そのため、LSFでは、このようなファイルに対して処理を実行できない場合や属性を正しく表現できない場合は、適切なエラーとして処理するようになっています。たとえば、open()は、2GB 以上のファイルに対してはエラーとなり、errnoにEOVERFLOW に設定します。

移行用API (transitional API)

LFS は、32bitから64bitへの移行用として、従来の32bitアクセスのAPIとは別に、それぞれのAPIに対応する64bit版のAPIを定義しています。例えば、open()に対するopen64()、seekに対するseek64、stdio.h系のAPIに関しても同様に、fopen()に対するfopen64()等 。これに伴って方の方もオフセットoff64_t が定義されています。

コンパイル環境

◎通常のコンパイル環境 (regular compilation environment)
これは既存のコンパイル環境であり、通常の32bit版のAPIがそのまま使用されます。

◎移行用コンパイル環境 (transitional compilation environment)
通常の32bit版のAPIに加えて、移行用の64bit版API使用可能になります。従来のプログラムには影響を与えませんが、64bit対応するためには、明示的にopen64()等を使用する必要があります。
_LARGEFILE64_SOURCEマクロを1に設定することによって、64bit版APIが使用可能になります。

◎「大規模ファイル」コンパイル環境 (`large file' compilation environment)
この環境では、ファイルアクセスの全APIが64bit対応になります。従来のAPIは全て64bit版にマップ(リダイレクト)されます。具体的には、open()コールはopen64コールにマップされます。この環境では、型定義も64bit版に置き換えられるので、従来のプログラムをこの環境でリコンパイルする場合は、その影響を考慮する必要があります。
_FILE_OFFSET_BITSマクロに64を定義することによって、この環境になります。

gccで大規模ファイル対応のコンパイル

最近のgcc(glibc)ではLSF使用に対応しています。よって、プログラムを2GB以上のファイルに対応させる場合は、makefile等で、
CPPFLAGS += -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64
等と書いてやればOKです。

4797328355【関連書籍】
GCC GNU C Compiler―Manual & Reference 遠藤 俊徳
プログラミング言語C ANSI規格準拠 B.W.カーニハン D.M.リッチー
Cプログラミングの落とし穴 A.コーニグ
Linux マルチメディアソフト集

使用しない仮引数

多くを知るものは少なく語る
Who knows most says least.
仮想関数やテンプレート関数等、同じシンタックスで複数の使われ方をするような関数では、使用しない仮引数というものが登場することがあります。例えば、
class ClassBase {
  virtual func(int hint);
};

class ClassA : pulic ClassBase {
  virtual func(int hint);
};

void
ClassA::func(int hint)
{
  // このClassのfuncでは仮引数hintは不要なので使用しない
  ...
}
仮想関数やテンプレート以外でも、関数仕様として拡張性を持たせるために用意されているものの、現状では使用しない場合等も考えられるでしょう。

さて、このような使用しない仮引数が使用しないだけで、動作上問題はないのですが、コンパイル時に未使用変数(unused variable)として警告目セージが出力されてしまいます。このような警告を出力されたままで放置すると、本来注意するべき警告が埋もれてしまう(見逃されてしまう)可能性もありますので、できれば避けたいところです。
ここでは、このような未使用仮引数の警告を抑制するいくつかの方法を紹介します。

コンパイラのunused属性を使用する

コンパイラによっては、変数や構造体に属性をしているための方法が用意されています。例えば、gccでは次のような指定が可能です。
void
func(int val __attribute__ ((unused)) )
{
 ...
}
(参考)GCC - 変数属性の指定

unused以外では、構造体のパック(pack)やアライメント(alignment)調整等、セクション配置等が指定できます。
この方法の問題は、指定方法がコンパイラ依存のため、移植性に問題がでる点です。

仮引数名を省略する

Cでもプロトタイプ宣言中では仮引数名を省略できましたが、C++では関数定義でも省略できます。使用しない仮引数は、変数名を省略することで、その仮引数を使用しないということをコンパイラに伝えることができます。
残念ながらこの方法はCでは使えません。

voidキャストを使う

この方法が個人的には最もおすすめです。使用しない変数を関数定義中でvoidキャストしてやる方法です。例をあげます。
#define UNUSED_VARIABLE(x) (void)(x)

void
func(int val)
{
 UNUSED_VARIABLE(x);
 ...
}
この方法のメリットは、C,C++共通で使える、移植性の問題もない、といったこともありますが、何よりあとから未使用変数をgrepし易いこと点があります。上記の例では、未使用変数宣言用のマクロを用意して使用していますので、ソースコード中からUNUSED_VARIABLEをgrepすることで、未使用変数を洗い出して見直すことができます。

【関連記事】
warningに気を配る

【関連リンク】
仮引数名の省略 (C++と組み込み環境)
GCC - 変数属性の指定

【参考書籍】
GCC GNU C Compiler―Manual & Reference
Introduction To Gcc (Richard M. Stallman)

コンパイル時の静的チェック(STATIC_ASSERT)

早起きは3億の徳

デバッグをスムーズに進めるにはどうすれば良いか。一つは、できるだけ早い段階でバグ(ミス)に気付くことです。assertやその派生マクロは、バグを早期発見する上で非常に有効な手段ですが、条件チェックは実行時でないと行われません。実行時ではなく、コンパイル時に静的にバグを発見できれば非常に便利です。

このようなコンパイル時の静的チェックのひとつがstatic_assertです。例えば、次のようにして使用します。
int send_msg(void* msg, size_t size);

int func()
{
  my_type_t my_data;

  ....

  STATIC_ASSERT(sizeof(my_type) <= sizeof(msg_t)); 
  send_msg(&my_data, sizeof(my_data));
}
通常のassertであれば、実行時上記コードを通った段階でabort()等で気付かせてくれますが、static_assertの場合は、コンパイル時にエラーになります。

では、このようなstatic_assertはどのようにして定義するのでしょうか。C++では、boost等でも用意されているtemplateを用いた方法がありますが、ここではCでも実現可能な方法を紹介します。
#define STATIC_ASSERT(expr) { char static_assertion_failed[(expr) ? 1 : 0]; }
分かるでしょうか?
通常長さ0の配列はコンパイルエラーになるため、exprがコンパイル時に成立していないと、コンパイルエラーになるのです。この方法の欠点は、エラーメッセージが「長さ0の配列は定義できません」等の意味不明なメッセージになる点です。少しでもわかりやすくするために、上の例では、変数名をstatic_assertion_failedとしています。
その他、C++でtemplateを用いた凝った方法を使うと、もう少し分かりやすいメッセージ表示にすることも可能です(関連書籍が参考になります)。


4894714353 【関連書籍】
Modern C++ Design―ジェネリック・プログラミングおよびデザイン・パターンを利用するための究極のテンプレート活用術

可変個引数マクロを使う

この世で変わらないのは、変わるということだけだ。
スウィフト

C99規格(ISO/IEC 9899:1999 - Programming Language C)は1999年12月にISOによって規格化されたC言語の新しい規格です。従来のC言語(C89やANSI C)には含まれていない新機能が数多く規格として導入されています。以前の記事「bool型の落とし穴」で触れた_Bool型もC99の新機能の一つです。

今回、C99の新機能の中で個人的にかなり便利だと思っている「可変個引数(可変個数引数)マクロ__VA_ARGS__」と「定義済みマクロ__func__」を紹介します。

拡張マクロ機能の使用例

最近のデバッガは高機能化しているとはいえ、ログ出力(ロギング)によるデバッグが便利なケースはまだまだあります。拡張マクロ機能はこのようなログ出力の記述時に、力を発揮します。
例えば、次のようなログ出力関数があったとします。
void debug_printf(fmt, ...);
可変引数関数は従来のC言語からサポートされています。関数本体の記述方法は関連リンク(Manpage of stdarg等)を参照して下さい。

さて、このログ出力関数はデバッグ関数のため、できればデバッグ中以外は完全に無効化してしまいたいと考えます。そこで、次のようなマクロを定義しようとします。
#if defined(DEBUG)
#define DEBUG_PRINTF(fmt, ...) debug_printf(fmt, ...)
#else
#define DEBUG_PRINTF(fmt, ...)
#endif
しかし、このような可変個数の引数を持つマクロは従来定義できなかったため、このコードはコンパイルエラーになります。C99ではこのような可変個数マクロがサポートされるようになりました。C99での記述は上記とは若干違い__VA_ARGS__マクロを使用します。
#if defined(DEBUG)
#define DEBUG_PRINTF(fmt, ...) debug_printf(fmt, __VA_ARGS__)
#else
#define DEBUG_PRINTF(fmt, ...)
#endif
これで、先程の目的は達成されました。
次に、このマクロにもう少し手を加えてみましょう。ログ出力はエラー検出時に出力されることが多いと思いますので、出力コードのあるソースファイルの場所を一緒に出力しておくと何かと便利です。例えば、次のようになります。
#if defined(DEBUG)
#define DEBUG_PRINTF(fmt, ...) \
  printf("%s:%d\n", __FILE__, __LINE__); \
  debug_printf(fmt, __VA_ARGS__)
#else
#define DEBUG_PRINTF(fmt, ...)
#endif
これで、ログ出力のあるファイル名と行数が一緒に出力されるため、エラー発生箇所の特定が容易になります。しかし、実際は、ソースの行数よりも関数名の方を知りたいことの方が多いでしょう。C99で導入された定義済みマクロ__func__がこの要求に応えてくれます。
#if defined(DEBUG)
#define DEBUG_PRINTF(fmt, ...) \
  printf("%s:%d:%s\n", __FILE__, __LINE__, __func__); \
  debug_printf(fmt, __VA_ARGS__)
#else
#define DEBUG_PRINTF(fmt, ...)
#endif
これで、関数名も出力されるようになりましたので、エラー発生箇所の識別が容易になります。

拡張マクロ機能のサポート状況

ここで紹介したC99の新機能(__VA_ARGS_と__func__)は、gccではサポートされていますが、VC++.NETではサポートされていないようです(その他のC99機能に関しても、VC++ではほとんどサポートされていません)。 非常に便利な機能なので、使用するに値すると思っていますが、移植性は低くなるということは覚えておく必要があります。移植性を考えれば、今回あげた例のようなデバッグコードのみの使用等に限定した方が良いでしょう。
# マクロなのでそもそも使いすぎない方が良いですが(^^;)

【関連記事】
printfとtypedefの微妙な関係 - 無視されがち?なformt warning
bool型の落とし穴

【関連リンク】
Manpage of STDARG
プログラミング言語Cの新機能(seclanのほえほえルーム)
定義済みマクロ__func__と可変長引き数マクロ(フリーウェア徹底活用講座)

【関連書籍】
GCC GNU C Compiler―Manual & Reference 遠藤 俊徳
GCC: The Complete Reference Arthur Griffith
新Visual C++ .NET入門 シニア編 林 晴比古

ポインタ引数とconst指定

最も賢い者と最も愚かなものだけが、決して変わることがない。
孔子
今回のテーマはconst修飾子。非常に有用な修飾子ですが、意外と使われていない方も多いんではないでしょうか。

constはいろんなところに登場しますが、今回は特にポインタ変数のconst指定について書きたいと思います。

例えば、次の関数プロトタイプがあったとします。
/* position値が有効かどうか調べる */
bool
is_valid(struct position_t* p_pos);
また、この関数を使用する次のコードがあります。
{
  struct position_t pos;
  ...
  if (is_valid(&pos)) {
  ...
}
このis_valid()呼び出しでは、posのアドレスを渡しています。 ポインタ渡しなので、posの内容は書き換えられる可能性があります。 そのため、呼び出し側ではposの値がis_valid()呼び出し後も変化していないということを期待することはできません。 その後も、posを使用するのであれば、posの値をコピーする、あるいはコピーの方をis_valid()に渡す必要があります。

とはいえ、普通is_valid()のような関数はposを書き換えたりはしないでしょう。 そこで、通常は次のように、const指定をつけます。
bool
is_valid(const struct position_t* p_pos);
これで、呼び出し側は安心してポインタを渡せます。

よくコメントで、INPUTのみでしか使いませんと書いてあるのもありますが、それだけでは不十分です。コメントはあくまで補足。INPUT引数には、const指定をつけましょう。

【関連書籍】
CプログラミングFAQ―Cプログラミングのよく尋ねられる質問 スティーブ・サミット
プログラミング言語C ANSI規格準拠 B.W.カーニハン D.M.リッチー
独習C ハーバート・シルト
Cプログラミングの落とし穴 A.コーニグ

charの落とし穴 - 暗黙の型変換と符号拡張

変化というものは、たとえ良い方向に変わっているときでさえ、常に障害と不快がともなう。
アーノルド・ベネット
前回unsignedでよく陥りがちなバグについて触れました。今回はその続編で、char型での落とし穴として、いわゆる符号拡張(sign extension)と暗黙の型変換(inplicit conversion)について説明します。

次のコードの問題点はわかるでしょうか?
typedef char value_t;
#define INVALID 0xff

/* valがINVALIDなら0、それ以外で1を返す */
int
check(value_t val)
{
  switch (val) {
  case INVALID:
    return 0;
  default:
    return 1;
}
一見問題なさそうに思えますが、実際このコードをコンパイルして、valにINVALID(0xff)を指定しても1が帰ってきます。なぜでしょう?

C言語のswitch分では、比較値はint型として扱われます。よって、valがswitchに渡されるときに、valの値はint型に暗黙の内に拡張されます。変換時には符号拡張が行われ0xff(charの-1)は0xffffffff(intの-1:intが32bitの場合)となってしまいます。よって、0xffとは一致しないのです(※1)。

これは上の例の用に、charをtypedefした型と、#defineで定義したマクロ値で、列挙型のような使い方をしようとした時に起こりがちなバグです。列挙型(enum)だとint相当でサイズが4byteになってしまうということで、このような使い方はよく見かけます。回避策としては、value_tをunsigned charでtypedefすることです。unsignedであれば、符号拡張はされないので、bitパターンはそのまま保持されます。

※1 charが符号付きかどうかは処理系依存です。動作することもあります。

【関連記事】
unsignedの落とし穴
テキストとしてのコーディング規約
warningに気を配る

【関連書籍】
Cプログラミングの落とし穴 A.コーニグ
CプログラミングFAQ―Cプログラミングのよく尋ねられる質問 スティーブ・サミット
プログラミング言語C ANSI規格準拠 B.W.カーニハン D.M.リッチー
独習C ハーバート・シルト

unsignedの落とし穴

よく見かけるバグのパターンなんかも、ちょっとずつ紹介していこうかなと思います。
最初のテーマは、符号無し型(unsigned)の落とし穴です。

符号無し型を、符号有り型(signed)と混在させて使い、結構いろんなバグの原因になっているのをよく見かけます。
次の例は、実際に経験された方も多いのではないでしょうか。

typedef unsigned int UINT;

{
UINT data[MAX];
UINT i;

for (i = MAX - 1; i >= 0; i--) {
/* dataの処理 */
func(data[i]);
}
}

実際に、上記のようなコードを書いて実行すると何が起こるでしょう?
答えは、永久ループです。

iはunsignedのため、決して0未満にはならないのです。
i==0の時の、i-1は、-1ではなく、0xffffffff(intが32bitの場合)です。

iがintあるいは他のsignedなら何の問題もないのですが、iの比較対象がunsigned型の定数だったりすると、コンパイル時にwarningが出て、ついついiの型をUINTで宣言してしまうことがあります。
そもそも、逆順に処理している(i--)が問題なのですが、これは事情によってそうせざるを得ないこともあります。

例えば、対応は下記のようになります。

{
UINT data[MAX];
UINT i;

for (i = MAX; i > 0; i--) {
/* dataの処理 */
func(data[i - 1]);
}
}

unsignedには他にもいろいろ注意が必要なので、またおいおい紹介していきます。

本のおすすめ

4873115655

4274065979

4822236862

4274068579

4822255131

B00SIM19YS


プロフィール

  • Author:proger
  • 組み込み関係で仕事してます

ブログ内検索

上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。