50

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

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

[2] 循環依存関係を完全に明確にすることで、循環依存関係を回避します。ClassAinがinClassA.hに循環依存している場合、前方参照を配置する必要があります。(これは、コンパイラが循環依存関係を自動的に解決する C# および Java とは異なることに注意してください。これは、IMO の悪いコーディング プラクティスを助長します)。繰り返しになりますが、コードがファイル内にある場合は循環依存を回避できますが、実際のプロジェクトでは、誰が誰に依存しているかがわからなくなるまで、ファイルにランダムなヘッダーが含まれる傾向があります。ClassBClassB.h.cpp.cpp

あなたの考え?

4

17 に答える 17

36

理由①コンパイル時間の短縮

私のプロジェクトにはありません: ソース ファイル (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 つの小さな変更のためにコンパイルすると、常に負けてしまいます。

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

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

于 2008-10-11T15:14:55.967 に答える
26

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

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

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

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

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

しかし、システムを設計する際には、 「.h に保存する」アプローチが有効であり、ポイント 2. が作成されました。私は通常、クラス階層を構築しているときにこれを行い、後でコード アーキテクチャが安定したときに、コードを .cpp ファイルに移動します。

于 2008-10-11T08:50:57.303 に答える
16

あなたのソリューションが機能すると言うのは正しいです。現在のプロジェクトと開発環境にとっては、短所さえないかもしれません。

しかし...

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

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

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

これらの問題はまだ発生していない可能性があります。そのため、現在の環境ではソリューションが問題ない可能性があるとお伝えしました。しかし、どんな変化にも備えておく方が常に良いことであり、これらの問題のいくつかに対処する必要があります。

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

于 2008-10-11T09:38:30.280 に答える
12

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

これにより、コンパイル時間が短縮され、実行可能ファイルが小さくなります。また、クラスの .h 宣言を見ることでクラスの概要を簡単に把握できるため、見た目もかなりすっきりしています。

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

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

于 2008-10-11T09:14:35.820 に答える
11

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

于 2008-10-11T08:47:32.490 に答える
4

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

于 2008-10-12T16:25:27.963 に答える
3

あなたは言語の設計範囲の外に出ています。いくつかの利点があるかもしれませんが、最終的にはお尻を噛むことになります.

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

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

于 2008-10-11T16:23:28.747 に答える
3

あなたがあきらめていることの 1 つは、匿名の名前空間です。

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

于 2008-10-11T15:55:33.577 に答える
3

Lazy C++をチェックしてみてください。すべてを 1 つのファイルに配置し、コンパイルの前に実行して、コードを .h ファイルと .cpp ファイルに分割することができます。これにより、両方の長所が得られる可能性があります。

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

于 2008-10-11T12:56:35.263 に答える
2

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を使用する開発者が、そのヘッダーファイルを使用するために開発者がインクルードする必要のある他のファイルについて知る必要がないという利点があります。

于 2008-10-11T17:58:46.433 に答える
2

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

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

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

于 2008-10-12T02:31:58.317 に答える
2

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

いずれにせよ、コンパイル時間によってプログラムの構造が決まることはありません。2 分ではなく 1.5 分でコンパイルできるプログラムよりも、読みやすく保守しやすいプログラムを作成する方がよいでしょう。

于 2008-10-11T14:06:20.380 に答える
1

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

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

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

于 2008-10-11T16:49:18.913 に答える
0

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

于 2014-04-16T08:29:27.217 に答える
0

誰も指摘していないことの 1 つは、大きなファイルをコンパイルするには多くのメモリが必要だということです。プロジェクト全体を一度にコンパイルすると、すべてのコードをヘッダーに入れることができたとしても実行不可能な巨大なメモリ空間が必要になります。

于 2008-10-30T01:16:24.490 に答える
0

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

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

私のkludgedファイルでは、そのようなアイテムをcppファイルの先頭に置くと、見つけやすくなります。

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

于 2008-10-11T09:07:12.497 に答える
0

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

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

于 2008-11-19T05:23:17.173 に答える