Java、Vala、またはC#でクラスを設計する場合、定義と宣言を同じソースファイルに配置します。しかし、C++では、定義と宣言を2つ以上のファイルに分離することが伝統的に好まれています。
ヘッダーファイルを使用して、Javaのようにすべてをそこに入れるとどうなりますか?パフォーマンスの低下はありますか?
答えは、作成するクラスの種類によって異なります。
C++のコンパイルモデルはCの時代にさかのぼるため、あるソースファイルから別のソースファイルにデータをインポートする方法は比較的原始的です。 #include
ディレクティブは、インクルードするファイルの内容をソースファイルに文字どおりコピーし、その結果を、作成したファイルであるかのように扱います。 1つの定義ルール(ODR)と呼ばれるC++ポリシーにより、当然のことながら、すべての関数とクラスに最大で1つの定義。つまり、クラスをどこかで宣言した場合、そのクラスのすべてのメンバー関数をまったく定義しないか、1つのファイルで1回だけ定義する必要があります。いくつかの例外があります(すぐに説明します)が、今のところ、このルールは、ハードで高速な、例外のないルールであるかのように扱います。
非テンプレートクラスを受け取り、クラス定義と実装の両方をヘッダーファイルに配置すると、1つの定義ルールで問題が発生する可能性があります。特に、コンパイルする2つの異なる.cppファイルがあり、どちらも#include
ヘッダーに実装とインターフェイスの両方が含まれていると仮定します。この場合、これらの2つのファイルを一緒にリンクしようとすると、リンカーはそれぞれにクラスのメンバー関数の実装コードのコピーが含まれていることを検出します。この時点で、1つの定義規則に違反しているため、リンカーはエラーを報告します。すべてのクラスのメンバー関数の2つの異なる実装があります。
これを防ぐために、C++プログラマーは通常、クラスをヘッダーファイルに分割します。ヘッダーファイルには、そのメンバー関数の宣言とともに、クラス宣言が含まれ、それらの関数は実装されていません。実装は、個別にコンパイルおよびリンクできる別の.cppファイルに入れられます。これにより、コードでODRの問題が発生するのを回避できます。方法は次のとおりです。まず、クラスヘッダーファイルを複数の異なる.cppファイルに#include
すると、それぞれのファイルが宣言のコピーを取得するだけです。 definitionsではなくメンバー関数なので、クラスのクライアントはいずれも定義を取得しません。これは、リンク時に問題が発生することなく、任意の数のクライアントがヘッダーファイルを#include
できることを意味します。実装を含む独自の.cppファイルは、メンバー関数の実装を含む唯一のファイルであるため、リンク時に、面倒なく他のクライアントオブジェクトファイルをいくつでもマージできます。これが、.hファイルと.cppファイルを分割する主な理由です。
もちろん、ODRにはいくつかの例外があります。これらの最初のものは、テンプレート関数とクラスを思い付きます。 ODRには、同じテンプレートクラスまたは関数に対して複数の異なる定義を使用できることを明示的に述べていますが、それらはすべて同等のものです。これは、主にテンプレートのコンパイルを容易にするためです。各C++ファイルは、他のファイルと衝突することなく、同じテンプレートをインスタンス化できます。このため、およびその他のいくつかの技術的な理由により、クラステンプレートには、対応する.cppファイルのない.hファイルのみが含まれる傾向があります。クライアントはいくつでも問題なくファイルを#include
できます。
ODRの他の主要な例外には、インライン関数が含まれます。仕様には、ODRはインライン関数には適用されないことが明確に記載されているため、インラインとしてマークされているクラスメンバー関数の実装を含むヘッダーファイルがあれば、それで問題ありません。 ODRを壊すことなく、任意の数のファイルがこのファイルを#include
できます。興味深いことに、クラスの本体で宣言および定義されているメンバー関数はすべて暗黙的にインライン化されるため、次のようなヘッダーがある場合:
#ifndef Include_Guard
#define Include_Guard
class MyClass {
public:
void DoSomething() {
/* ... code goes here ... */
}
};
#endif
その後、ODRを壊す危険はありません。これを次のように書き換えると
#ifndef Include_Guard
#define Include_Guard
class MyClass {
public:
void DoSomething();
};
void MyClass::DoSomething() {
/* ... code goes here ... */
}
#endif
次に、あなたはwouldメンバー関数がインラインでマークされておらず、複数のクライアント#include
このファイルがある場合、MyClass::DoSomething
の複数の定義が存在するため、ODRを壊します。
要約すると、ODRの破損を防ぐために、クラスを.h/.cppペアに分割する必要があります。ただし、クラステンプレートを記述している場合は、.cppファイルは必要ありません(おそらく1つもありません)。また、クラスのすべてのメンバー関数をインラインでマークしてもかまいません。 .cppファイルは避けてください。
ヘッダーファイルに定義を置くことの欠点は次のとおりです。
ヘッダーファイルA-metahodA()の定義が含まれています
ヘッダーファイルB-ヘッダーファイルAが含まれます。
ここで、methodAの定義を変更するとします。ヘッダーファイルAがBに含まれているため、BだけでなくファイルAもコンパイルする必要があります。
最大の違いは、すべての関数がインライン関数として宣言されていることです。一般に、コンパイラーは十分に賢く、これは問題になりませんが、最悪のシナリオでは、定期的にページ違反が発生し、コードが非常に遅くなります。通常、コードはパフォーマンス上の理由ではなく、設計上の理由で分離されています。
一般に、実装をヘッダーから分離することをお勧めします。ただし、テンプレートなど、実装がヘッダー自体に含まれる場合には例外があります。
すべてをヘッダーに入れることに関する2つの特定の問題:
コンパイル時間は、場合によっては大幅に増加します。 C++のコンパイル時間は十分に長いので、望んだことではありません。
実装に循環依存関係がある場合、すべてをヘッダーに保持することは困難または不可能です。例えば:
header1.h
struct C1
{
void f();
void g();
};
header2.h
struct C2
{
void f();
void g();
};
impl1.cpp
#include "header1.h"
#include "header2.h"
void C1::f()
{
C2 c2;
c2.f();
}
impl2.cpp
#include "header2.h"
#include "header1.h"
void C2::g()
{
C1 c1;
c1.g();
}