[迷信] とりあえず memset で初期化

局所変数、特に集成体を宣言した後、実際に必要かどうかに関わらず、必ず memset でゼロクリアする人は大勢います。しかし、そんなコードを見かけたら、それを書いた人のコードはすべて疑ってかかった方がよいかもしれません。

まずは、次のコードをご覧ください。

struct A
{
  int a;
  double b;
  char *c;
};

A a[10];
memset(a, 0, sizeof(a));

よく見かけるコードですが、上のコードは、必ずしも期待した結果になるとは限りません。なぜなら、double 型やポインタ型は、これらを構成する全ビットが 0 になったとしても、オブジェクトの値が 0 になるかどうかは分からないからです。

確かに、ほとんどの処理系では上記のコードでも問題なく、そして期待通りに動作します。しかし、それはあくまでも"たまたま"動いているに過ぎません。そうした不安定な要素をなくすために行った初期化が、かえってコードを怪しくしてしまっているのです。

単に、集成体の全要素をゼロクリアしたいだけであれば、

A a[10] = { 0 };

とすれば十分です。こう書くと、おそらく次のような反論が返ってくることでしょう。「その方法では、構造体の詰め物がゼロクリアされない」と。しかし、構造体の詰め物にアクセスして、言語仕様上保証される結果を期待することには無理があります。

構造体の詰め物をゼロクリアしたい理由は、多くの場合、memcmp を使って一致判定を行いたいことが理由でしょう。しかし、整数型以外をゼロクリアしても結果が保証されないのと同様、比較の場合もうまくいくとは限りません。具体的には、内部表現が異なる場合でも、値としては同じになるかもしれないからです。

あるいは、こんな反論も聞こえてきそうです。「memset を使った方が効率がよい」と。本当に効率がよいかどうかは実測してみるか、コンパイル結果を見て、ステップ数を計算してみてください。必ずしも memset の方が効率がよいわけではないことに気付くはずです。

仮に memset の方がずっと効率がよかったとしても、こんな汚い方法による最適化は最終手段にすべきです。それに、こんな初期化はそれ自体が不要な場合もあり、単なる"おまじない"に過ぎないことも多いのです。

書籍紹介

参考情報

C++に限れば…

C++なら構造体にコンストラクタを追記するのが初期化としては正しいかもしれませんね。

「それを書いた人のコードはすべて疑ってかかった方がよいかもしれません」という言い方には疑問を感じますが、既にコンストラクタが定義された構造体をメンバに持つ集成体をmemsetなどでゼロクリアされると困ったことにはなりますね。

グローバル変数がゼロで初期化されるのと同レベルのことでは無いでしょうか。
この状態で「初期化」という言葉を使うのがいけないのかな。

デバッグのためかと

memsetでの0クリアは未初期化によるバグを発見しやすくするのが一番の目的だと思います。
各要素を0クリアしておかないと(別に0でなくてもいいんですけど)、初期化を忘れた際に、再現性が低く追いにくいバグになることがあるので、それを避けるために0クリアしたりすることはあります。
C99だと構造体の初期化演算子があるのでそんなことしなくてもいいんですけど。

私もデバッグのためだと思います。 ですが、私

私もデバッグのためだと思います。
ですが、私の場合の要件は若干異なります。

私の場合の要件は、主にデバッガーで構造体の内容(に限らずメモリの内容)を確認
しながらステップ動作させるような場合の利便性向上を目的としています。

構造体にしろ配列にしろ、不定な値が入っている状況は、肉眼による目視時に余計な
情報、不正な情報との区別が非常に着きにくく、『観察』という目的を阻害します。

構造体の内容が定まった値(0等)で初期化されていないと、正常に動作しないコード
は極力書かない主義ですが、肉眼での観察に対する利便性と、コードの品質管理につ
いては全く別の問題だと考えます。

gccの3.4くらいのバージョンで-Wallを指定した際

gccの3.4くらいのバージョンで-Wallを指定した際
A a[10] = { 0 };
でwarningが出て抑制するオプションが無かったので、memset利用も許して欲しいです。

今は-Wno-missing-field-initializersで抑制できるのでそっち使いますが。

コメントありがとうございます。

gcc 3.4.6で試してみましたが、-Wallを付けても警告は出ませんでした。

警告というのは問題につながるかもしれない怪しいコードを指摘してくれるものですよね。
それなのに、問題のないコードに対する警告を黙らせるために、怪しいコードを書くことで対応するというのはどう考えてもおかしくないですか?

_

警告出たコードは本文に書いてあるやつじゃなくて下記のほうでした
A a = { 0 };

_

そうですね。おかしいですね。あと、警告が出るオプションは -Wall でなくて -Wextra でした。手元のgcc3.4.5のmingwで試しました。失礼しました。

> それを書いた人のコードはすべて疑ってかかった方がよいかもしれません。
この書き方に過剰反応しました。失礼しました。

静的配列ならいいけど・・・

A = (TYPE *)malloc(sizeof(TYPE));
memset(A, 0, sizeof(TYPE));

このようなケースでは型ごとに初期化関数を用意しろということですかね。

Re:静的配列ならいいけど・・・

コメントありがとうございます。

ご指摘のケースでは、TYPEがどんな型なのかにもよるかと思います。
整数型そのもの、または整数型だけをメンバに持つ構造体であれば、mallocのあとにmemsetを呼ぶよりはcallocを使うべきではないでしょうか?
整数型以外を含む構造体の場合、そうしたメンバが少なければ、callocのあとで、そのメンバだけ代入してもよいでしょう。
あるいは、処理系に完全に依存したモジュールであれば、memset等で初期化しても実質的な問題はないと思います。
C99なら複合リテラルを代入するという方法もないわけではありません。
あと、C++であれば、A = new TYPE(); で済みますね。

いずれの場合も、その初期化が本当に必要であれば、ということが前提です。
ゼロクリアした直後に、別途各メンバに値を代入するようなコードが結構多いですから。

>

> 整数型そのもの、または整数型だけをメンバに持つ構造体であれば、mallocのあとにmemsetを呼ぶよりはcallocを使うべきではないでしょうか?

内部仕様が公開されていない mbstate_t 型の変数を動的に割り当てたい場合は
どのように対処されますか?

それに、malloc+memsetをcallocに変更するだけでは、
以下の問題をクリアできないので、全く以て解決策になっていないと思います。

# よく見かけるコードですが、上のコードは、必ずしも期待した結果になるとは限りませ
# ん。なぜなら、double 型やポインタ型は、これらを構成する全ビットが 0 になったと
# しても、オブジェクトの値が 0 になるかどうかは分からないからです。
#
# 確かに、ほとんどの処理系では上記のコードでも問題なく、そして期待通りに動作しま
# す。しかし、それはあくまでも"たまたま"動いているに過ぎません。そうした不安定な
# 要素をなくすために行った初期化が、かえってコードを怪しくしてしまっているのです

コメントありがとうございます。

> 内部仕様が公開されていない mbstate_t 型の変数を動的に割り当てたい場合は
> どのように対処されますか?

mbstate_t型はオブジェクト型だとしか規定されていませんので(たまたま整数型、または整数型だけで構成される修正型の場合もありますが)、

> 整数型そのもの、または整数型だけをメンバに持つ構造体であれば、

には該当しません(もちろん、特定処理系に特化するつもりであれば該当することもあり得ます)。
上記は、あくまでも、malloc + memoryが使える状況であれば、わざわざ2つの関数に分けるのではなく、callocを使えばよいのでは、ということです。

mbstate_t型をmallocやcalloc等で割りつける状況はそう多くありませんが、どういう割付け方法をとるにせよ、本来であれば、

mbstate_t state;
mbrtowc(NULL, "", 1, &state);

のようにして初期化するのが筋です(シフトシーケンスを使わないことがわかっていれば、ここまでしなくてもよいとは思いますが)。

mbstate_t型に限らず、内部表現が明確になっていない型の場合は、それを初期化する関数やマクロがふつうは用意されていますし、用意すべきですね。

補足

mbstate_t型の場合、ゼロクリアしたとしてもそれが初期変換状態とは限りませんので、ゼロクリア自体が不要な典型的な例かと思います。訂正:値0を持つmbstate_t型は初期状態でした。
多次元配列の初期化では、mbstate_t型の変数をゼロクリアする例を挙げましたが、それはあくまでも内部表現が分からない型の変数をゼロクリアする方法であって、それで初期変換状態になるという話ではありません。

{}では十分ではない?

集成体の0初期化は A a[10] = {};で十分だと思っていましたが、
文法を見た所、これが通用するのはC++だけのようですね。

VC++だと A a[10] = { 0

VC++だと
A a[10] = { 0 };
も結局memset呼ぶ出しになるんですよね
しかもpragma intrinsic指定でも関数呼び出しになってCRT使わないコード書いてるとき悩む悩む

コメントありがとうございます。

> A a[10] = { 0 };
> も結局memset呼ぶ出しになるんですよね

確かにそういう処理系は少なくありません。
他に、構造体の代入時に memcpy を使うものとか。
なので、memset を使うべきかどうかはコンパイラ任せにすればよいのです。

memsetを使う人のコードを疑うなら、 memsetを使

memsetを使う人のコードを疑うなら、
memsetを使う処理系も疑うべきでは?

memsetを使う処理系

初期化にmemsetを使う処理系では、memsetで処理されるわけですが、それはmemsetを使って初期化しているプログラマとたまたま意見が一致しているだけのことです。

意見が一致しているので、memsetの利用による予期せぬ弊害は恐らく起きないと思います。

しかし、memsetを使っていない処理系の場合、予期せぬ弊害が現れるかもしれません。

そこで、処理系の実装を気にせず初期化したいのであれば、ライブラリが用意している初期化方法を利用するのが適切だということです。

独自に定義した構造体ならばメンバ毎に初期化していくのが安全でしょう。
比較の際もメンバ毎に比較するのが妥当でしょう。
そういった初期化や比較を行う関数等をセットで用意してあげるべきです。

使用者は、作成者が想定した手段で初期化、比較をすることで、作成者の意図した使い方ができるわけです。
作成者がmemsetを使ってくれと言っているのであれば、当然memsetを使うべきですし、そうでなければ「安易に」memsetを使うべきではないのです。

そうなると、結局のところmemsetによる初期化の出番は、パフォーマンスの問題を解決する最終手段として用いるぐらいにしかならないのではないでしょうか。

このエントリーを含むはてなブックマーク