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

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

ただいまの
回答率

88.60%

結局C++でヘッダファイルはどう作るのがいいのか

解決済

回答 4

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 3,627

nishiys

score 33

質問

C++初心者です.ヘッダファイルの書き方について調べていたのですが,記事によって言っていることが違ったりしていました.
結局のところ,ヘッダファイルの構造をどのようにするのが良いのか,またそれをどのように判断しているのかを教えていただきたいです.

以下に調査した内容をまとめ,現状の理解をまとめましたので,これをもとにご回答いただけると幸いです.

(個人で書いて使うような小さいプログラムでは,結局どう作っても自由という結論になりそうなので,「ある程度大規模なコードの開発」を前提にお答えいただけると助かります.)

① ヘッダと実装を分ける目的

これについては,下記質問に議論されており,
C\+\+ \- 【C\+\+】なぜヘッダと実装はわけるべきなのでしょうか(\.hに実装を書くことは邪道か)|teratail
ざっくりまとめると以下のようになると思います.

  1. [第三者提供時にコードを隠蔽するため]; 実装のソースを公開せずにビルド済みの(静的/動的)ライブラリとヘッダだけ提供すれば第3者が利用することができる.(C/C++はコンパイルするといわゆるネイティブコード(機械語+α)を出力するが,デバッグ用途で出力されたものを除けば,これに型情報は含まれないため,逆コンパイルできず,コードを隠蔽できる.)
  2. [定義の重複を避けるため]; C++にはOne Definition Rule(ODR)があり,「宣言」は何度されもいいが,「定義」は2度されてはいけない.複数ソースから呼び出されるヘッダファイルに定義が書かれていると,重複定義が起こる.(ただしtemplateとinline関数はODRの例外.そして,class定義内でメンバ関数を定義した場合は暗黙にインライン関数となるため,これもODRの対象外.)
  3. [コンパイル時間短縮のため]; ヘッダと実装を分けていないがために,一部しか変更していないのに全てコンパイルし直す必要が出てくる状況が起こる.

② 注意しなければならない問題

  1. 循環参照
  2. 重複定義
  3. コードの管理性・可読性
  4. コンパイル速度
  5. 実行ファイルの動作速度

これらについて,いくつかの記事で以下のような記述がありました.

  • 重複定義を避けるために,ヘッダファイルではINCLUDEガード(#ifndef)をする.
  • しかしながらこれも万能ではなく,ヘッダファイルの中で他のヘッダファイルを複数インクルードしているときなどでは,参照エラーが出たりする事がある.
  • それを避けるため,プリコンパイル済みヘッダー(ヘッダー宣言ヘッダーファイル)を作る方法がある.(【C\+\+】ヘッダファイルとcppファイルの事故らない扱い方\(2/2\)
  • しかしながらこの方法は,循環参照を起こす温床になる.(ヘッダーファイルは慎重に扱わないと危険です
  • そもそもプリコンパイル済みヘッダーは,VC++やVisual Studioなどの統合環境でおなじみの1ヘッダー1クラスの形式においてコンパイルを楽にする方法である.しかしこの1ヘッダー1クラスの形式はプログラムが大規模になるほど管理が大変であり,ある程度共通のカテゴリで1ヘッダーにいくつかのクラスを書くほうが良い.(ヘッダーファイルは慎重に扱わないと危険です

また,

③ ではヘッダファイルをどう書くか?

①でまとめたような理由から,特に大規模なコードではヘッダファイルに実装は書かないほうが良いことはわかりました.
そしてヘッダファイル内で別のヘッダファイルを読み込むことは,②のような問題を引き起こす原因になるため,避けられるなら避けたい(できるだけ少なくしたい)こともわかりました.

では,これらを踏まえて②の問題を全て解決するにはどのようにすればよいのでしょうか?
(もしくはそのような唯一の方法はなく,ある種のトレードオフになるのでしょうか.)

以上長文になりましたが,ご回答いただけると幸いです.
よろしくお願いいたします.

【追記】ご回答頂いた内容のまとめ

基本方針

  • (1)重複定義,(2)循環参照をなくすというMUSTな課題を解決しつつ,可能な限り(3)コードの管理性・可読性,(4)コンパイル速度,(5)実行ファイルの速度を意識したコーディングを行う.
  • (3)-(5)はトレードオフであり,要求に合わせてコーディングを変える.

(1)重複定義

重複定義は必ず避けられる問題.以下をしっかりやれば,重複定義は避けられる場合が多い.

  • ヘッダと実装を分離する
  • インクルードガードをきっちり書く
  • ヘッダ内で依存先ヘッダのインクルードを正しく指定する

ヘッダと実装を分離しておいたほうが良い理由は,
ヘッダで実体を定義した場合,複数の.cppからそのヘッダをインクルードすると,それぞれの.cppでその実体が定義されてしまい,重複定義となるから.(C\+\+ \- リンクエラー:既に定義されている|teratail

しかし,ヘッダに実装を書くことも往々にしてある.その場合は上記のような問題が起こることを認識し,相応の対応を取れば良いだけ.

重複定義を避ける方法の一つとして重要なのが,inline関数/変数である.

※inlineに関する補足(上記記事より)

かつてinline関数の意味は,関数を強制的にインライン展開させるための機能だった.昔のコンパイラー技術が未熟だった時代のC++コンパイラーは関数をインライン展開するべきかどうかの判断ができなかったため,inlineキーワードが追加された.インライン展開してほしい関数をinline関数にすることで,コンパイラーはその関数がインライン展開するべき関数だと認識した.
しかしコンパイラが賢くなった現代ではむしろ,ODRの回避の目的で使用することが多い.

(2) 循環参照

循環参照の問題を語る場合は,以下の2つを区別することが大切.

  • バグによる循環参照
  • 意図的な避けられない循環参照

意図的な循環参照の解決方法は,例えば以下の様な記事を参照.

(3)コードの管理性・可読性

(4),(5)がそれほど問題にならないのであれば,最も優先したい項目.

(4)コンパイル速度

これは,ヘッダと実装を分けておくことの大きなメリットの一つ.
他には,pImplも(一番の目的は隠蔽だと思いますが)コンパイル速度を上げるテクニックの一つ.
ただしpImplは

  • ソースコードが複雑になる
  • 実行速度が少し落ちる

などのデメリットもあり,注意が必要.

(5)実行ファイルの速度

これは最初から意識するものというよりは,速度が問題になったときに対応すれば良い.
ヘッダと実装を分けた事による速度の違いは通常無視できる程度に小さいし,inline関数でヘッダに書いたほうが速いというのも対応策の一つというだけで,最初から考えるべきものでも無い.

問題になったときは,プロファイルによりボトルネックを探すなどして対応する.

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

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

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

    クリップを取り消します

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

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

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

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

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

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

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

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

    質問の評価を下げる

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

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

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

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

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

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

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

    詳細な説明はこちら

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

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

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

回答 4

checkベストアンサー

+9

こんにちは。

これは、人によって見解が異なると思いますので、私の見解を書いてみます。

① ヘッダと実装を分ける目的

概ね同じ意見です。

② 注意しなければならない問題
重複定義を避けるために,ヘッダファイルではINCLUDEガード(#ifndef)をする.

ここまで同じ意見ですが、これ以降については同意できません。

しかしながらこれも万能ではなく,ヘッダファイルの中で他のヘッダファイルを複数インクルードしているときなどでは,参照エラーが出たりする事がある.

これはインクルード・ガードの問題ではなく単なるバグでしょう。

それを避けるため,プリコンパイル済みヘッダー(ヘッダー宣言ヘッダーファイル)を作る方法がある.

えっ! プリコンパイル済ヘッダーは、コンパイル速度を上げるための仕組みです。
インクルード・ミスを回避するための方法ではありません。リンク先の人は勘違いしているようです。

そもそもインクルードの順序を調整しないといけないという問題は、依存先ヘッダのインクルードをサボるから発生する問題です。ちゃんと全てのヘッダが自分が依存する他のヘッダをインクルードしておけば、インクルードの順番を気にする必要はありません。そのためにインクルード・ガードすることが必須となります。

もう一つのリンク先にある「循環参照」問題はヘッダのインクルードとは無関係です。
クラスAとクラスBを循環参照したい時、コンパイラに文句を言わせないためにどのように宣言すればよいのか?の問題です。少なくとも片方は前方宣言してポインタか参照で「参照」し、cpp側で中身にアクセスするのが定番ですが、必ずしもそうしなければ行けないというものではありません。

基本に忠実に,クラスの定義はヘッダファイル,メンバ関数の定義はソースファイルとすると,動作が遅くなる

これ以降の話は、確かにその傾向はありますが、たかが知れていますので事実上無視して良いと思います。
ギチギチに高速化するような時だけ気にすれば十分と思います。

特に大規模なコードではヘッダファイルに実装は書かないほうが良いことはわかりました.

そんなことは全くないです。それをいっちゃうと 標準ライブラリに採用されることも少なくない boost が泣くかも。
とはいえ、ヘッダに大きな関数を書かないで済む時は書かない方が、プログラマに優しいです。

ヘッダファイル内で別のヘッダファイルを読み込むことは,②のような問題を引き起こす原因になる

原因と結果がひっくり返っています。ヘッダ・ファイル内で依存するヘッダをインクルードしないから、そのような問題が起こるのです。

②の問題を全て解決するにはどのようにすればよいのでしょうか?

そのような解は存在しません。常にトレードオフです。

重複定義は面倒なケースもありますが、必ず回避できます。(最近はinline変数も有りますので、可読性を改善できそうです。)
循環参照は頑張るのみです。当然ですが、バグによる循環参照はデバッグします。
意図的に循環参照する時、cpp側で実装し辛いテンプレート同士で循環参照する時は地獄を見ます。ヘッダ分割とか気にしないでコンパイルできることを目指します。(私のキャパではなりふり構ってられないです。)

上記2つの解決は必須です。それ以外の3つについてはトレードオフです。私はコードの管理性・可読性を最優先とし、特に高速化する必要がある部分のみ実行速度を優先します。コンパル速度も気にする余裕がある時には気にします。(可能な時は大き目の関数はcpp側に書く)

投稿

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

  • 2019/06/23 19:19

    大変丁寧なご回答誠にありがとうございます.特に②,③について,勘違いしていた部分が多かったようですね.非常に勉強になりました.

    追加でご質問なのですが
    1. 「バグによる循環参照」は
    * ヘッダと実装をしっかり分ける(ヘッダに実装を書くと循環参照のバグが起こりやすい?)
    * インクルードガードをしっかり書く
    * ヘッダ内で依存するinclude先をしっかり書く
    をやれば必ず避けられる問題である.

    2. 「意図的な循環参照」は
    * クラスの前方宣言
    * pImpl
    を使えば基本的に解決できる.

    3. テンプレートを使用した場合はヘッダにしか実装をかけないため,①の1.のようなコードを完全に隠蔽する必要のある場合にはテンプレートは使用できない.

    という理解で合っていますでしょうか?
    認識の間違いがあれば教えていただけると幸いです.

    キャンセル

  • 2019/06/23 19:38

    う~~ん、そんなにガチガチなスタイルはC++プログラミングに向かないです。
    臨機応変に対応しないと出来るのに出来ないと判断してしまうケースがより増えると思いますよ。(びっくりすることが出来てしまうことが意外にありますから。)

    誤>をやれば必ず避けられる問題である
    正>をやれば避けやすい問題である

    pImplとは無関係な循環参照もあると思います。(例えば、相手のインスタンスを自身の中に保持しないようなケース)

    誤>テンプレートを使用した場合はヘッダにしか実装をかけないため、
    正>テンプレートを使用した場合はヘッダにしか実装をかけない場合が多いため、

    キャンセル

  • 2019/06/23 20:20

    ご回答+アドバイスありがとうございます.大変勉強になりました.
    「こうしたほうがバグを避けやすい,解決できる場合が多い」という前提知識は付けつつ,それに固執せず柔軟に対応できるうよう勉強していきたいと思います.

    キャンセル

+2

詳しい回答は他の方に譲るとして…

結局のところ「ヘッダーファイルを使った方が便利だよね」
と思われているから使われているのであって、その便利さは
使う人によって、また対象案件によっても違ってくるでしょう。

そういうのをまとめるためにコーディング規約やその他の
決めごとがあるわけですし、それに従って作るわけですよね。
(その「決め事」の是非はまた別の問題ですのでここではスルー)

ご質問をみて思ったのは、もっと柔軟なヘッダーファイルの
使い方を知れば、また考え方も変わってくるのでは? です。
インターネットでは数多のソースコードが公開されている
訳ですし、それを調べれば勉強になることでしょう。

ただ、場当たり的に見て行っても(時間は有限ですし)
飽きるでしょうから、そこは先達に見ておくべきものを
紹介していただくのは有用だと思います。
(それをどうするか判断するのは自分ですけどね :-)

投稿

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

  • 2019/06/23 18:08

    ご回答ありがとうございます.勉強を始めたばかりなので,これから様々なヘッダファイルの使い方を見て学んで行きたいと思います.アドバイス誠にありがとうございます.

    >そこは先達に見ておくべきものを紹介していただくのは有用だと思います。

    もし有用な資料等ご存じでしたら,教えていただけると幸いです.

    キャンセル

+2

「ある程度大規模なコードの開発」を前提に…

そのような場面では,個人的な見解では,

  • 具体実装を隠蔽する
  • 不要なヘッダのインクルードを避ける

等して,とにかく不必要な依存関係を撒き散らさないようにすることが最優先です.
(①の1や,①の3 に該当するでしょうか)

このようなことを全く考えないメンバがいる場を経験すれば,本当にうんざりするほど理解できると思います.

inlineか否かで速度差が…なんてことを考えねばならない状況がどれほどあるのか不明ですが,
そういうのは,本当に必要になってから(そこまで切り詰めないと「遅い」という状況にならない限り)考えるようなことではないと思います.

その他,循環とか重複がどうのとかいう事柄は「単なるバグ」であって,書き方の指針に関することではないと思います.

投稿

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

  • 2019/06/23 18:03

    ご回答ありがとうございます.「具体実装の隠蔽」と「不要なヘッダのインクルードを避ける」ためにfanaさんはどのような方法をとられているか教えていただけませんか?
    僕の理解が正しければ,
    * クラスの前方宣言を用いる
    * (t_obaraさんに教えて頂いた)pimplを用いる
    などがあると思いますが...

    キャンセル

+1

個人的にはpimplがオススメです。

https://ja.cppreference.com/w/cpp/language/pimpl

上記記載の通り、実行ファイルの動作速度という点で不利ですが、
通常動作速度が問題になった時に気にすればよく、プロファイル
によりボトルネックを明確にして対処すべき事柄です。
※ そういう意味では「ある種のトレードオフ」とも言えます。

投稿

  • 回答の評価を上げる

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

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

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

  • 回答の評価を下げる

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

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

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

  • 2019/06/23 17:53

    pImplのことは知りませんでした.調べてみたところ,
    * ユーザ側が見ることになるヘッダファイルの内容がすっきりする。
    * 実装の詳細をユーザ側に見られることがなくなり、不正な方法でアクセスすることもできなくなる。
    * ヘッダのインクルードファイルが減るので、結合度が減る。
    * コンパイル時間が高速化される
    * バイナリ互換性が向上する
    * Pimpl は要求時に作ることもできるので、ネットワーク接続など制限のあるリソースやコストの高いリソースの割当のときに役に立つ。
    などのメリットがあるようですね.([簡単に Pimpl を使ってみる \- お茶漬けびより](http://pickles-ochazuke.hatenablog.com/entry/2017/07/20/135530)
    ありがとうございます.
    プロファイルについても,行ったことがないので今度大きいコードを書いたときに勉強してみたいと思います.

    キャンセル

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

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

関連した質問

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