Armadilloフォーラム

構造体の初期化について

fukasawa

2019年4月18日 11時39分

お世話になっております。
Armadillo実践開発ガイドを参考にC言語のプログラミングを習得中です。

ArmadilloというよりはC言語の質問になってしまうのですが、組み込み特有の理由もあるかもしれないので質問させてください。
ご回答いただけると幸いです。

Armadillo実践開発ガイド 第2部 p127の下記部分について質問があります。

```
static void set_sig_handler(int sig_list[], ssize_t num, __sighandler_t handler)
{
struct sigaction sa;

//ハンドラ関数を設定
memset(&sa, 0, sizeof(sa));
sa.sa_handler = handler;

以下略
```

このうちmemsetは構造体の値を0になるように初期化しているのだと思いますが、
そもそも構造体の初期値は0ではないのでしょうか、それとも処理系などに依存してしまうのでしょうか。

またC99(-std=gnu99)を使用する場合は、

```
struct sigaction sa = {.sa_handler = handler};
```

としてしまってもよろしいでしょうか。

よろしくおねがいします。

コメント

at_mizo

2019年4月18日 12時53分

溝渕です。

> このうちmemsetは構造体の値を0になるように初期化しているのだと思いますが、
> そもそも構造体の初期値は0ではないのでしょうか、それとも処理系などに依存してしまうのでしょうか。

bssセクションに配置されるグローバル変数等は0で初期化されます。

スタックに配置されるローカル変数の初期値は不定です。

> またC99(-std=gnu99)を使用する場合は、
>
> ```
> struct sigaction sa = {.sa_handler = handler};
> ```
>
> としてしまってもよろしいでしょうか。

sa_handler以外のメンバの値が不定となりますが、それで構わなければ良いです。

fukasawa

2019年4月18日 14時29分

回答ありがとうございました。

> スタックに配置されるローカル変数の初期値は不定です。
> sa_handler以外のメンバの値が不定となりますが、それで構わなければ良いです。

0ではなく、不定だったのですね。
おっしゃる通り、そのまま使うのはやはり危険な気もしますので、0初期化したい方がよいですね。
ところで`memset`の代用として`={0}`でもよろしいでしょうか。

```
struct sigaction sa = {0};
sa.sa_handler = handler;
```

at_mizo

2019年4月18日 14時41分

溝渕です。

> ところで`memset`の代用として`={0}`でもよろしいでしょうか。

先頭メンバのみ0で初期化されます。

全メンバが0で初期化されることを期待して書いているのであれば、意図した
挙動はしないと思います。

fukasawa

2019年4月18日 19時20分

> 先頭メンバのみ0で初期化されます。

これなのですが、やはり先頭メンバ以外も(グローバル変数と同じ値で)初期化されるのではないでしょうか。

JIS X 3010 (C99)から引用いたします(https://kikakurui.com/x3/X3010-2003-01.html)。

"集成体型の要素又はメンバの個数より波括弧で囲まれた並びにある初期化子が少ない場合,又は大きさが既知の配列の要素数よりその配列を初期化するための文字列リテラル中の文字数が少ない場合,その集成体型の残りを,静的記憶域期間をもつオブジェクトと同じ規則で暗黙に初期化する。"

また別のブログの記事からも引用します。(https://cpplover.blogspot.com/2010/09/blog-post_18.html)
"T x = {0} ;
これは、「Tの最初の要素あるいはメンバーを、0で初期化し、その他をすべて、staticストレージと同じ方法で初期化する」という意味である。staticストレージは、明示的に初期化子を欠かなくても、必ずzero-initializedされる。そのため、このような意味になる。"

また以下のコードをArmadillo上で実行してみました。

```
#include
#include

typedef struct
{
int x;
double y;
char s;
} Point;

int main(void)
{
Point p1;
Point p2 = {0};
Point p3 = {1,2,'A'};
Point p4;
memset(&p4, 0, sizeof(Point));

printf("p1 = (%i, %f, %c)\n", p1.x, p1.y, p1.s);
printf("p2 = (%i, %f, %c)\n", p2.x, p2.y, p2.s);
printf("p3 = (%i, %f, %c)\n", p3.x, p3.y, p3.s);
printf("p4 = (%i, %f, %c)\n", p4.x, p4.y, p4.s);
}
```

結果は以下です。

```
p1 = (-560, -0.000000, )
p2 = (0, 0.000000, )
p3 = (1, 2.000000, A)
p4 = (0, 0.000000, )
```

偶然かもしれませんが、想定している動作になります。

ただ気になるのは`arm-linux-gnueabi-gcc`を使った場合のみ、警告が出ます。

```
a.c:14:3: warning: missing initializer [-Wmissing-field-initializers]
a.c:14:3: warning: (near initialization for ‘p2.y’) [-Wmissing-field-initializers]
```

間違っているかもしれないのですが、確認いただけないでしょうか。
よろしくおねがいします。

hermes3m

2019年4月18日 20時56分

横から失礼します。

JIS規格に記載の通りであれば、おっしゃる通りに初期化できるのだと思います。
ただ、コンパイラは規格の通りに動作するものとは限りません(過去にもいくつか有名な
トラブルがありました)。特に移植の際には心配です。さらに、そのソースを読んだ人が
また同じような疑問を持ってしまうという心配などを考えれば、新しい規格にこだわる
必要はどれだけあるだろうかとも考えてしまいます。
実際、警告をだすコンパイラがあるということは、その記述方法で規格の通りに動作しない
環境が存在する可能性を心配するべきなのかもしれません。

私はそういう場合、新しい規格で動作するはずのコードと、新しい規格を使わないコードを
条件付きコンパイルで簡単に切り替えられるように2通り作って様子を見ます。
assertに相当する機能をつけてテストを綿密に行うことも考えられるでしょう。
もちろん、後からコードを読む人のためにコメントをちゃんと付けて。

こういう危機回避の手段を講じた上で使ってみるのはいかがですか。
「仕様書にこう書いてあるけど、ちょっと不安」というデバイスを使うのと同じでしょうかね。

> > 先頭メンバのみ0で初期化されます。
>
> これなのですが、やはり先頭メンバ以外も(グローバル変数と同じ値で)初期化されるのではないでしょうか。
>
> JIS X 3010 (C99)から引用いたします(https://kikakurui.com/x3/X3010-2003-01.html)。
>
> "集成体型の要素又はメンバの個数より波括弧で囲まれた並びにある初期化子が少ない場合,又は大きさが既知の配列の要素数よりその配列を初期化するための文字列リテラル中の文字数が少ない場合,その集成体型の残りを,静的記憶域期間をもつオブジェクトと同じ規則で暗黙に初期化する。"
>
> また別のブログの記事からも引用します。(https://cpplover.blogspot.com/2010/09/blog-post_18.html)
> "T x = {0} ;
> これは、「Tの最初の要素あるいはメンバーを、0で初期化し、その他をすべて、staticストレージと同じ方法で初期化する」という意味である。staticストレージは、明示的に初期化子を欠かなくても、必ずzero-initializedされる。そのため、このような意味になる。"
>
> また以下のコードをArmadillo上で実行してみました。
>
> ```
> #include
> #include
>
> typedef struct
> {
> int x;
> double y;
> char s;
> } Point;
>
> int main(void)
> {
> Point p1;
> Point p2 = {0};
> Point p3 = {1,2,'A'};
> Point p4;
> memset(&p4, 0, sizeof(Point));
>
> printf("p1 = (%i, %f, %c)\n", p1.x, p1.y, p1.s);
> printf("p2 = (%i, %f, %c)\n", p2.x, p2.y, p2.s);
> printf("p3 = (%i, %f, %c)\n", p3.x, p3.y, p3.s);
> printf("p4 = (%i, %f, %c)\n", p4.x, p4.y, p4.s);
> }
> ```
>
> 結果は以下です。
>
> ```
> p1 = (-560, -0.000000, )
> p2 = (0, 0.000000, )
> p3 = (1, 2.000000, A)
> p4 = (0, 0.000000, )
> ```
>
> 偶然かもしれませんが、想定している動作になります。
>
> ただ気になるのは`arm-linux-gnueabi-gcc`を使った場合のみ、警告が出ます。
>
> ```
> a.c:14:3: warning: missing initializer [-Wmissing-field-initializers]
> a.c:14:3: warning: (near initialization for ‘p2.y’) [-Wmissing-field-initializers]
> ```
>
> 間違っているかもしれないのですが、確認いただけないでしょうか。
> よろしくおねがいします。

izawa

2019年4月19日 9時52分

毎度お世話様、izawa@ittoです。老婆心ながら、Cの仕様に関することなので。

既にhermes3mさんに指摘されている通り、またfukasawaさんが調べた通り、初期化子が足りなくてもそのメンバーは0フィルされます。
記憶に間違いがなければ、c89の時点でそう規定されていた筈です。
# 何故か組み込み系は未だにmemset()したがる人が多いのですが。

ですので、gccやclangの眷属であれば勿論、そうでなくても余程変態なコンパイラでなければ問題ないと考えてよろしいかと。
# 逆にそのようなコンパイラを採用するようであれば採用者の神経を疑いますが。

その上で、サンプルコードなど参考にしたソースがmemset()を使っている場合などにどうするかはポリシーの問題です。
# 私なら、引用範囲をコメントで囲って敢えて手を付けないようにします。手を付ける場合は、引用にならないように徹底的にやりますが。

尚、C++の場合は構造体にコンストラクタを持たせることができますので、そのような場合はmemset()してはいけません。

y.nakamura

2019年4月19日 18時29分

中村です。

話が少し外れますが、質問があります。

> 尚、C++の場合は構造体にコンストラクタを持たせることができますので、そのような場合はmemset()してはいけません。

私は、"={0}"での初期化とmemset()での初期化と、
そのコードの状況によってどちらも使うことがあって、
この2つの方法でのゼロ初期化に限っては、
CかC++かを意識したことはありませんでした。

C++の場合、memset()を使うとどういう影響がありますか?
コンストラクタがなければmemset()でも問題はないと
考えて大丈夫でしょうか?
operatorを定義することもありますが、
これは大丈夫でしょうか?

以下は、投稿のついでに・・・

> 記憶に間違いがなければ、c89の時点でそう規定されていた筈です。

はい、たしかC89だったと思います。

> ですので、gccやclangの眷属であれば勿論、そうでなくても余程変態なコンパイラでなければ問題ないと考えてよろしいかと。
> # 逆にそのようなコンパイラを採用するようであれば採用者の神経を疑いますが。

同感です。

--
なかむら

izawa

2019年4月22日 14時53分

伊澤です。

> > 尚、C++の場合は構造体にコンストラクタを持たせることができますので、そのような場合はmemset()してはいけません。
>
> 私は、"={0}"での初期化とmemset()での初期化と、
> そのコードの状況によってどちらも使うことがあって、
> この2つの方法でのゼロ初期化に限っては、
> CかC++かを意識したことはありませんでした。
>
> C++の場合、memset()を使うとどういう影響がありますか?
> コンストラクタがなければmemset()でも問題はないと
> 考えて大丈夫でしょうか?
> operatorを定義することもありますが、
> これは大丈夫でしょうか?
>
件のコメントでは簡潔にするために端折って書きましたが、その辺りを細かく。
# 但し、C++11のinitializer_listに言及するとややこしいので割愛。

・初期化子(={0})による初期化
コンストラクタがあると、初期化子による初期化はできません。

・memset()による初期化
例えばコンストラクタでメンバーに値をセットしていた場合、memset()によってその値を潰されてしまう恐れがあります。
# 値だけならまだしも、コンストラクタでメモリ確保してそのポインタをメンバで保持していたら……

逆に初期化子を使える場合は、コンストラクタがないということですからmemset()しても大丈夫でしょう。

まとめると、memset()していいのは初期化子が使えるクラスのみ。なので、殆ど使う必要がないってことになります。

ファイル ファイルの説明
memset.cpp コメント内容を確認するためのサンプル

y.nakamura

2019年4月22日 17時21分

中村です。

伊澤さん、
丁寧な説明、ありがとうございます。

> ・memset()による初期化
> 例えばコンストラクタでメンバーに値をセットしていた場合、memset()によってその値を潰されてしまう恐れがあります。

ということは、コンストラクタがなければ、C++の場合も
memset()でゼロ初期化しても大丈夫ということですね。

> まとめると、memset()していいのは初期化子が使えるクラスのみ。なので、殆ど使う必要がないってことになります。

CでもC++でも・・・ですが、
先の私の投稿で、"={0}"とmemset()と、
その時々でどちらも使うことがあると書きましたので、
memset()をどういうときに使っているかを書いておきます。

たとえば、malloc()したものを初期化するときや、
静的に配置した構造体を後から(何かに使った後などに)
全クリアする必要があるときなどです。
(この投稿の最後に書くような方法もありますが...)

malloc()をcalloc()にすればmemset()でクリアする
必要はないですが、calloc()自体がmemset()で
ゼロクリアしているのと同じですよね。

あと、"={0}"を使うと警告がでるgccの場合もです。
できるだけ警告は消すようにしてますので。
(警告が出たまま放置する場合もありますけど)

"={0}"を使う例としては、宣言時にゼロクリアする他に、
malloc()したものや、すでに存在するものを
あとからゼロクリアするときに、
memset()ではなく"={0}"を使って、
次のような使い方をすることもあります。

void foo_clear(struct Foo *foo) {
  static const struct Foo foo0 = {0};
  *foo = foo0;
}

ここで引数で渡されるfooは、malloc()したものや、
何度も初期化する必要があるような変数です。

--
なかむら

izawa

2019年4月22日 18時16分

度々伊澤です。
私なんかが中村さんにあれこれ言うほどのこともありませんが。

ポリシーがしっかりしているのでしたら、memset()も悪くありませんね。
私自身は他人が書いたコードなどをデバッグする際に、memcpy()やmemset()は目の敵にしているものでw

staticなオブジェクトは0フィルされていることが保証されているので、
static const struct Foo foo0;のように初期化子を書かなくても事は足りますね。
# 可読性は少々悪くなりますが。

at_hanada

2019年4月22日 20時30分

花田です。

伊澤さんがご指摘の内容で様々な視点がカバーされており、異論はございません。

エビデンス的なもの(?)として、3年ほど前に話題になった下記文書がありまして。

How to C (as of 2016)
https://matt.sh/howto-c

日本語訳もあります。

2016年、C言語はどう書くべきか (前編) | POSTD
https://postd.cc/how-to-c-in-2016-1/
2016年、C言語はどう書くべきか (後編) | POSTD
https://postd.cc/how-to-c-in-2016-2/

「現代ではこう書こうぜ」という呼びかけなわけですが、後編の「C言語では自動割り当て構造体の静的初期化が可能」が今回の議論に関する箇所ですね。

以下余談。「何故か組み込み系は未だにmemset()したがる人が多いのですが。」に、大昔の環境を想定したローカルなコーディング規約的なものが残っている可能性を想像したものの。
IPAが発行した「改訂版・組込みソフトウェア開発向けコーディング作法ガイド[C言語版]」という古の書物を開き、冒頭「領域は、初期化してから使用する。」という項に目を通してみましたが、2007年時点でもC90よりは新しいANSI基準でGNU環境も想定しており、memset()しろとは書いておりませんでした(^^;

y.nakamura

2019年4月22日 20時39分

中村です。

伊澤さん、
コメントありがとうございます。

> staticなオブジェクトは0フィルされていることが保証されているので、

関数外のグローバルなところで宣言したときは、
デフォルトでゼロになるのを利用することもあります。
後からソース修正してゼロでない値を初期値にする可能性が
ある場合には、意識的にゼロを代入しています。
(この話は構造体かどうかに関係なく、です)

> static const struct Foo foo0;のように初期化子を書かなくても事は足りますね。
> # 可読性は少々悪くなりますが。

関数内でstatic宣言した変数もゼロフィルされるんですね。
これは自信がなかったので、値(0)をセットしてました。
可読性のことも、もちろんあるのですけど。

コンパイラ(リンカかな?)がその変数をどのセクションに
配置するかを考えれば、ゼロになって当然といえば当然なの
かもしれませんが。。。

--
なかむら

fukasawa

2019年4月23日 18時22分

みなさま。たくさんの情報をいただきありがとうございます。

> コンパイラは規格の通りに動作するものとは限りません(過去にもいくつか有名なトラブルがありました)。特に移植の際には心配です。

ご指摘はもっともですね。新しい言語規格などに関しては動作を注意深く見たいと思います。
ただ今回のmemsetに関してはC89からあるようですし、コンパイルの不具合<私のポカ、と考えて

```
struct sigaction sa = {.sa_handler = handler};
```

といたします。

https://postd.cc/how-to-c-in-2016-1/
"現時点では、Clangが正しいシンタックスに対して警告を出すことがあります。よって、-Wno-missing-field-initializersを追加すべきです。GCCではGCC4.7.0以後、この不必要な警告が修正されました。"

atde5の`arm-linux-gnueabi-gcc`のバージョンは4.6.3、`gcc`のバージョンは4.7.2なので、異なった警告が出るようです。
とりあえず`-Wno-missing-field-initializers`を追加してコンパイルすることにします。

またC++の使用も検討していたので、C++に関する情報も教えていただきありがとうございます。