質問: 質問 -- C++初心者です.ヘッダファイルの書き方について調べていたのですが,記事によって言っていることが違ったりしていました. 結局のところ,ヘッダファイルの構造をどのようにするのが良いのか,またそれをどのように判断しているのかを教えていただきたいです. 以下に調査した内容をまとめ,現状の理解をまとめましたので,これをもとにご回答いただけると幸いです. (個人で書いて使うような小さいプログラムでは,結局どう作っても自由という結論になりそうなので,「ある程度大規模なコードの開発」を前提にお答えいただけると助かります.) ### 1 ヘッダと実装を分ける目的 これについては,下記質問に議論されており, C\+\+ \- 【C\+\+】なぜヘッダと実装はわけるべきなのでしょうか(\.hに実装を書くことは邪道か)|teratail ざっくりまとめると以下のようになると思います. 1. [第三者提供時にコードを隠蔽するため]; 実装のソースを公開せずにビルド済みの(静的/動的)ライブラリとヘッダだけ提供すれば第3者が利用することができる.(C/C++はコンパイルするといわゆるネイティブコード(機械語+α)を出力するが,デバッグ用途で出力されたものを除けば,これに型情報は含まれないため,逆コンパイルできず,コードを隠蔽できる.) 2. [定義の重複を避けるため]; C++にはOne Definition Rule(ODR)があり,「宣言」は何度されもいいが,「定義」は2度されてはいけない.複数ソースから呼び出されるヘッダファイルに定義が書かれていると,重複定義が起こる.(ただしtemplateとinline関数はODRの例外.そして,class定義内でメンバ関数を定義した場合は暗黙にインライン関数となるため,これもODRの対象外.) 3. [コンパイル時間短縮のため]; ヘッダと実装を分けていないがために,一部しか変更していないのに全てコンパイルし直す必要が出てくる状況が起こる. ### 2 注意しなければならない問題 1. 循環参照 2. 重複定義 3. コードの管理性・可読性 4. コンパイル速度 5. 実行ファイルの動作速度 これらについて,いくつかの記事で以下のような記述がありました. * 重複定義を避けるために,ヘッダファイルではINCLUDEガード(#ifndef)をする. * しかしながらこれも万能ではなく,ヘッダファイルの中で他のヘッダファイルを複数インクルードしているときなどでは,参照エラーが出たりする事がある. * それを避けるため,プリコンパイル済みヘッダー(ヘッダー宣言ヘッダーファイル)を作る方法がある.(【C\+\+】ヘッダファイルとcppファイルの事故らない扱い方\(2/2\)) * しかしながらこの方法は,循環参照を起こす温床になる.(ヘッダーファイルは慎重に扱わないと危険です) * そもそもプリコンパイル済みヘッダーは,VC++やVisual Studioなどの統合環境でおなじみの1ヘッダー1クラスの形式においてコンパイルを楽にする方法である.しかしこの1ヘッダー1クラスの形式はプログラムが大規模になるほど管理が大変であり,ある程度共通のカテゴリで1ヘッダーにいくつかのクラスを書くほうが良い.(ヘッダーファイルは慎重に扱わないと危険です) また, * 基本に忠実に,クラスの定義はヘッダファイル,メンバ関数の定義はソースファイルとすると,動作が遅くなる.(C\+\+ ヘッダとソースでファイルを分ける 基本編 \- Qiita) * それを避けるため,ヘッダファイル内でできるだけinline関数で実装を書いてしまったほうがいい.(C\+\+ ヘッダとソースでファイルを分ける 基本編 \- Qiita) * しかしながら,これはコードの可読性を落とす原因になる(ヘッダに実装を書いたか,ソースに書いたかわからなくなる)上,1の1.で述べたようにコードを隠蔽したい場合には使えない方法である. ### 3 ではヘッダファイルをどう書くか? 1でまとめたような理由から,特に大規模なコードではヘッダファイルに実装は書かないほうが良いことはわかりました. そしてヘッダファイル内で別のヘッダファイルを読み込むことは,2のような問題を引き起こす原因になるため,避けられるなら避けたい(できるだけ少なくしたい)こともわかりました. では,これらを踏まえて2の問題を全て解決するにはどのようにすればよいのでしょうか? (もしくはそのような唯一の方法はなく,ある種のトレードオフになるのでしょうか.) 以上長文になりましたが,ご回答いただけると幸いです. よろしくお願いいたします. 【追記】ご回答頂いた内容のまとめ ---------------- #### 基本方針 * (1)重複定義,(2)循環参照をなくすというMUSTな課題を解決しつつ,可能な限り(3)コードの管理性・可読性,(4)コンパイル速度,(5)実行ファイルの速度を意識したコーディングを行う. * (3)-(5)はトレードオフであり,要求に合わせてコーディングを変える. #### (1)重複定義 重複定義は必ず避けられる問題.以下をしっかりやれば,重複定義は避けられる場合が多い. * ヘッダと実装を分離する * インクルードガードをきっちり書く * ヘッダ内で依存先ヘッダのインクルードを正しく指定する ヘッダと実装を分離しておいたほうが良い理由は, ヘッダで実体を定義した場合,複数の.cppからそのヘッダをインクルードすると,それぞれの.cppでその実体が定義されてしまい,重複定義となるから.(C\+\+ \- リンクエラー:既に定義されている|teratail) しかし,ヘッダに実装を書くことも往々にしてある.その場合は上記のような問題が起こることを認識し,相応の対応を取れば良いだけ. 重複定義を避ける方法の一つとして重要なのが,inline関数/変数である. * cpp17book/035\-cpp17\-core\-inline\-variables\.md at master · EzoeRyou/cpp17book ##### ※inlineに関する補足(上記記事より) かつてinline関数の意味は,関数を強制的にインライン展開させるための機能だった.昔のコンパイラー技術が未熟だった時代のC++コンパイラーは関数をインライン展開するべきかどうかの判断ができなかったため,inlineキーワードが追加された.インライン展開してほしい関数をinline関数にすることで,コンパイラーはその関数がインライン展開するべき関数だと認識した. しかしコンパイラが賢くなった現代ではむしろ,ODRの回避の目的で使用することが多い. #### (2) 循環参照 循環参照の問題を語る場合は,以下の2つを区別することが大切. * バグによる循環参照 * 意図的な避けられない循環参照 意図的な循環参照の解決方法は,例えば以下の様な記事を参照. * 一週間で身につくC\+\+言語の基本\|第2日目:クラス間の相互参照 #### (3)コードの管理性・可読性 (4),(5)がそれほど問題にならないのであれば,最も優先したい項目. #### (4)コンパイル速度 これは,ヘッダと実装を分けておくことの大きなメリットの一つ. 他には,pImplも(一番の目的は隠蔽だと思いますが)コンパイル速度を上げるテクニックの一つ. ただしpImplは * ソースコードが複雑になる * 実行速度が少し落ちる などのデメリットもあり,注意が必要. #### (5)実行ファイルの速度 これは最初から意識するものというよりは,速度が問題になったときに対応すれば良い. ヘッダと実装を分けた事による速度の違いは通常無視できる程度に小さいし,inline関数でヘッダに書いたほうが速いというのも対応策の一つというだけで,最初から考えるべきものでも無い. 問題になったときは,プロファイルによりボトルネックを探すなどして対応する.
回答: > > 「ある程度大規模なコードの開発」を前提に... > > > そのような場面では,個人的な見解では, * 具体実装を隠蔽する * 不要なヘッダのインクルードを避ける 等して,とにかく不必要な依存関係を撒き散らさないようにすることが最優先です. (1の1や,1の3 に該当するでしょうか) このようなことを全く考えないメンバがいる場を経験すれば,本当にうんざりするほど理解できると思います. inlineか否かで速度差が...なんてことを考えねばならない状況がどれほどあるのか不明ですが, そういうのは,本当に必要になってから(そこまで切り詰めないと「遅い」という状況にならない限り)考えるようなことではないと思います. その他,循環とか重複がどうのとかいう事柄は「単なるバグ」であって,書き方の指針に関することではないと思います.