ヘッダーの地獄を防ぐにはどうすればよいですか?


45

新しいプロジェクトをゼロから始めています。約8人の開発者、1ダースほどのサブシステム、それぞれ4つまたは5つのソースファイル。

「ヘッダー地獄」、別名「スパゲッティヘッダー」を防ぐために何ができますか?

  • ソースファイルごとに1つのヘッダーですか?
  • サブシステムごとにプラス?
  • 関数プロトタイプからtypdef、stucts、enumを分離しますか?
  • サブシステムの内部をサブシステムの外部のものから分離しますか?
  • ヘッダーまたはソースがスタンドアロンコンパイル可能である必要があるかどうかにかかわらず、すべての単一ファイルを主張しますか?

私は「最善の」方法を求めているのではなく、何に気を付け、悲しみを引き起こす可能性があるのか​​を示し、それを回避しようとすることができます。

これはC ++プロジェクトになりますが、C情報は将来の読者に役立つでしょう。


16
Large-Scale C ++ Software Designのコピーを入手してください。ヘッダーの問題を回避するだけでなく、C ++プロジェクトのソースファイルとオブジェクトファイル間の物理的な依存関係に関する多くの問題が発生します。
Doc Brown

6
ここでの答えはすべて素晴らしいです。オブジェクト、メソッド、関数を使用するためのドキュメントをヘッダーファイルに追加する必要があります。ソースファイルにまだドキュメントがあります。私にソースを読ませないでください。これがヘッダーファイルのポイントです。実装者でない限り、ソースを読む必要はありません。
ビル・ドアの

1
私はあなたと以前に働いたことがあると確信しています。しばしば:-(
Mawg

5
あなたが説明することは大きなプロジェクトではありません。良いデザインは常に歓迎されますが、「大規模システム」の問題に直面することはないでしょう。
サム

2
Boostには実際にはすべてを含むアプローチがあります。個々の機能には独自のヘッダーファイルがありますが、より大きなモジュールにはすべてが含まれるヘッダーもあります。これは、毎回数百個のファイルを強制的にインクルードすることなく、header-hellを最小限に抑えるために非常に強力であることがわかりました。
コートアンモン

回答:


39

簡単な方法:ソースファイルごとに1つのヘッダー。ユーザーがソースファイルについて知る必要のない完全なサブシステムがある場合は、必要なすべてのヘッダーファイルを含むサブシステムのヘッダーを1つ用意します。

ヘッダーファイルは、それ自体でコンパイルできる必要があります(または、単一のヘッダーを含むソースファイルをコンパイルする必要があるとしましょう)。どのヘッダーファイルに必要なものが含まれているかを見つけたら、他のヘッダーファイルを探し出す必要があります。これを強制する簡単な方法は、最初にすべてのソースファイルにヘッダーファイルを含めることです(doug65536に感謝します。ほとんどの場合、実現すらせずにそれを行います)。

使用可能なツールを使用してコンパイル時間を短縮してください-各ヘッダーを1回だけ含める必要があります。コンパイル時間を短縮するためにプリコンパイル済みヘッダーを使用し、可能な場合はコンパイル済みモジュールを使用してコンパイル時間をさらに短縮します。


難しいのは、他のサブシステムで宣言された型のパラメーターを使用したサブシステム間の関数呼び出しです。
Mawg

6
トリッキーであるかどうかにかかわらず、「#include <subsystem1.h>」をコンパイルする必要があります。それをどのように達成するかはあなた次第です。@FrankPuffer:なぜですか?
gnasher729

13
@Mawgこれは、別個のサブシステムの共通性を含む別個の共有サブシステムが必要であること、または各サブシステムの単純化された「インターフェース」ヘッダーが必要であることを示します。 。クロスインクルードなしでインターフェイスヘッダーを記述できない場合、サブシステムの設計が台無しになり、サブシステムがより独立したものになるように設計し直す必要があります。(これには、共通サブシステムを3番目のモジュールとして引き出すことが含まれる場合があります。)
RM

8
ヘッダーが独立していることを確認するための優れた手法は、ソースファイルに常に独自のヘッダーを最初に含めるというルールを設定することです。これにより、依存関係のインクルードを実装ファイルからヘッダーファイルに移動する必要がある場合にキャッチします。
doug65536

4
@FrankPuffer:コメントを削除しないでください。特に他の人がコメントに応答する場合は、応答をコンテキストレスにするため、削除しないでください。いつでも新しいコメントでステートメントを修正できます。ありがとう!私はあなたが実際に言ったことを知って興味を持ってんだけど、今はなくなっ:(だ
MPW

18

最も重要な要件は、ソースファイル間の依存関係を減らすことです。C ++では、クラスごとに1つのソースファイルと1つのヘッダーを使用するのが一般的です。したがって、適切なクラス設計がある場合、ヘッダーの地獄に近づくことすらありません。

これを他の方法で見ることもできます:すでにプロジェクトにヘッダーの地獄がある場合は、ソフトウェア設計を改善する必要があることを確信できます。

特定の質問に答えるには:

  • ソースファイルごとに1つのヘッダーですか?→ はい、これはほとんどの場合うまく機能し、物を見つけやすくします。しかし、それを宗教にしないでください。
  • サブシステムごとに1つですか?→ いいえ、なぜこれをしたいのですか?
  • 関数プロトタイプからtypdef、stucts、enumを分離しますか?→ いいえ、機能と関連タイプは一緒に属します。
  • サブシステムの内部をサブシステムの外部のものから分離しますか?→ はい、もちろんです。これにより、依存関係が減少します。
  • スタンドアロンのヘッダーまたはソースのすべてのファイルが準拠していると主張しますか?→ はい、ヘッダーを別のヘッダーの前に含める必要はありません。

12

他の推奨事項に加えて、依存関係を減らすことに沿って(主にC ++に適用可能):

  1. 本当に必要なものだけを、必要な場所に含めます(可能な限り低いレベル)。例 ソースでのみ呼び出しが必要な場合は、ヘッダーに含めないでください。
  2. 可能な限りヘッダーで前方宣言を使用します(ヘッダーには、ポインターまたは他のクラスへの参照のみが含まれます)。
  3. 各リファクタリング後にインクルードをクリーンアップします(コメントアウト、コンパイルの失敗箇所の確認、そこへの移動、まだコメント化されているインクルード行の削除)。
  4. 同じファイルに一般的な機能を詰めすぎないでください。機能別にそれらを分割します(たとえば、Loggerは1つのクラスであるため、1つのヘッダーと1つのソースファイル、SystemHelper ditoなど)。
  5. スタンドアロンの関数ではなく、静的メソッドのみで構成されるクラスであっても、オブジェクト指向の原則に固執するか、代わりに名前空間を使用します
  6. 特定の一般的な機能では、他の無関係なオブジェクトからインスタンスを要求する必要がないため、シングルトンパターンはかなり便利です。

5
#3では、ツールのinclude-what-you-useが役立ち、推測とチェックによる手動の再コンパイルのアプローチを回避します。
RM

1
この文脈でシングルトンの利点は何ですか?本当にわかりません。
フランクパファー

@FrankPuffer私の理論的根拠はこうです:シングルトンがなければ、クラスのインスタンスは通常、ロガーのようなヘルパークラスのインスタンスを所有します。3番目のクラスがそれを使用したい場合、2つのクラスを使用することを意味する所有者からヘルパークラスの参照を要求する必要があります。シングルトンを使用すると、ヘルパークラスのヘッダーを含めるだけで、そこからインスタンスを直接要求できます。この論理に欠陥がありますか?
マーフィー

1
#2(前方宣言)は、コンパイル時間と依存関係のチェックに大きな違いをもたらします。この答え(のようstackoverflow.com/a/9999752/509928)を示し、それはC ++にとCの両方に適用される
デイブ・コンプトン

3
関数がインラインで定義されていない限り、値で渡すときにも前方宣言を使用できます。関数宣言は、完全な型定義が必要なコンテキストではありません(関数定義または関数呼び出しはそのようなコンテキストです)。
StoryTeller-アンスランダーモニカ

6

ソースファイルごとに1つのヘッダー。ソースファイルが実装/エクスポートするものを定義します。

必要な数のヘッダーファイル。各ソースファイルに含まれます(独自のヘッダーで始まります)。

ヘッダーファイルを他のヘッダーファイルに含める(含めることを最小限にする)ことは避けてください(循環依存関係を避けるため)。詳細については、「2つのクラスがC ++を使用して相互に認識できますか?」に対するこの回答を参照してください

このテーマについては、Lakosによる大規模なC ++ソフトウェア設計に関する書籍全体があります。ソフトウェアの「レイヤー」を持つことについて説明します。高レベルのレイヤーは低レベルのレイヤーを使用し、逆もまた同様です。これにより、循環依存を回避できます。


4

ヘッダーヘルルには2種類あるため、あなたの質問は根本的に答えられないと主張します。

  • 何百万もの異なるヘッダーを含める必要があり、それらすべてを地獄で覚えている人さえいますか?そして、それらのヘッダーのリストを維持しますか?あー
  • あなたが一つのことを含め、バベルの塔全体を含めたことがわかるような種類(または、Boost of Boostと言うべきでしょうか...)

問題は、前者を回避しようとすると、ある程度は後者になり、その逆も同様です。

また、3番目の種類の地獄もあります。これは循環依存関係です。注意しないとこれらが表示される可能性があります...それらを回避することは非常に複雑ではありませんが、それを行う方法について考える時間を取る必要があります。CppCon 2016のレベル化に関するJohn Lakosのトーク(またはスライドのみ)を参照してください。


1
循環依存を常に回避できるとは限りません。例は、エンティティが相互に参照するモデルです。少なくとも、サブシステムの循環を制限しようとすることができます。つまり、サブシステムのヘッダーを含めると、循環を抽象化できます。
nalply

2
@nalply:コードではなく、ヘッダーの循環依存関係を回避することを意味しました...循環ヘッダー依存関係を回避しないと、おそらく構築できません。しかし、はい、取られたポイント、+ 1。
アインポクラム-モニカを復活させる

1

分離

最終的には、コンパイラーとリンカーの特性の微妙な違いを欠いた最も基本的な設計レベルで、一日の終わりに私にデカップリングすることです。つまり、各ヘッダーでクラスを1つだけ定義する、ピンプルを使用する、宣言するだけで定義しないで宣言する必要がある型に宣言を転送する、または<iosfwd>ソースファイルごとにヘッダーを1つだけ含む、前方宣言を含むヘッダー(例:、宣言/定義されているもののタイプに基づいて一貫してシステムを編成します。

「コンパイル時の依存関係」を減らす技術

また、いくつかの手法はかなり役立ちますが、これらのプラクティスを使い果たしても、システム内の平均ソースファイルを見つけるには2ページのプリアンブルが必要です。 #includeインターフェイスデザインの論理的な依存関係を減らすことなくヘッダーレベルでコンパイル時の依存関係を減らすことに重点を置きすぎると、厳密に言えば「スパゲッティヘッダー」とはみなされないかもしれませんが、それでも、実際の生産性と同様の有害な問題につながると言うでしょう。結局のところ、もしコンパイルユニットが何かをするために大量の情報を表示する必要がある場合、ビルド時間の増加につながり、開発者を作成している間に戻って物事を変更しなければならない理由を増やすことになります彼らは毎日のコーディングを終わらせようとしているだけで、システムに頭突きしているように感じます。それ'

たとえば、各サブシステムが非常に抽象的なヘッダーファイルとインターフェイスを提供するようにできます。しかし、サブシステムが相互に分離されていない場合、動作するために混乱のように見える依存関係グラフを持つ他のサブシステムインターフェイスに依存するサブシステムインターフェイスで、スパゲッティに似たものが得られます。

外部型への宣言の転送

開発者がビルドサーバーでCIを有効にするのに2日間待機する間に、ビルドに2時間かかった元のコードベースを取得しようと試みたすべてのテクニックのうち、開発者が変更をプッシュしている間、追いついて失敗するため)、私にとって最も疑わしいのは、他のヘッダーで定義された型を前方宣言することでした。そして、後から見た場合に最も疑わしいプラクティスである「ヘッダースパゲッティ」(私がトンネルの相互依存関係を念頭に置いた設計)は、他のヘッダーで定義されたタイプを前方宣言していました。

次のFoo.hppようなヘッダーを想像すると:

#include "Bar.hpp"

またBar、ヘッダーでは、定義ではなく宣言を必要とする方法のみを使用します。class Bar;の定義がBarヘッダーに表示されるのを避けるために宣言するのは簡単なように思えるかもしれません。練習を除き、多くの場合、あなたはどちらかの使用があることコンパイル単位のほとんどを見つけることができますFoo.hppまだ必要終わるBar含まれるようになるの追加負担でとにかく定義するBar.hppの上に自分自身をFoo.hpp、またはあなたはそれが本当に助けと99をして別のシナリオに遭遇コンパイル単位の%は、を含めなくても機能しますBar.hpp。ただし、宣言の確認が必要なBar理由と、なぜFoo ほとんどのユースケースに関係ない場合は、それについて知る必要があります(ほとんど使用されていない別の依存関係をデザインに負担させるのはなぜですか)。

なぜなら概念的にはFooから切り離されていないからBarです。のヘッダーがのヘッダーFooに関する情報を必要としないように作成しました。これは、Barこれら2つを互いに完全に独立させる設計ほど実質的ではありません。

埋め込みスクリプト

これは実際には大規模なコードベース用ですが、システムの少なくとも最も高レベルの部分に組み込みスクリプト言語を使用することは非常に便利です。Luaを1日で埋め込み、システム内のすべてのコマンドを一様に呼び出すことができました(コマンドは抽象的で、ありがたいことに)。残念ながら、開発者が別の言語の導入に不信感を抱き、おそらく最も奇妙なことに、パフォーマンスが最大の疑念であるという障害に遭遇しました。しかし、他の懸念を理解することはできましたが、ユーザーがボタンをクリックしたときにコマンドを呼び出すためにスクリプトを利用するだけであれば、パフォーマンスは問題ではないはずです。ボタンのクリックに対する応答時間のナノ秒の違いを心配しますか?)。

一方、大規模なコードベースでコンパイル時間を短縮する手法を徹底的に試した後、私がこれまでに見た中で最も効果的な方法は、システム内のあるものが動作するために必要な情報量を本当に減らすアーキテクチャであり、コンパイラからヘッダーを切り離すだけではありませんただし、これらのインターフェイスのユーザーは、最低限必要なこと(人間とコンパイラの両方の観点から、コンパイラの依存関係を超える真の分離)を行う必要があります。

ECSは一例に過ぎません(ただし、使用することはお勧めしません)が、遭遇したことにより、ECSによってテンプレートやその他の多くの便利な機能をうまく活用しながら、驚くほど迅速に構築できる非常に壮大なコードベースを作成できることがわかりました自然、非常に分離されたアーキテクチャを作成します。システムはECSデータベースについてのみ知る必要があり、通常は少数のコンポーネントタイプ(場合によっては1つ)だけで処理を行います。

ここに画像の説明を入力してください

デザイン、デザイン、デザイン

そして、人間の概念レベルでのこれらの種類の分離されたアーキテクチャ設計は、コードベースが成長し、成長し、成長するにつれて、私が上で検討したどの技術よりもコンパイル時間を最小化するという点でより効果的です。コンパイル単位とリンク時間で必要な情報量を乗算するコンパイル単位(平均的な開発者が何かを行うために大量の物を含める必要があるシステムでは、コンパイラーだけでなく、何でもするために大量の情報を知る必要があります)。また、ビルド時間の短縮とヘッダーのもつれの解消よりも多くの利点があります。これは、開発者が何かを行うためにすぐに必要なものを超えてシステムについて多くを知る必要がないことを意味するためです。

たとえば、数百万のLOCにまたがるAAAゲーム用の物理エンジンを開発するために専門の物理開発者を雇うことができ、利用可能なタイプやインターフェースなどの最低限の絶対情報を知っている間、彼は非常に迅速に始めることができますシステム概念だけでなく、物理エンジンを構築するために必要な情報量が減少することは当然のことであり、同様にスパゲッティに類似するものがないことを示唆しながら、ビルド時間の大幅な短縮につながります。システム内のどこでも。そして、それがこれらの他のすべてのテクニックより優先することを私が提案していることです:システムをどのように設計するか。他のテクニックを使い果たすと、それを行うと一番上に着氷します。


1
素晴らしい答えです!Althoguh にきびが何であるかを発見するために少し掘り下げなければならなかった:
Mawg

0

それは意見の問題です。この答えとその答えをご覧ください。また、プロジェクトのサイズにも大きく依存します(プロジェクトに数百万のソース行があると思われる場合、数十万のソース行があると同じではありません)。

他の回答とは対照的に、サブシステムごとに1つの(かなり大きな)パブリックヘッダー(「プライベート」ヘッダーを含む可能性があり、多くのインライン関数の実装用に個別のファイルがある場合があります)をお勧めします。ヘッダーに複数の#include ディレクティブしかない場合もあります。

多くのヘッダーファイルが推奨されるとは思わない。特に、クラスごとに1つのヘッダーファイル、またはそれぞれ数十行の多数の小さなヘッダーファイルはお勧めしません。

(大量の小さなファイルがある場合、すべての小さな翻訳単位にそれらの多くを含める必要があり、全体のビルド時間が低下する可能性があります)

本当に欲しいのは、サブシステムとファイルごとに、それを担当するメイン開発者を特定することです。

最後に、小規模なプロジェクト(たとえば、10万行未満のソースコード)では、それほど重要ではありません。プロジェクト中に、コードをリファクタリングし、別のファイルに再編成することが非常に簡単にできます。コードのチャンクを新しい(ヘッダー)ファイルにコピー&ペーストするだけで、大したことではありません(より難しいのは、ファイルを再編成する方法を賢く設計することであり、それはプロジェクト固有です)。

(私の個人的な好みは、大きすぎるファイルと小さすぎるファイルを避けることです。私はしばしばそれぞれ数千行のソースファイルを持っています。私は数百行または数行のヘッダーファイル(インライン化された関数定義を含む)を恐れていません数千人)

GCCでプリコンパイル済みヘッダーを使用する場合(コンパイル時間を短縮するための賢明なアプローチである場合があります)、単一のヘッダーファイル(他のすべてのヘッダーファイルとシステムヘッダーも含む)が必要です。

C ++では、標準ヘッダーファイルが多くのコードをプルしていること注意してください。たとえば#include <vector>、Linux上のGCC 6で1万行(18100行)を引いています。そして#include <map> 、ほぼ40KLOCに拡張します。したがって、標準ヘッダーを含む多くの小さなヘッダーファイルがある場合、ビルド中に数千行を再解析することになり、コンパイル時間が低下します。これが、多くの小さなC ++ソース行(せいぜい数百行)が嫌いなのですが、C ++ファイルが少なくても大きい(数千行)ことを好む理由です。

(したがって、常に間接的にもいくつかの標準ヘッダーファイルを含む数百の小さな C ++ファイルがあると、ビルド時間が非常に長くなり、開発者を困らせます)

Cコードでは、多くの場合、ヘッダーファイルはより小さなものに展開されるため、トレードオフは異なります。

インスピレーションを得るために、既存の フリーソフトウェアプロジェクト(たとえばgithub)での以前の実践も見てください。

依存関係は適切なビルド自動化システムで処理できることに注意してください。GNU makeドキュメントを調べてください。GCCへのさまざまな-Mプリプロセッサフ​​ラグに注意してください(自動的に依存関係を生成するのに役立ちます)。

言い換えれば、あなたのプロジェクト(100未満のファイルと数十の開発者)はおそらく「ヘッダー地獄」に本当に関心を持つほどには大きくないので、あなたの懸念は正当化されません。ヘッダーファイルは数十(またはそれ以下)しか持てず、翻訳単位ごとに1つのヘッダーファイルを選択することも、1つのヘッダーファイルを選択することもできます。 「header hell」(ファイルのリファクタリングと再編成はかなり簡単なままなので、最初の選択はあまり重要ではありません)。

(「ヘッダー地獄」-あなたにとっては問題ではない-に焦点を当てるのではなく、優れたアーキテクチャを設計するために焦点を合わせてください)


あなたが言及する技術は正しいかもしれません。しかし、私が理解したように、OPはコンパイル時間ではなく、コードの保守性と組織を改善するためのヒントを求めていました。そして、これら2つの目標の間には直接的な対立が見られます。
マーフィー

しかし、それはまだ意見の問題です。そして、OPは明らかにそれほど大きくないプロジェクトを開始しているようです。
バジルスタリンケビッチ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.