web-dev-qa-db-ja.com

すべてのコードをC ++のヘッダーファイルに入れることの長所と短所は?

(ほぼ)すべてのコードがヘッダーファイルに存在するようにC++プログラムを構成できます。基本的にはC#またはJavaプログラムのように見えます。ただし、コンパイル時にすべてのヘッダーファイルをプルするには、少なくとも1つの.cppファイルが必要です。今ではこのアイデアを絶対に嫌う人もいます。しかし、これを行うことの説得力のある欠点は見つかりませんでした。いくつかの利点を挙げます。

[1]コンパイル時間が短縮されます。 .cppファイルは1つしかないため、すべてのヘッダーファイルは1回だけ解析されます。また、1つのヘッダーファイルを複数回インクルードすることはできません。インクルードしないと、ビルドが中断されます。別のアプローチを使用する場合、より高速なコンパイルを実現する方法は他にもありますが、これは非常に簡単です。

[2]循環依存を完全に明確にすることで、それらを回避します。 ClassA.hClassAClassB.hClassBに循環依存している場合、前方参照を配置する必要があり、それが目立ちます。 (これは、コンパイラが循環依存関係を自動的に解決するC#&Javaとは異なります。これにより、IMOのコーディング慣行が不適切になります)。ここでも、コードが.cppファイルにある場合は、循環依存関係を回避できます。実際のプロジェクトでは、.cppファイルには、誰が誰に依存しているかがわからなくなるまで、ランダムなヘッダーが含まれる傾向があります。

あなたの考え?

45
user15071

理由[1]コンパイル時間が速い

私のプロジェクトにはありません。ソースファイル(CPP)には、必要なヘッダー(HPP)のみが含まれています。したがって、小さな変更のために1つのCPPのみを再コンパイルする必要がある場合、再コンパイルされないファイルの数は10倍になります。

おそらく、プロジェクトをより論理的なソース/ヘッダーに分割する必要があります。クラスAの実装を変更しても、クラスB、C、D、Eなどの実装を再コンパイルする必要はありません。

理由[2]循環依存を回避します

コードの循環依存?

申し訳ありませんが、この種の問題が実際の問題になることはまだありません。たとえば、AがBに依存し、BがAに依存しているとします。

struct A
{
   B * b ;
   void doSomethingWithB() ;
} ;

struct B
{
   A * a ;
   void doSomethingWithA() ;
} ;

void A::doSomethingWithB() { /* etc. */ }
void B::doSomethingWithA() { /* etc. */ }

この問題を解決する良い方法は、このソースをクラスごとに少なくとも1つのソース/ヘッダーに分割することです(Javaの方法と同様ですが、1つのソースと1つのヘッダーがあります)クラス):

// A.hpp

struct B ;

struct A
{
   B * b ;
   void doSomethingWithB() ;
} ;

// B.hpp

struct A ;

struct B
{
   A * a ;
   void doSomethingWithA() ;
} ;

// A.cpp
#include "A.hpp"
#include "B.hpp"

void A::doSomethingWithB() { /* etc. */ }

// B.cpp
#include "B.hpp"
#include "A.hpp"

void B::doSomethingWithA() { /* etc. */ }

したがって、依存関係の問題はなく、コンパイル時間も高速です。

私は何か見落としてますか?

「実世界」のプロジェクトに取り組むとき

実際のプロジェクトでは、誰が誰に依存しているかがわからなくなるまで、cppファイルにはランダムなヘッダーが含まれる傾向があります。

もちろん。ただし、これらのファイルを再編成して「1つのCPP」ソリューションを構築する時間があれば、それらのヘッダーをクリーンアップする時間があります。ヘッダーの私のルールは次のとおりです。

  • ヘッダーを分解して、可能な限りモジュール化する
  • 不要なヘッダーは絶対に含めないでください
  • シンボルが必要な場合は、前方宣言します
  • 上記が失敗した場合にのみ、ヘッダーを含めます

とにかく、すべてのヘッダーは自給自足である必要があります。つまり、次のことを意味します。

  • ヘッダーには、必要なすべてのヘッダーが含まれます(必要なヘッダーのみ-上記を参照)
  • 1つのヘッダーを含む空のCPPファイルは、他に何も含める必要なしにコンパイルする必要があります

これにより、順序の問題と循環依存が解消されます。

コンパイル時間は問題ですか?次に...

コンパイル時間が本当に問題になる場合は、次のいずれかを検討します。

  • プリコンパイル済みヘッダーの使用(これはSTLとBOOSTに非常に役立ちます)
  • http://en.wikipedia.org/wiki/Opaque_pointer で説明されているように、PImplイディオムを介した結合を減らします。
  • ネットワーク共有コンパイルを使用する

結論

あなたがしていることは、すべてをヘッダーに入れることではありません。

基本的に、すべてのファイルを1つの最終ソースに含めます。

おそらく、あなたは完全なプロジェクトのコンパイルの観点から勝っています。

しかし、1つの小さな変更をコンパイルすると、常に負けてしまいます。

コーディングするとき、私はしばしば小さな変更をコンパイルし(コンパイラにコードを検証させるためだけの場合)、最後にもう一度、プロジェクト全体を変更することを知っています。

私のプロジェクトがあなたのやり方で組織されていたら、私は多くの時間を失うでしょう。

33
paercebal

私はポイント1に同意しません。

はい、.cppは1つだけで、最初から作成する方が高速です。ただし、最初から作成することはめったにありません。小さな変更を加えると、毎回プロジェクト全体を再コンパイルする必要があります。

私はそれを逆にすることを好みます:

  • 共有宣言を.hファイルに保持する
  • .cppファイルの1か所でのみ使用されるクラスの定義を保持する

したがって、私の.cppファイルのいくつかはJavaまたはC#コード;)のように見え始めます

しかし、'ものを.hに保持する'アプローチは、システムの設計時に適しています。これは、ポイント2を作成したためです。私は通常、クラス階層を構築しているときにそれを行い、後でコードアーキテクチャが安定したときに、コードを.cppファイルに移動します。

25
Milan Babuškov

あなたはあなたの解決策がうまくいくと言うのは正しいです。現在のプロジェクトや開発中の環境に短所がない場合もあります。

だが...

他の人が述べたように、すべてのコードをヘッダーファイルに入れると、コードの1行を変更するたびに完全なコンパイルが強制されます。これはまだ問題ではないかもしれませんが、プロジェクトが十分に大きくなり、コンパイル時間が問題になる可能性があります。

もう1つの問題は、コードを共有するときです。まだ直接心配していないかもしれませんが、コードの潜在的なユーザーからできるだけ多くのコードを隠しておくことが重要です。コードをヘッダーファイルに入れることで、コードを使用するプログラマーはコード全体を見る必要がありますが、その使用方法に関心があるだけです。コードをcppファイルに入れると、バイナリコンポーネント(静的または動的ライブラリ)とそのインターフェイスのみをヘッダーファイルとして配信できます。これは、環境によってはより単純な場合があります。

これは、現在のコードをダイナミックライブラリに変換できるようにする場合に問題になります。実際のコードから切り離された適切なインターフェイス宣言がないため、コンパイルされたダイナミックライブラリとその使用法インターフェイスを読み取り可能なヘッダーファイルとして提供することはできません。

あなたはまだこれらの問題を抱えていないかもしれません、それがあなたの解決策があなたの現在の環境で大丈夫かもしれないと私が言っていた理由です。ただし、変更に備えておく方が常に適切であり、これらの問題のいくつかに対処する必要があります。

PS:C#またはJavaについては、これらの言語があなたの言うことをしていないことに注意する必要があります。それらは実際には(cppファイルのように)ファイルを独立してコンパイルしており、各ファイルのインターフェースをグローバルに保存します。これらのインターフェイス(およびその他のリンクされたインターフェイス)は、プロジェクト全体をリンクするために使用されます。そのため、循環参照を処理できます。 C++はファイルごとに1つのコンパイルパスしか実行しないため、インターフェイスをグローバルに格納することはできません。そのため、ヘッダーファイルに明示的に書き込む必要があります。

16
Vincent Robert

あなたはその言語がどのように使われることを意図していたかを誤解しています。 .cppファイルは、実際には(インラインコードとテンプレートコードを除いて)、システムにある実行可能コードの唯一のモジュールです。 .cppファイルはオブジェクトファイルにコンパイルされ、オブジェクトファイルがリンクされます。 .hファイルは、.cppファイルに実装されているコードの前方宣言のためにのみ存在します。

これにより、コンパイル時間が短縮され、実行可能ファイルが小さくなります。また、.h宣言を確認することでクラスの概要をすばやく確認できるため、かなりきれいに見えます。

インラインコードとテンプレートコードについては、どちらもリンカーではなくコンパイラーによってコードを生成するために使用されるため、.cppファイルごとにコンパイラーが常に使用できる必要があります。したがって、唯一の解決策は、それを.hファイルに含めることです。

ただし、クラス宣言を.hファイルに、すべてのテンプレートとインラインコードを.inlファイルに、すべての非テンプレート/インラインコードの実装を.cppファイルに含めるソリューションを開発しました。 .inlファイルは私の.hファイルの下部に#includeされています。これにより、物事がクリーンで一貫性のある状態に保たれます。

11
user19302

私にとって明らかな欠点は、常にすべてのコードを一度にビルドする必要があることです。 .cppファイルの場合、個別にコンパイルできるため、実際に変更されたビットのみを再構築します。

9

あなたはチェックアウトしたいかもしれません Lazy C++ 。これにより、すべてを1つのファイルに配置し、コンパイル前に実行して、コードを.hファイルと.cppファイルに分割できます。これはあなたに両方の長所を提供するかもしれません。

コンパイル時間が遅いのは、通常、C++で記述されたシステム内の過度の結合が原因です。たぶん、コードを外部インターフェースを備えたサブシステムに分割する必要があります。これらのモジュールは、別々のプロジェクトでコンパイルできます。このようにして、システムの異なるモジュール間の依存関係を最小限に抑えることができます。

3
Chris de Vries

このアプローチの欠点の1つは、並列コンパイルを実行できないことです。今はコンパイルが速くなっていると思うかもしれませんが、複数の.cppファイルがある場合は、自分のマシンの複数のコアで、またはdistccやIncredibuildなどの分散ビルドシステムを使用して、それらを並行してビルドできます。

3
Don Neufeld

あなたが諦めていることの1つは、名前空間なしでは生きていけないということです。

これらは、クラスの実装ファイルの外部では見えないはずのクラス固有のユーティリティ関数を定義するのに非常に価値があることがわかりました。また、シングルトンインスタンスのように、システムの他の部分からは見えないはずのグローバルデータを保持するのにも最適です。

3
user21714

言語の設計範囲外になります。あなたにはいくつかの利点があるかもしれませんが、それは最終的にあなたを尻に噛むでしょう。

C++は、宣言のあるhファイル、および実装のあるcppファイル用に設計されています。コンパイラはこの設計に基づいて構築されています。

はい、それが良いアーキテクチャかどうかについて人々は議論しますが、それはデザインです。 C++ファイルアーキテクチャを設計する新しい方法を再発明するよりも、問題に時間を費やす方がよいでしょう。

3
Paul Nathan

インターフェースと実装の観点から、.hファイルと.cppファイルの分離について考えるのが好きです。 .hファイルにはもう1つのクラスへのインターフェース記述が含まれ、.cppファイルには実装が含まれます。完全にきれいな分離を妨げる実際的な問題や明確さがある場合もありますが、それが私が始めるところです。たとえば、小さなアクセサ関数は、わかりやすくするために、通常、クラス宣言にインラインでコーディングします。より大きな関数は.cppファイルにコード化されています

いずれにせよ、コンパイル時間によってプログラムの構造を決定されないようにしてください。 2分ではなく1.5分でコンパイルされるプログラムよりも読み取り可能で保守可能なプログラムを用意することをお勧めします。

2
mxg

MSVCのプリコンパイル済みヘッダーを使用していて、Makefileまたはその他の依存関係ベースのビルドシステムを使用している場合を除いて、繰り返しビルドする場合は、個別のソースファイルを使用するとコンパイルが速くなるはずです。私の開発はほとんど常に反復的であるため、変更しなかった他の20のソースファイルよりも、ファイルx.cppで行った変更をどれだけ速く再コンパイルできるかを重視しています。さらに、APIよりもソースファイルの変更頻度がはるかに高いため、変更頻度は低くなります。

循環依存について。私はpaercebalのアドバイスをさらに一歩進めます。彼には、互いにポインタを持つ2つのクラスがありました。代わりに、あるクラスが別のクラスを必要とする場合に、より頻繁に遭遇します。この場合、依存関係のヘッダーファイルを他のクラスのヘッダーファイルにインクルードします。例:

// foo.hpp
#ifndef __FOO_HPP__
#define __FOO_HPP__

struct foo
{
   int data ;
} ;

#endif // __FOO_HPP__

// bar.hpp
#ifndef __BAR_HPP__
#define __BAR_HPP__

#include "foo.hpp"

struct bar
{
   foo f ;
   void doSomethingWithFoo() ;
} ;
#endif // __BAR_HPP__

// bar.cpp
#include "bar.hpp"

void bar::doSomethingWithFoo()
{
  // Initialize f
  f.data = 0;
  // etc.
}

循環依存関係とは少し関係のないこれをインクルードする理由は、ヘッダーファイルをウィリーニリーにインクルードする代わりの方法があると感じているからです。この例では、構造体バーのソースファイルに構造体fooヘッダーファイルは含まれていません。これはヘッダーファイルで行われます。これには、barを使用する開発者が、そのヘッダーファイルを使用するために開発者がインクルードする必要のある他のファイルについて知る必要がないという利点があります。

2
terson

ヘッダー内のコードの問題の1つは、インライン化する必要があることです。インライン化しないと、同じヘッダーを含む複数の変換ユニットをリンクするときに複数定義の問題が発生します。

元の質問では、プロジェクトにはcppが1つしかないと指定されていましたが、再利用可能なライブラリを対象としたコンポーネントを作成している場合はそうではありません。

したがって、可能な限り最も再利用可能で保守可能なコードを作成するために、ヘッダーファイルにはインライン化されたコードとインライン化できないコードのみを配置してください。

2
pk.

多くの人がこのアイデアには多くの短所があると指摘していますが、少しバランスを取り、プロを提供するには、ライブラリコードをヘッダーに完全に含めることは、他のコードから独立しているため、理にかなっていると思いますそれが使用されるプロジェクトの設定。

たとえば、さまざまなオープンソースライブラリを利用しようとしている場合、プログラムへのリンクにさまざまなアプローチを使用するように設定できます。オペレーティングシステムの動的に読み込まれるライブラリコードを使用するものもあれば、静的にリンクされるように設定されるものもあります。マルチスレッドを使用するように設定されているものもあれば、そうでないものもあります。そして、これらの互換性のないアプローチを整理しようとすることは、プログラマーにとって、特に時間の制約がある場合、非常に圧倒的な作業になる可能性があります。

ただし、完全にヘッダーに含まれているライブラリを使用する場合は、これらすべてが問題になることはありません。合理的なよく書かれたライブラリのために「それはうまくいく」。

1

テンプレートクラスを使用している場合は、とにかく実装全体をヘッダーに配置する必要があります...

プロジェクト全体を一度に(単一のベース.cppファイルを介して)コンパイルすると、「プログラム全体の最適化」や「モジュール間の最適化」などが可能になります。これは、いくつかの高度なコンパイラでのみ使用できます。これは、すべての.cppファイルをオブジェクトファイルにプリコンパイルしてからリンクする場合、標準のコンパイラでは実際には不可能です。

0
Doug

static-or-global-variableは、透明性がさらに低く、おそらくデバッグ不可能です。

たとえば、分析のために反復の総数を数えます。

私のクラッジファイルでは、そのようなアイテムをcppファイルの先頭に置くと、簡単に見つけることができます。

「おそらくデバッグ不可能」とは、日常的にそのようなグローバルをWATCHウィンドウに配置することを意味します。常にスコープ内にあるため、プログラムカウンタが現在どこにあるかに関係なく、WATCHウィンドウは常にそれに到達できます。このような変数をヘッダーファイルの先頭の{}の外側に配置することで、すべてのダウンストリームコードにそれらを「認識」させることができます。それらを{}の内側に配置することにより、プログラムカウンターが{}の外側にある場合、デバッガーはそれらを「スコープ内」とは見なさなくなると思います。一方、kludge-global-at-Cpp-topを使用すると、link-map-pdb-etcに表示される程度にグローバルである場合でも、extern-statementがないと、他のCppファイルはそれに到達できません。 、偶発的な結合を回避します。

0
pngaz

誰も提起していないことの1つは、大きなファイルをコンパイルするにはlotのメモリが必要だということです。プロジェクト全体を一度にコンパイルするには、非常に大きなメモリスペースが必要になるため、すべてのコードをヘッダーに配置できたとしても、それは実現不可能です。

0
Greg Rogers

オブジェクト指向プログラミングの重要な哲学は、データを隠して、実装をユーザーから隠してカプセル化されたクラスに導くことにあります。これは主に、クラスのユーザーが主に、静的型だけでなくインスタンス固有の公的にアクセス可能なメンバー関数を使用する抽象化レイヤーを提供するためです。その後、クラスの開発者は、実装がユーザーに公開されていない限り、実際の実装を自由に変更できます。実装がプライベートでヘッダーファイルで宣言されている場合でも、実装を変更するには、依存するすべてのコードベースを再コンパイルする必要があります。一方、実装(メンバー関数の定義)がソースコード(非ヘッダーファイル)にある場合、ライブラリは変更され、依存するコードベースはライブラリの改訂バージョンと再リンクする必要があります。そのライブラリが共有ライブラリのように動的にリンクされている場合、関数のシグネチャ(インターフェイス)を同じに保ち、実装を変更しても、再リンクする必要はありません。利点?もちろん。

0