質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

89.52%

C言語でファイル読み込みをするときのベストプラクティスを教えてください。

解決済

回答 5

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 657

maic

score 6

前提

BITMAPのファイルの読み書きをしようとしています。
書籍やgithubをながめていると、「ヘッダーのメンバ毎に変数を定義して読み込む方法」をよく見かけます。

// メンバ毎に変数を定義し読み込む
unsigned int bitmap_size;
fread(&bitmap_size, 4, 1, fp);
// 以降、メンバの数だけ似たような記述が増える

課題

上記の記述だとヘッダーの数だけ行数が増えていき、冗長に感じたので、事前に定義していた構造体のサイズ分メモリ空間を用意して一気に読み込もうとしました。

BITMAPINFOHEADER bitmapInfoHeader;
fread(&bitmapInfoHeader, sizeof(BITMAPINFOHEADER), 1, fp);


ですが、構造体のメモリアラインメントのせいでBITMAPINFOHEADER構造体 BITMAPFILEHEADER構造体のサイズがメンバーサイズの合算値にならないため、期待したサイズよりも多く読み込んでいるようです。
この方法では読み込みだけでなく書き込みでも同様にパディング分ゴミが入ってしまいます。

追記:実際にはBITMAPFILEHEADERが14byteから16byteにアラインメントされてしまったため、BITMAPINFOHEADERの読み込みがずれてしまっていたようです。

上記の課題を避けるために#progma packなどでアラインメントの最適化をキャンセルすることができるとは思うのですが、いまいちこれがクリーンコーディングと言えるのか、大規模なOSS開発で是とされているのか、なにぶんC/C++での開発経験が乏しい私には判断がつきませんでした。

教えてほしいこと

今回はC言語で書いていますが、モダンで大規模なOSSのC++プロジェクトだと前提を置いたとして
BITMAPに限らず、ファイルの読み書きを行うときには多少冗長ながらも、最初に述べた「ヘッダーのメンバ毎に変数を定義して読み込む方法」が理想的(or よく使われる書き方)なのでしょうか?

他にも、C/C++で開発をしている方で美しい書き方を知っていればぜひ教えてください。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • yominet

    2019/06/02 19:09

    ›構造体のメモリアラインメントのせいでBITMAPINFOHEADER構造体のサイズがメンバーサイズの合算値にならないため、

    ›期待したサイズよりも多く読み込んでいるようです。

    このあたり、どういうソースを書き、どういう結果なり、
    そして、その結果に対してどう調査して、この結論にいたりましたか?

    キャンセル

  • maic

    2019/06/02 19:43 編集

    質問ありがとうございます。
    INFOHEADERに関しては、回答いただいた通りアラインメントが取れていて、
    実際にはその前に読み込んでいたFILEHEADERのアラインメントが悪さをしていることがわかりました。

    ```c
    // WORD : unsigned short (2byte)
    // DWORD : unsigned long (4byte)
    typedef struct tagBITMAPFILEHEADER {
    WORD bfType;
    DWORD bfSize;
    WORD bfReserved1;
    WORD bfReserved2;
    DWORD bfOffBits;
    } BITMAPFILEHEADER, *PBITMAPFILEHEADER;
    ```

    ```c
    // うまくいかない例
    // printf("output: %d\n", sizeof(BITMAPFILEHEADER)) // output: 16 (メンバの合計サイズは14)
    BITMAPFILEHEADER bitmapFileHeader;
    fread(&bitmapFileHeader, 14, 1, fp);
    ```
    `bfType`が4バイトアラインメントにより、2バイト分追加されているようです。
    その結果、その後の処理もずれてしまっていました。
    また、当然ではありますがFILEHEADERを以下のように各メンバーずつ読み込んでいくと、アラインメントを気にせず格納できました。INFOHEADERに関しては一気に読み込むことはできました。
    FILEHEADERのようにアラインメントが取れていないものに関しては一つずつ読むしかないのでしょうか。

    ```
    fread(&(bitmapFileHeader.bfType), 2, 1, fp);
    fread(&(bitmapFileHeader.bfSize), 4, 1, fp)
    ```

    キャンセル

回答 5

checkベストアンサー

+1

こんにちは。

BITMAPINFOHEADERはアライメントは取れているので、通常はパディングは発生しないはずです。

事前に定義していた構造体のサイズ分メモリ空間を用意して一気に読み込もうとしました。

私が見たことがあるサンプルは大抵この方法でリード/ライトしていますし、私自身もそのようにすることがあります。

もしかして、BITMAPFILEHEADERとBITMAPINFOHEADERをもつ構造体を定義して、そこへ読み込もうとするとおっしゃる問題が発生すると思います。

BITMAPに限らず、ファイルの読み書きを行うときには多少冗長ながらも、最初に述べた「ヘッダーのメンバ毎に変数を定義して読み込む方法」が理想的(or よく使われる書き方)なのでしょうか?

個人的にはこれはバグを生みやすいので「無し」です。

C/C++では、データ交換についてはどうしてもCPUの特性を把握して処理系毎に専用のコードを書く必要がある場合が多いです。(特にエンディアン)
アライメントもその一つと思います。msvcでは #progma pack を使うのは落とし所のように感じます。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/06/02 21:17

    maicさん
    回答でリンクした先のサンプル・ソースではBITMAPFILEHEADERも普通に読んでます。恐らくWindows.h側で #pragma pack しているだろうと思います。これにより「普通」に読めるはずです。
    また、サンプル・ソースとご自身のプログラムとの相違を確認してみるのも手と思いますよ。

    キャンセル

  • 2019/06/02 21:38

    @pepperleaf
    様々な実装のコンパイラや環境が乱立していた昔に、激しい環境依存と戦っていたプログラマたちは本当にすごいですね。
    そんな彼らが築き上げてきたものを知るためにも、今になってC言語の勉強を始めました。

    キャンセル

  • 2019/06/02 21:44

    @Chironian
    リプライ、本当にありがとうございます。
    やはり#pragma packでアライメントの設定を切り替えていたのですね。
    おっしゃるように自分のコードと見比べてみてサンプルのように「普通」?に読み込むことができず調べていたら下記のような記事を見つけました。
    https://www.mm2d.net/main/prog/c/image_io-05.html

    pragma packなどでアライメント設定をいじるか、一つずつ読み込むか、bufferに一度置くかの選択肢になるのですね。
    とても勉強になりました。ありがとうございます。

    キャンセル

+1

とりあえず、思いついたOSSなBMP読み込みについて調べた所
opencvは1要素ずつ読み込み
ImagaMagickも1要素ずつ

プラットフォームに縛られない必要がある場合は1要素ずつになるような気がします。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/06/04 21:45

    回答ありがとうございます。
    リンクまで貼っていただけて、本当に助かります。
    やはりプラットフォームの問題なのですね。
    今回質問したことで、なぜこのような書き方をするのか理由がわかったので、とても納得して書けそうです。

    キャンセル

+1

メモリアライメントの問題は、CPUのアーキテクチャに依存しますから、「使っているCPUに合わせる(合わせざるを得ない)[と諦める]」のがベストプラクティスだと思います。

どんなにソフトで頑張ってみても、CPUチップ内部の素子と配線で決まっているエンディアンを変えることはできませんから。
16bitのCPUが出てきたとき、32bitのCPUが出てきたとき、64bitのCPUが出てきたとき等に、多くの先達が苦労して出した現実解が冗長に見えるコードだと思います。

エンディアンが異なるCPUがあって、それらに対応しようとすれば1種類のCPUを対象とした綺麗なコードのようにはなりません。
異なるCPUで使えるようにするには、異なる部分に対応するために冗長な部分が必要になりますからね。

CPUのアーキテクチャの種類は、より高い性能を目指して今後も増えていきます。
同じコアでも高い演算能力を目指したもの、低消費電力を目指したもの、特殊機能を目指したもの(AI用エッジ等)などなど、に対応するには綺麗事では済まないと思います。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/06/04 21:40

    回答ありがとうございます。
    > メモリアライメントの問題は、CPUのアーキテクチャに依存しますから、「使っているCPUに合わせる(合わせざるを得ない)[と諦める]」のがベストプラクティスだと思います。

    なるほど。正直Cを始めるまで、この言語ががこれ程までにCPUやメモリについて考える必要があるとは思ってもみませんでしたし、冗長に見えていたコードにも実は意味があるということを今回の体験で知れました。ありがとうございます。

    キャンセル

0

まずはヘッダとか考えないで、ある程度の容量のバッファを用意して、そこに一括して読み込み、そこからヘッダなどのデータを読み出し、分割していけばいいです

直接ヘッダを読み出す、という考え方は捨てるほうがいいです

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/06/02 20:14 編集

    回答ありがとうございます。
    大変恐縮ですが、回答内容で理解できていない部分があり、質問させていただきます。

    > まずはヘッダとか考えないで、ある程度の容量のバッファを用意して、そこに一括して読み込み

    ヘッダを考慮しないというのは、たとえばとりあえず200byte分だけ読み込んでその中でヘッダーをパースしていき、ヘッダーの終端が分かった段階で実データのボディ先頭までfseekして、読み込むということでしょうか?
    あらかじめ拡張子などの情報によりヘッダサイズにはあたりがついていると思うのでその分を読み込むほうが事故を生みにくいような気がするのですが、どうでしょうか。

    > 直接ヘッダを読み出す
    直接読み出すというのは「課題」の部分で言及した
    `fread(&bitmapInfoHeader, sizeof(BITMAPINFOHEADER), 1, fp);`
    のように構造体のサイズ分読み込んでしまうやり方ということでしょうか?


    バッファをに関しては、以下のようにバイト配列を用意して
    `unsigned char header_buf[HEADERSIZE];`
    `header_buf[0]`のように値にアクセスし、説明変数(or 構造体)に代入するような流れになるイメージで大丈夫ですか?
    例となるようなコードがあればURLなど教えていただけると幸いです。

    キャンセル

  • 2019/06/02 23:16

    ヘッダ部分を直接ヘッダ構造体に読み込む、というのは、例えばエンディアンが違うCPUに変わったときに破綻します。また、アライメントの違うCPUに変わったときに修正を余儀なくされたりもします。
    そんなことでコードに手を加えなければならないような作りにするというのはまずいです。
    アライメントが違おうが、エンディアンが違おうがきちんと動作するコードを書くのがベストだとは思いませんか。
    そのためには、読み込む場合にはバイト列として読み込み、関数なりを介してヘッダ構造体へ展開するようにしましょう
    で、ヘッダ分だけを読み、必要に応じてBODY分までシークして読む、というのは、コード量からしても速度の点から見ても全くメリットはありません。

    キャンセル

  • 2019/06/04 21:50

    ご返信大変ありがとうございます。
    > アライメントが違おうが、エンディアンが違おうがきちんと動作するコードを書くのがベストだとは思いませんか。
    本当にその通りですね。直接ヘッダを読み込むやり方がバグを生むという事もご意見いただけなければわからなかったことですし、それが環境によってどれだけ問題を起こすのかも理解できました。
    不躾な質問にも丁寧にご回答くださりありがとうございました。納得しながら実装できそうです。

    キャンセル

0

うーん、やっぱりバイナリをそのまま扱うのが間違いな気がする。JSONで扱うべきではと。(現代のPCでJSON読み込みが遅くて問題になることはない)。いやProtocol Bufferあるやろという声も聞こえそうですが個人的にはうーんという思い。

文字列ならpaddingもendianもその他諸々も関係ないですしね。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2019/06/04 21:24

    回答ありがとうございます。
    > やっぱりバイナリをそのまま扱うのが間違いな気がする。JSONで扱うべきではと。
    まったくその通りだと思います。ハッシュテーブルは基本的にO(1)でしょうし仰る通り、読み込みの遅さは気にならなそうです。
    ただ、このプログラムは自分のCSへの理解度を上げるために書いている部分が多く、CPUの気持ちを分かりたいと思って書き始めました。こういった部分についても質問文の中で言及するべきでした。
    Protocol Bufferについては全く知らなかったので新しい発見になりましたありがとうございます!

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 89.52%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

同じタグがついた質問を見る