移植性のあるCプログラミング

C言語を使って移植性の高いプログラミングを行うのは簡単ではありません。このブログでは、筆者自身の備忘録を兼ねて、移植性のあるプログラミングのノウハウを記録していきたいと思います。
<< August 2016 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 >>
 
PROFILE
RECOMMEND
Cプログラミング診断室―さらに美しく健康的なプログラムのために
Cプログラミング診断室―さらに美しく健康的なプログラムのために (JUGEMレビュー »)
藤原 博文
私がある程度C言語でプログラミングができるようになった頃、この本の旧版を読みました。
非常に辛口の内容であり、読者によっては嫌悪感を抱く方もおられるかもしれませんが、「Cプログラミング診断室」は、C言語に関する数少ない有益な書籍のひとつです。もっとも、元の内容がかなり古いため、今となってはやや時代遅れとなった部分もあります。しかし、それを差し引いても、十分に読む価値のある一冊です。
RECOMMEND
新ANSI C言語辞典
新ANSI C言語辞典 (JUGEMレビュー »)
平林 雅英
まだC言語を覚えたてだった頃、この本の前の版である「ANSI C言語辞典」がボロボロになるまで活用しました。私が購入したC言語関連の書籍の中では、最もコストパフォーマンスが高かったと思います。
あまりにもボロボロになったので、改訂版にあたる、この「新ANSI C言語辞典」を購入しました。旧版を最初に手にしてから10年以上経ちますが、今なお手放すことができない必携の一冊です。
ARCHIVES
RECENT COMMENT
RECENT TRACKBACK
MOBILE
qrcode

無料ブログ作成サービス JUGEM
 
スポンサーサイト

一定期間更新がないため広告を表示しています


- | | - | -
配列の要素数を知る
sizeof演算子のように、配列型のオブジェクトを実引数として渡すと、要素数を表す定数式に展開してくれるマクロが欲しくなるときはよくあります。そんなときは、次のような定番のマクロを定義することで対応するのが普通だと思います。

#define countof(array) (sizeof(array)/sizeof((array)[0]))

これでも、一応最低限の要件は満たすわけですが、次のような使い方をした場合には、期待通りの振る舞いになりません。

int a[10];
int *p = a;
size_t n = countof(p);

これは、使い方が間違っているので正しく機能しないのは当然なのですが、それならそれで、コンパイルエラーになって欲しいところです。そこで、もう少し工夫してみることにしましょう。

まず、countofマクロの実引数が配列型かポインタ型かを見分ける方法を検討してみることにします。配列型というのは、ほとんどの演算で暗黙的にポインタ型に型変換されてしまいます。僅かな例外がsizeof演算子と&演算子です。

このうちsizeof演算子では、配列型のサイズとポインタのサイズがたまたま同じになることも十分考えられるので、あまり有効な手段とはいえません。では、&演算子はどうでしょうか?
続きを読む >>

空ポインタ定数 NULL
敢えて「空ポインタ定数 NULL」と書いたのは、いまだにNULLと'¥0'を混同する人がいるからです。NULLマクロはあくまでも空ポインタ定数であり、ナル文字である'¥0'とは全く意味が異なります。

と、簡単なことは最初に書いておいて、ここからはもっとディープな話題に入ります。今回のテーマであるNULLですが、このマクロで表される空ポインタ定数というのは、実は関数へのポインタには代入することができません。理由は前回の記事でも書いたように、NULLが (void*)0 のように、voidへのポインタとして定義されることが多いからです。

では、(void*)0 ではなく、0 と定義されていれば関数へのポインタに代入してもよいかというと、文法上できるかどうかでいえばできますが、そんなことをすれば、そのソースコードの移植性は失われてしまいます。

というわけで、NULLは関数へのポインタに代入(初期化も)することはできませんが、NULLと関数へのポインタの比較はどうやらできるようです。ただ、NULLを関数へのポインタに代入できないということは、NULLと関数へのポインタが互換性がないということです。したがって、関数へのポインタとの比較には、NULLではなく 0 を使用することをお勧めします。

さて、NULLの定義内容が (void*)0 であることが多いことは前述したとおりですが、0 の値を持つ整数定数として定義してもよいことになっています。ここで注意しないといけないのは、何らかの理由でNULLマクロを自作する場合には、安易に 0 と書いてはいけないということです。

なぜなら、可変個引数のように、関数原型によって型が特定されない関数の実引数としてNULLを渡す場合、ポインタのサイズと同じにしておかなければならないからです。int型が16ビットで、ポインタ型が32ビットの環境では、NULLを0に定義すると可変個引数として渡した場合に誤動作します。したがって、このような場合には 0 ではなく 0L に定義することになるわけです。

C++では、慣例的にNULLマクロは使用せず、代わりに整数定数の 0 を使用します。0 を使用すると、上記の問題が起きるのはC++でも同じですが、そもそもC++では可変個引数を利用する機会が少ないこと、また、関数の実引数として渡す場合、多重定義を正しく解決するには、NULLをそのまま渡すのではなくキャストが必要なことから、実際には問題が起こりにくいのだと思います。

ちなみに、C++ではNULLマクロを (void*)0 に定義することはできません。なぜなら、C++ではvoidへのポインタからvoid以外へのポインタに暗黙的な変換が行われないからです。

voidへのポインタ
voidへのポインタというと、どんな型へのポインタでも代入でき、どんな型へのポインタにでも暗黙的に型変換できる汎用ポインタです。しかし、voidへのポインタとの相互変換ができないポインタがあることをご存知でしょうか?

JIS X3010:2003の6.3.2.3 ポインタには、次のように記述されています。
voidへのポインタは, 任意の不完全型若しくはオブジェクト型へのポインタに, 又はポインタから, 型変換してもよい。任意の不完全型又はオブジェクト型へのポインタを, voidへのポインタに型変換して再び戻した場合, 結果は元のポインタと比較して等しくなければならない。
つまり、相互に型変換できるのは、不完全型へのポインタとオブジェクト型へのポインタだけということになります。

不完全型というのは、要素数を指定しない配列型や、struct タグ名だけの構造体や、同様の共用体・列挙体や、void型のことです。オブジェクト型というのは、算術型(整数型・実浮動小数点型・複素数型)・集成体型(配列型・構造体型)・共用体型・ポインタ型の総称です。

続きを読む >>

ヘッダ名
随分久々の更新ですが、今回は#include指令で指定するヘッダ名に関する話題です。ヘッダ名の指定方法が大きく分けて二つあることは、C言語を使われている方であればよくご存知のはずです。すなわち、<...>の形式と"..."の形式です。まずは、この二つの形式の違いから見ていきたいと思います。

まず、<...>の形式ですが、これは本来は処理系が提供するヘッダを指定するためのものですが、規格上はやや曖昧で、「処理系で処理可能なヘッダ」という分かったようなわからないような記述になっています。そして、このヘッダというのは必ずしもファイルである必要はありません。また、どのようにしてヘッダの探索場所を指定するか、どのようにしてヘッダを識別するかも処理系定義になっています。

次に、"..."形式ですが、こちらは基本的にはソースファイルを取り込むためのものです。そして、指定したソースファイルの探索方法は処理系定義です。

最も一般的な探索手順は、#include指令が記述されたソースファイルと同じディレクトリをまずは探索し、そこで見つからなければ、そのソースファイルが取り込まれたソースファイルと同じディレクトリを探索し...というように、最初に#include指令を使ったソースファイルと同じディレクトリまで遡るというものです。他には、常に作業ディレクトリを探索するという処理系もあります。

また、標準規格には"..."形式に関するところ(JIS X3010:2003 6.10.2 ソースファイル取込み)で、「この探索をサポートしていない場合」という記述があるので、"..."形式を指定しても<...>と同じになってしまう場合があるようです。この場合に、ヘッダがファイルではないとすると、ヘッダを追加する術は全くなくなってしまうわけですが、簡易的なインタープリタのようなものを除けば、まずこのような処理系に遭遇することはないと思います。

ところで、これを書くとWindows環境で作業されている方々は愕然とするかもしれませんが、ヘッダ名の中に逆斜線が含まれていた場合の動作は未定義になります。他にも ', ", //, /* が含まれていた場合も動作が未定義になります。また、規格上識別されることが保証されるヘッダ名は、英字で始まる英数字の並びの後に、1つのピリオドと英字1文字が続く名前ということになっています。また、英字の大文字・小文字の区別があるとは限らず、ピリオドの前の英数字の並びのうち確実に有効なのは6文字(C99では8文字)です。

続きを読む >>

ポインタ同士の減算
最近話題になったので備忘録代わりに投稿しておきます。ポインタ同士の減算についての話題です。

ポインタというのは、規格上は何らかのオブジェクトしか指すことができませんし、ポインタ同士の減算ができるのは、同じ集成体に属すものだけです。しかし、現実にはそんな制約を杓子定規に守っていると、カーネルやデバイスドライバや低水準のライブラリを実装するのが困難になります。

そこで、任意のアドレスを指すポインタを使ったり、それらの減算を行ったりするわけですが、同じ配列の要素を指すポインタ同士の減算では起きなかった事態に遭遇することがあります。

ポインタ同士の減算結果は、<stddef.h>ヘッダで定義されるptrdiff_t型という符号付き整数型になるわけですが、典型的な32ビットの処理系では、アドレス空間もptrdiff_t型もともに32ビットであり、2Gバイトを超えて離れたchar型のポインタ同士を減算するとオーバーフローが発生してしまいます。

これを回避するためには、いったん符号無し整数型にポインタをキャストしてから減算する必要があります。しかし、ポインタのサイズ以上の符号無し整数型として何を選ぶかも厄介な問題です。C99であれば、<stdint.h>ヘッダで定義されるuintptr_t型を使えばよいのですが、古い規格ではそもそもポインタの値を格納できる汎整数型が存在すること自体保証されません。

untptr_t型の代わりになりそうなものとしてsize_t型がありますが、size_t型はsizeof演算子の結果の型ですので、オブジェクトのサイズ以上は表現できません。したがって、2Gバイトを超えるようなサイズは表現できないことが多々あります。

なかなか難しい問題ですが、この問題を回避するには、処理系をある程度特定して、条件付きコンパイルなどの方法を採るしかなさそうです。

sizeof(型名) * CHAR_BIT != 有効ビット幅
char型が8ビットでlong型が32ビットの場合、sizeof(long)は4バイトになりますから、sizeof(long)*CHAR_BIT == 32で、バイト数に1バイトあたりのビット数(ここでは8ビット)を掛ければ有効ビット数になります。ところが、常にこうしたことが成り立つとは限りません。

特に、DSPなどでは、long型が48ビットのような中途半端な有効ビット幅を持っていたりします。これを32ビット境界に整列させようとすると、long型は8バイトでなければなりません。すなわち、sizeof(long)*CHAR_BIT == 64となり、有効ビット数である48ビットとは異なる結果になったりします。

そういった特殊なDSPなどに遭遇する機会は少ないかもしれませんが、浮動小数点型の場合は、同様の状況に遭遇することも比較的多いかと思います。例えば、Intelのi686をターゲットとするGCCでは、long double型の有効ビット幅が80ビットですが、境界調整の関係上、sizeof(long double)は12だったりします*1

もっとも、浮動小数点型の場合は、シフト演算や論理演算が使えないこともあって、有効ビット幅が直接影響することは少ないでしょうが、整数型についても、移植性を考慮に入れるのであれば、sizeof(型名)*CHAR_BIT を有効ビット幅として扱うべきではありません。ましてや、sizeof(型名)*8 などとするのは最悪です。


*1 バージョンによります。

構造体メンバの配置
移植性に関する話題としては、FAQに近いものとして、構造体メンバの配置に関するものがあります。つまり、構造体の最初のメンバのアドレスと、構造体全体の先頭アドレスが一致するのかどうかということと、構造体メンバの順序は保証されるのか、ということです。

これについては、標準規格を紐解けば、すぐに事実関係が明らかになります。JIS X3010:2003 6.7.2.1 構造体指定子及び共用体指定子 がそれにあたります。該当部分を引用すると、

 構造体オブジェクト内では, 非ビットフィールドメンバ及びビットフィールドが置かれる単位は, 宣言された順に増加するアドレスをもつ。構造体オブジェクトへのポインタは, 適切に変換すれば, その先頭メンバ(又はビットフィールドならば, それが置かれた単位)を指す。さらに, その逆も成り立つ。構造体オブジェクトの中に名前のない詰め物があってもよいが, 先頭には名前のない詰め物があってはならない。

となっています。
つまり、構造体の最初のメンバのアドレスと、構造体全体の先頭アドレスは一致しますし、構造体メンバは記述された順に並んでいることが規格上保証されています。ただし、ビットフィールドの場合は、各メンバではなく、ビットフィールドが置かれる単位についてですので、注意が必要です。

と、まあ、ここまでは建前です。数ある処理系の中には、独自拡張(というより非標準仕様)によって、境界調整に応じて、メンバを適切に入れ替えることで、構造体のサイズの縮小を図るものがあります。これは、いわゆるパッキングとは異なります。パッキングは境界調整を無視して、すべてバイト境界やハーフワード等に配置するわけですが、上述のものはメンバの入れ替えによって名前のない詰め物をなくそうとしているわけです。

このような仕様を持つ処理系では、通常、#pragma指令を使うか、コンパイルオプションでこのような機能を有効にします。#pragma指令の場合は使わなければ済むのですが、コンパイルオプションの場合は厄介です。つまり、(非標準処理系まで含めた)高い移植性を持たせるには、構造体メンバの配置に依存するコードは書かない方が無難そうです。

float型対double型
どうも、「float型は遅い」という迷信がいまだに残っているようです。おそらく、そのような迷信の元となっているのはここではないかと思います。「Cプログラミング診断室」は名著ですが、全ての内容を鵜呑みにするには、少々古すぎます。

確かに標準化以前のC言語では、float型の変数はいったんdouble型に変換されてから処理されていたため、float型を使うとかえって遅くなっていました。しかし、現在ではfloat型はfloat型のままで計算することが基本になっており、JIS X3010:2003 6.3.18 通常の算術型変換 では「型が要求する精度や範囲を超えて表現してもよい」という、非常に消極的な表現しかされていません。

つまり、float型の計算はfloat型で行うけれども、何ならdouble型やlong double型を使ってもいいよ、という程度でしかないのです。実際、多くの処理系はfloat型の計算をfloat型のままで行いますし、float型を使った方がFPUは高速に処理します。ましてや、ローエンドの処理系で、FPUを持たないものや、FPUがあっても単精度しかサポートしない場合には、double型を使うと非常に高くつくことがあります。さらには、DSPのベクタユニットなどでは、float型専用の機能もあったりします。

動作に関しては、float型を使わずにdouble型だけを使っても、移植性が損なわれることはありませんが、double型の方が高速であることを期待して使用しているのであれば、ある意味で、移植性が損なわれているとも言えます。

整数の除算・剰余算
これは比較的よく知られていることですが、一方のオペランドが正で、他方のオペランドが負の場合の除算の結果は、その絶対値が代数上の商より大きくなるか小さくなるかは処理系定義です。また、

a == ((a / b) * b + a % b)

は真になりますので、2つのオペランドの符号が異なる場合の剰余算の結果の符号も処理系定義になります。

そのため、演算結果に移植性を持たせるためには、いったん両方のオペランドの絶対値を求めてから商と剰余を計算し、後で符号を調整するか、div関数やldiv関数を用いる必要があります。

C++でも、この除算・剰余算に関する状況は同じなのですが、C99になると、オペランドの符号が異なる場合でも、除算結果は必ずゼロ方向に丸められることが規格上保証されるようになります。このことは、除算結果がゼロ方向に丸められない処理系の絶滅宣言と受け取ることができます。

すなわち、規格上は処理系定義の動作であったとしても、現実問題としては、整数の除算・剰余算の振る舞いは処理系に依存しないとみなすことができるはずです。これは、C++であってもおそらくは問題ないはずです。どうしても不安な場合は、念のため、処理系に対する過程を文書化しておくとよいのかもしれません。

関数のアドレスと空ポインタの比較
最近のホットな話題なので、忘れないうちに投稿しておきます。関数のアドレスと空ポインタの比較についてです。

フック関数など、特に必要がなければ、実体は何も定義しないまま、関数のシンボルを0番地に配置しておき、必要になった時点で実際の関数で置き換えるといった技法がよく使われます。

具体的には、GNUのリンカスクリプトでPROVIDEによってウィークなシンボルを定義したり、GCCのweak alias属性を付けたり、インラインアセンブラを使ったり、シンボルを0に配置するアセンブリ言語のソースとCの関数定義を使い分けたりといった具合です。

このとき、フック関数を呼び出す箇所では、関数が存在するかどうかを調べるために、0(空ポインタ)との比較を行うことになります。

extern void hook(void);

if (hook != 0)
  hook();

ところが、標準Cでは、いかなる関数へのポインタも空ポインタと等しくならないことが、言語仕様上保証されています。結果として、hookのアドレスを実際に調べるまでもなく、hook != 0は常に真であると判断しても、本来であれば問題がないわけです。

結果として、最適化オプションを付けてコンパイルすると、hook != 0の部分は常に真として扱われ、実際にはhookが0番地であり、関数定義が存在しないにも関わらず、hook関数を呼び出してしまうといった事態を招きます。

元々が、通常のCの使い方から逸脱した技法で、処理系に依存した方法を使った結果として起こる問題なのですが、こんな場合でも、極力移植性のある記述を行わなければならないことがあります。
この問題を回避する方法はいくつか検討されたのですが、最終的に落ち着いた方法だけを紹介することにします。

extern void hook(void);
void (* volatile func)(void) = hook;

if (func != 0)
  (*func)();

ここで、funcは関数へのポインタを格納するための変数なのですが、volatileが付いているところがポイントです。volatileが付いていると、コンパイラが知らない方法でfuncの値が変化するかもしれないため、func != 0が常に真であるとみなすことができなくなります。結果として、func != 0の評価は期待通り行われることになります。
Check