C ++では、関数、変数、定数の宣言と定義は次のように分離できます。
function someFunc();
function someFunc()
{
//Implementation.
}
実際、クラスの定義では、これがよくあることです。クラスは通常、そのメンバーを使用して.hファイルで宣言され、対応する.Cファイルで定義されます。
このアプローチの長所と短所は何ですか?
C ++では、関数、変数、定数の宣言と定義は次のように分離できます。
function someFunc();
function someFunc()
{
//Implementation.
}
実際、クラスの定義では、これがよくあることです。クラスは通常、そのメンバーを使用して.hファイルで宣言され、対応する.Cファイルで定義されます。
このアプローチの長所と短所は何ですか?
歴史的に、これはコンパイラを支援するためのものでした。名前を使用する前に、名前のリストを提供する必要がありました。これが実際の使用法であろうと、前方宣言であろうと (C のデフォルトの関数プロトタイプは別として)。
現代の言語用の最新のコンパイラは、これがもはや必要ではないことを示しているため、ここでの C および C++ (および Objective-C、およびおそらくその他) の構文は歴史的な手荷物です。実際、これは C++ の大きな問題の 1 つであり、適切なモジュール システムを追加しても解決されません。
短所は次のとおりです: 多くのネストされたインクルード ファイル (以前にインクルード ツリーをトレースしたことがありますが、それらは驚くほど巨大です) と、宣言と定義の間の冗長性 - すべてがコーディング時間とコンパイル時間の延長につながります (同等の C++ と C++ のコンパイル時間を比較したことがあります)。 C# プロジェクト? これが違いの理由の 1 つです)。ヘッダー ファイルは、提供するすべてのコンポーネントのユーザーに提供する必要があります。ODR 違反の可能性。プリプロセッサへの依存 (多くの最新の言語はプリプロセッサ ステップを必要としません)。これにより、コードが脆弱になり、ツールが解析しにくくなります。
メリット:あまりない。ドキュメントの目的で 1 つの場所にグループ化された関数名のリストを取得すると主張することもできますが、最近のほとんどの IDE には何らかのコード折りたたみ機能があり、どのようなサイズのプロジェクトでもドキュメント ジェネレーター (doxygen など) を使用する必要があります。よりクリーンでプリプロセッサのないモジュールベースの構文を使用すると、ツールがコードをたどってこれ以上のものを提供することが容易になるため、この「利点」はほとんど意味がないと思います。
これは、C/C++ コンパイラがどのように機能するかの人工物です。
ソース ファイルがコンパイルされると、プリプロセッサは各 #include ステートメントをインクルード ファイルの内容に置き換えます。その後、コンパイラはこの連結の結果を解釈しようとします。
次に、コンパイラはその結果を最初から最後まで調べて、各ステートメントを検証しようとします。コード行が以前に定義されていない関数を呼び出す場合、関数はあきらめます。
ただし、相互に再帰的な関数呼び出しに関しては、次のような問題があります。
void foo()
{
bar();
}
void bar()
{
foo();
}
ここでは、不明なfoo
ためコンパイルされませんbar
。2 つの関数を入れ替えると、未知のbar
ようにコンパイルされませんfoo
。
ただし、宣言と定義を分離する場合は、必要に応じて関数を並べ替えることができます。
void foo();
void bar();
void foo()
{
bar();
}
void bar()
{
foo();
}
ここで、コンパイラが処理foo
するとき、コンパイラは という関数のシグネチャをすでに知っており、bar
満足しています。
もちろん、コンパイラは別の方法で動作する可能性がありますが、それは C、C++、およびある程度の Objective-C で動作する方法です。
短所:
直接なし。とにかく C/C++ を使用している場合は、それが最善の方法です。言語/コンパイラを選択できる場合は、これが問題にならないものを選択できます。宣言をヘッダー ファイルに分割する際に考慮すべき唯一のことは、相互に再帰的な #include ステートメントを避けることですが、それがインクルード ガードの目的です。
利点:
もちろん、関数を公開することにまったく関心がない場合でも、通常はヘッダーではなく実装ファイルで完全に定義することを選択できます。
標準では、関数を使用する場合、宣言がスコープ内にある必要があります。これは、コンパイラーがプロトタイプ (ヘッダー・ファイル内の宣言) に対して何を渡すかを検証できる必要があることを意味します。もちろん、可変個の関数の場合を除いて、そのような関数は引数を検証しません。
これが必要とされなかった C について考えてみてください。当時、コンパイラは、戻り値の型の指定を int にデフォルト設定することを扱いませんでした。ここで、void へのポインタを返す関数 foo() があるとします。ただし、宣言がなかったため、コンパイラは整数を返さなければならないと考えます。たとえば一部の Motorola システムでは、整数とポインタが異なるレジスタに返されます。現在、コンパイラは正しいレジスタを使用しなくなり、代わりに他のレジスタの整数にキャストされたポインタを返します。このポインターを操作しようとした瞬間、すべてが崩壊します。
ヘッダー内で関数を宣言しても問題ありません。ただし、ヘッダーで宣言および定義する場合は、それらがインラインであることを確認してください。これを実現する 1 つの方法は、クラス定義内に定義を配置することです。それ以外の場合は、inline
キーワードを先頭に追加します。そうしないと、ヘッダーが複数の実装ファイルに含まれている場合に、ODR 違反が発生します。
宣言と定義を C++ ヘッダー ファイルとソース ファイルに分離することには、主に 2 つの利点があります。1つ目は、クラス/関数/何かが複数の場所にある場合に、 1つの定義ルールの問題を回避することです。#include
第二に、このようにすることで、インターフェースと実装を分離します。クラスまたはライブラリのユーザーは、ヘッダー ファイルを参照するだけで、それを使用するコードを記述できます。また、 Pimpl Idiomを使用してこれをさらに一歩進めて、ライブラリの実装が変更されるたびにユーザー コードを再コンパイルする必要がないようにすることもできます。
.h ファイルと .cpp ファイルの間でコードが繰り返されることの欠点については既に述べました。私は C++ コードを長く書きすぎたのかもしれませんが、それほど悪くはないと思います。いずれにせよ関数シグネチャを変更するたびにすべてのユーザー コードを変更する必要があるので、もう 1 つのファイルは何でしょうか? 初めてクラスを作成し、ヘッダーから新しいソース ファイルにコピー アンド ペーストする必要がある場合にのみ面倒です。
実際のもう 1 つの欠点は、サードパーティのライブラリを使用する適切なコードを作成 (およびデバッグ) するには、通常、その内部を確認する必要があることです。つまり、変更できなくてもソース コードにアクセスできます。ヘッダー ファイルとコンパイル済みのオブジェクト ファイルしかない場合、バグが自分のせいなのか相手のせいなのかを判断するのは非常に困難です。また、ソースを見ると、ドキュメントではカバーされていない可能性のあるライブラリを適切に使用および拡張する方法についての洞察が得られます。すべての人がライブラリに MSDN を同梱しているわけではありません。そして、優れたソフトウェア エンジニアは、あなたが夢にも思わなかったようなことをコードで行うという厄介な習慣を持っています。;-)
アドバンテージ
宣言を含めるだけで、他のファイルからクラスを参照できます。定義は、コンパイルプロセスの後半でリンクできます。
まだ見ていない利点の 1 つ: API
オープン ソースでない (つまり、プロプライエタリな) ライブラリまたはサード パーティ コードは、ディストリビューションと共に実装されません。ほとんどの企業は、ソース コードを配布することに満足していません。簡単な解決策は、DLL の使用を許可するクラス宣言と関数シグネチャを配布することです。
免責事項: 私はそれが正しい、間違っている、または正当化されていると言っているのではなく、私はそれをたくさん見てきました.
基本的に、クラス/関数/何でも2つのビューがあります:
名前、パラメーター、およびメンバー (構造体/クラスの場合) を宣言する宣言と、関数の動作を定義する定義です。
欠点の中には繰り返しがありますが、大きな利点の 1 つは、関数を次のように宣言int foo(float f)
し、実装 (=定義) に詳細を残すことができることです。そのため、関数 foo を使用したい人は誰でも、ヘッダー ファイルとライブラリへのリンクをインクルードするだけです。 objectfile に含まれているため、ライブラリ ユーザーとコンパイラは、定義されたインターフェイスを気にするだけで済みます。これにより、インターフェイスの理解が促進され、コンパイル時間が短縮されます。
不利益
これは多くの繰り返しにつながります。ほとんどの関数シグネチャは、(Paulious が指摘したように) 2 つ以上の場所に配置する必要があります。
前方宣言の大きな利点の 1 つは、慎重に使用すると、モジュール間のコンパイル時の依存関係を削減できることです。
ClassA.h が ClassB.h のデータ要素を参照する必要がある場合、多くの場合、ClassA.h で前方参照のみを使用し、ClassB.h を ClassA.h ではなく ClassA.cc に含めることができるため、コンパイル時間が短縮されます。依存。
大規模なシステムの場合、これはビルドの時間を大幅に節約できます。
この分離が正しく行われると、実装のみが変更された場合にコンパイル時間が短縮されます。