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

C言語を使って移植性の高いプログラミングを行うのは簡単ではありません。このブログでは、筆者自身の備忘録を兼ねて、移植性のあるプログラミングのノウハウを記録していきたいと思います。
<< June 2017 | 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 >>
 
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
 
スポンサーサイト

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


- | | - | -
空ポインタ定数 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以外へのポインタに暗黙的な変換が行われないからです。

スポンサーサイト

- | 02:16 | - | -
コメント
from:   2006/09/13 7:40 AM
C++ ではなく C であれば、 (void*)0 は void* 型である前に
空ポインタ定数である (6.3.2.3) ので関数ポインタを初期化することも、
関数ポインタへ代入することも可能ではないですか?
from: たかぎ   2006/09/13 9:23 AM
コメントありがとうございます。

JIS X3010:2003の6.3.2.3では、「値0をもつ整数定数式又はその定数式を型void*にキャストした式を, 空ポインタ定数(null pointer constant)と呼ぶ。」とあるだけで、関数へのポインタ型への変換等についての記載はないようです。

前回の記事でも書いたように、void*は任意の不完全型またはオブジェクト型へのポインタとの間で相互変換ができることになっていますが、関数型というのは不完全型でもオブジェクト型でもありませんから、void*との相互変換はできません。

NULLが0または0Lに定義されていれば、関数へのポインタへの代入や初期化に使えますが、それでは移植性が損なわれます。

「空ポインタ定数をポインタ型に型変換した場合」とあるのは、空ポインタ定数の定義方法次第ではどんなポインタ型にでも変換できるからだと認識しています。
from:   2006/09/13 8:42 PM
> 関数へのポインタ型への変換等についての記載はないようです。
そんなこと言ったら、整数型である 0 から関数へのポインタへの変換についても記載は無いことになってしまいませんか?

それに、 (void*)0 から関数へのポインタへの変換が他のデータポインタと同じく不正であるなら、空ポインタ定数の定義で (void*) へのキャストについて言及する意味がまったくありません。

事実以下のようなコードに対して、
void (*p)(void);
p = (void*)0;
p = (void*)&p;
gcc3.4, msvc8 の両方とも、2つの代入のうち後者にしか警告を出力しませんでした。

関数へのポインタであっても、空ポインタ定数からの変換によって正しくヌルポインタが生成されると考えるのが正しいと思います。
from: たかぎ   2006/09/13 11:39 PM
> そんなこと言ったら、整数型である 0 から関数へのポインタへの変換についても記載は無いことになってしまいませんか?

6.3.2.3の空ポインタ云々の記述のあとに、「整数は任意のポインタ型に型変換できる。」とあります。これが、0を関数へのポインタに代入できる根拠です。

> それに、 (void*)0 から関数へのポインタへの変換が他のデータポインタと同じく不正であるなら、空ポインタ定数の定義で (void*) へのキャストについて言及する意味がまったくありません。

空ポインタ定数の定義のあとに続く記述は、主に比較に関する内容で、空ポインタ定数から他の型への変換いついては「空ポインタ定数をポインタ型に型変換した場合」という記述があるだけです。0の場合も(void*)0の場合もあるなら、状況に応じてどんなポインタにでも変換できるわけですが、ここで書かれているのは「変換した場合」であって、「変換できる」とも「変換できない」とも書かれていません。

> gcc3.4, msvc8 の両方とも、2つの代入のうち後者にしか警告を出力しませんでした。

上でも書いたように、(空ポインタ定数かどうかに関わらず)voidへのポインタから関数へのポインタへの変換は「できる」とも「できない」とも規格としては要求していないわけで、これはすなわち未定義の動作ということになります。

未定義の動作であれば、「変換でき」てもかまわないわけです。また、特定の処理系で変換できることを根拠とするのであれば、先日公開されたTurbo Explorerのリファレンスの「NULL #define」のところで、「これは, 関数ポインタとの互換性はありません。」と記述されていることもまた根拠になるはずです(コンパイルはできてしまうのですが)。

3.4.3の参考のところでは、未定義の動作に対して、「翻訳時又はプログラム実行時に, 文書化された, 環境に特有な方法で処理してもよい(診断メッセージの発行を伴っても伴わなくてもよい。)。」とあるように、実際、VC++8の「C Language Reference」における「Conversions to and from Pointer Types」には、

An integral constant expression with value 0 or such an expression cast to type void * can be converted by a type cast, by assignment, or by comparison to a pointer of any type.

とありますから、任意の型(関数型を含む)へのポインタに対する変換ができることが文書化されているわけです。

> 関数へのポインタであっても、空ポインタ定数からの変換によって正しくヌルポインタが生成されると考えるのが正しいと思います。

規格が変換できることを要求していない以上、それは未定義の動作です。

ちなみにC++では、4.10で「ゼロと評価される右辺値をもつ整数型の汎整数定数式を, 空ポインタ定数を呼ぶ。空ポインタ定数は, ポインタ型に変換することができる。」と、きわめて明確に記述されています。

かなり長くなってしまいましたが、私はこのように解釈しています。何かおかしいところがありましたら、ご指摘くださると助かります。
from:   2006/09/14 8:54 AM
なるほど。 6.3.2.3 だけではそのような解釈も可能ですね。
そこで空ポインタ定数について規格が言及している他の箇所を調べてみました。

6.5.16.1 代入(Simple Assignment)の制限(Constraints)において、代入式がとりうるオペランドの組み合わせとして左辺がポインタで右辺が空ポインタ定数である場合が挙げられています。
ここでポインタの指す先がオブジェクトであるか関数であるかの区別はありません。
そして続く意味(Semantics)で、右辺の値が代入式の型(左辺の型)に変換されるという動作が規定されています。
初期化については 6.7.8.11 により、これと同じ制限と動作になります。

同様に 6.5.9 の等値比較(Equality operators)の中でも、オペランドが任意のポインタ型と空ポインタ定数の組み合わせである場合に、空ポインタ定数が他方のポインタ型に変換されることになっています。

つまり 6.3.2.3 にある空ポインタ定数から任意のポインタ型への変換はこれらの規定された動作を行うために必要とされています。
(void*)0 が空ポインタ定数となっている以上、 (void*)0 の関数へのポインタに対する代入および初期化も正しく空ポインタを生むと考えられます。

C++ では (void*)0 は空ポインタ定数には含まれないので適用外です。
from: たかぎ   2006/09/14 11:30 AM
> 6.5.16.1 代入(Simple Assignment)の制限(Constraints)において、代入式がとりうるオペランドの組み合わせとして左辺がポインタで右辺が空ポインタ定数である場合が挙げられています。
> ここでポインタの指す先がオブジェクトであるか関数であるかの区別はありません。
> そして続く意味(Semantics)で、右辺の値が代入式の型(左辺の型)に変換されるという動作が規定されています。
> 初期化については 6.7.8.11 により、これと同じ制限と動作になります。

そうなのですが、そこに書かれているのはあくまでも「制約」であって、型変換に関する既存のルールを拡大するものでもなければ、特例を与えるものでもないと思います。

> 同様に 6.5.9 の等値比較(Equality operators)の中でも、オペランドが任意のポインタ型と空ポインタ定数の組み合わせである場合に、空ポインタ定数が他方のポインタ型に変換されることになっています。

比較に関しては、6.3.2.3の空ポインタに関する記述と同じですね。

> つまり 6.3.2.3 にある空ポインタ定数から任意のポインタ型への変換はこれらの規定された動作を行うために必要とされています。

はい。代入や初期化や比較を行うには変換が必要です。しかし、「必要である」ということと「変換ができる」ということは等価ではありません。また、その「必要である」というのは、記述されたコードが正しい意味規則となるために必要なのであって、処理系に課された要求というわけではありません。
空ポインタ定数から関数へのポインタへの変換が必要なら、(明らかに)変換可能な整数定数 0 を空ポインタ定数として使用すべきだと思います。

> (void*)0 が空ポインタ定数となっている以上、 (void*)0 の関数へのポインタに対する代入および初期化も正しく空ポインタを生むと考えられます。

「(void*)0 から関数へのポインタに変換できる」という明確な記述はどこにもありません。

もっとも、私もいまいち自信がないことも事実です。また、現存する処理系の多くは変換できてしまうこともまた事実です。しかし、移植の妨げになるかもしれないリスクを冒してまで、0 と書けば済むところを、敢えてNULLと書く必然性はないと思うわけです。
from: たかぎ   2006/09/14 12:07 PM
少し補足です。

型変換の仕様である6.3の中で「変換される」とか「変換する」とあれば、それは「変換できる」の意味を兼ねていると思います。しかし、式の仕様である6.5の記述で「変換される」とか「変換する」とあったとしても、それは型変換のルールを超えて「変換できる」意味にはならないはずです。

もし、型変換のルールを超えて変換できるのであれば、6.5.4のキャスト演算子の記述で、「括弧で囲まれた型名に続く式は, 式の値を指定された型に型変換する。」とありますから、浮動小数点型とポインタ型の相互変換も(キャスト演算子を使えば)できなければならないことになります。しかし、実際にはそんなことはあり得ません。

つまり、型変換に関するルールは常に6.3の記述に従うと考えてよいはずです。それ以外の箇所での記述は、普通は変換できるものを特定の状況では変換できない制約を課すことはできても、逆はないと思います。
from:   2006/09/16 4:33 AM
6.3 の中に「変換できる」という記述がなければそれは保証されないという理屈であれば、多くの変換が(空ポインタ定数からの変換と同じく)「変換された場合〜」という書き方になっているので、実際に変換が許されるかどうか保証されていないことになってしまいませんか?

「変換できる」と明記されているのはポインタ関連の変換の一部だけで、他の多くの変換についてはそのような書き方にはなっていないようです。

また 6.3(Conversions) の最初のパラグラフの最後に、以下のように 6.5 を参照する記述もあります。
"The list in 6.3.1.8 summarizes the conversions performed by most ordinary operators; it is supplemented as required by the discussion of each operator in 6.5."

やはり空ポインタ定数である (void*)0 が関数へのポインタに変換できない可能性があるというのは無理があると思います。

空ポインタを得るのにわざわざ NULL を使う必要が無いことには同意します。実際、もう何年も自分で NULL を使った覚えはありません。しかしそれはこのエントリが主張されているような問題のためではありません。
from: たかぎ   2006/09/16 10:51 PM
6.3をよく読み返してみると、算術型変換については変換できることを前提とした書き方になっていますね。それに対してポインタについては、変換できるものを列挙する書き方になっています。

不完全型やオブジェクト型へのポインタと、関数へのポインタの相互変換については、直接は触れられていないので未定義の動作ですが、空ポインタまたは空ポインタ定数については動作が規定されているので未定義にならないという解釈もできなくもないようです。

「空ポインタ定数をポインタ型に型変換した場合, 〜」だけだと、どちらの解釈も可能ですが、その下の「空ポインタを他のポインタ型に型変換すると, その型の空ポインタを生成する。」は一通りの意味にしか解釈できませんから。

値によって未定義になったりならなかったりということは、算術型でも表現範囲等の理由で起こり得るので、まあそうなのでしょう。

この辺の解釈については、あまり詳しく触れている文献などもないので、今回の議論は有意義でした。
コメントする









 
トラックバック
この記事のトラックバックURL
http://portable-c.jugem.jp/trackback/22