web-dev-qa-db-ja.com

C ++ヘッダーファイルに実装を含めるにはどうすればよいですか?

OK、C/C++の専門家ではありませんが、ヘッダーファイルの目的は関数を宣言することであり、C/CPPファイルは実装を定義することだと思いました。

しかし、今夜いくつかのC++コードを確認すると、クラスのヘッダーファイルでこれが見つかりました...

public:
    UInt32 GetNumberChannels() const { return _numberChannels; } // <-- Huh??

private:
    UInt32 _numberChannels;

では、なぜヘッダーに実装があるのですか? constキーワードと関係がありますか?それはクラスメソッドをインライン化しますか?この方法で行うことと、CPPファイルで実装を定義することの利点/ポイントは何ですか?

59
MarqueIV

OK、C/C++の専門家ではありませんが、ヘッダーファイルの目的は関数を宣言することであり、C/CPPファイルは実装を定義することだと思いました。

ヘッダーファイルの真の目的は、複数のソースファイル間でコードを共有することです。コード管理を改善するために、実装から宣言を分離するために一般的にが使用されますが、これは要件ではありません。ヘッダーファイルに依存しないコードを作成することも、ヘッダーファイルだけで構成されるコードを作成することもできます(STLおよびBoostライブラリはその良い例です)。 preprocessorが_#include_ステートメントを検出すると、ステートメントを参照されているファイルの内容に置き換え、compilerは、完了した前処理済みコードのみを表示します。

したがって、たとえば、次のファイルがある場合:

Foo.h:

_#ifndef FooH
#define FooH

class Foo
{
public:
    UInt32 GetNumberChannels() const;

private:
    UInt32 _numberChannels;
};

#endif
_

Foo.cpp:

_#include "Foo.h"

UInt32 Foo::GetNumberChannels() const
{
    return _numberChannels;
}
_

Bar.cpp:

_#include "Foo.h"

Foo f;
UInt32 chans = f.GetNumberChannels();
_

preprocessorはFoo.cppとBar.cppを別々に解析し、compiler次に解析します:

Foo.cpp:

_class Foo
{
public:
    UInt32 GetNumberChannels() const;

private:
    UInt32 _numberChannels;
};

UInt32 Foo::GetNumberChannels() const
{
    return _numberChannels;
}
_

Bar.cpp:

_class Foo
{
public:
    UInt32 GetNumberChannels() const;

private:
    UInt32 _numberChannels;
};

Foo f;
UInt32 chans = f.GetNumberChannels();
_

Bar.cppはBar.objにコンパイルされ、Foo::GetNumberChannels()を呼び出すための参照が含まれます。 Foo.cppはFoo.objにコンパイルされ、Foo::GetNumberChannels()の実際の実装が含まれます。コンパイル後、linkerは.objファイルを照合し、それらをリンクして最終的な実行可能ファイルを生成します。

では、なぜヘッダーに実装があるのですか?

メソッドの宣言内にメソッドの実装を含めることにより、暗黙的にインラインとして宣言されます(明示的に使用できる実際のinlineキーワードもあります)。コンパイラーが関数をインライン化する必要があることを示すのは、関数が実際にインライン化されることを保証するものではありません。しかし、もしそうなら、インライン関数がどこから呼び出されても、関数にジャンプして呼び出し元に戻るためにCALLステートメントを生成する代わりに、関数の内容が呼び出しサイトに直接コピーされます終了します。コンパイラは、可能であれば、周囲のコードを考慮して、コピーされたコードをさらに最適化できます。

Constキーワードと関係がありますか?

いいえ。constキーワードは、メソッドが実行時に呼び出されるオブジェクトの状態を変更しないことをコンパイラに示すだけです。

この方法で行うことと、CPPファイルで実装を定義することの利点/ポイントは何ですか?

効果的に使用すると、コンパイラは通常、より高速で最適化されたマシンコードを生成できます。

99
Remy Lebeau

ヘッダーファイルに関数を実装することは完全に有効です。これに関する唯一の問題は、one-definition-ruleを破ることです。つまり、他の複数のファイルのヘッダーを含めると、コンパイラエラーが発生します。

ただし、1つの例外があります。関数をインラインとして宣言すると、one-definition-ruleから免除されます。クラス定義内で定義されたメンバー関数は暗黙的にインラインであるため、これがここで発生しています。

インライン自体は、関数がインライン化の適切な候補である可能性があることをコンパイラーに示すヒントです。つまり、単純な関数呼び出しではなく、関数の定義に呼び出しを拡張します。これは、生成されたファイルのサイズをより高速なコードに引き換える最適化です。現代のコンパイラでは、関数にこのインライン化のヒントを提供することは、one-definition-ruleに与える影響を除いて、ほとんど無視されます。また、コンパイラは、inline(明示的または暗黙的に)宣言されていなくても、適切と思われる関数を常に自由にインライン化できます。

あなたの例では、引数リストの後のconstの使用は、メンバー関数が呼び出されるオブジェクトを変更しないことを示します。実際には、これはthisによって指し示されたオブジェクト、および拡張によりすべてのクラスメンバーがconstと見なされることを意味します。つまり、それらを変更しようとすると、コンパイル時エラーが生成されます。

28
Agentlien

暗黙的に宣言されたinlineはメンバー関数であるためdefinedクラス宣言内。これは、コンパイラーがインライン化することを意味しませんが、 1つの定義ルール を破らないことを意味します。 constとはまったく関係ありません*。また、関数の長さと複雑さにも無関係です。

非メンバー関数である場合、inlineとして明示的に宣言する必要があります。

inline void foo() { std::cout << "foo!\n"; }

* メンバー関数の最後にあるconstの詳細については、 here を参照してください。

5
juanchopanza

プレーンCでも、ヘッダーファイルにコードを挿入することができます。それを行う場合、通常はstaticを宣言する必要があります。そうしないと、同じヘッダーを含む複数の.cファイルによって「乗算定義関数」エラーが発生します。

プリプロセッサはインクルードファイルをテキストでインクルードするため、インクルードファイル内のコードはソースファイルの一部になります(少なくともコンパイラの観点からは)。

C++の設計者は、優れたデータ隠蔽を備えたオブジェクト指向プログラミングを可能にすることを望んでいたため、多くのゲッター関数とセッター関数を見ることが期待されていました。彼らは不当なパフォーマンスの低下を望んでいませんでした。そのため、C++は、ゲッターとセッターをヘッダーで宣言できるだけでなく、実際に実装できるように設計されているため、インライン化されます。示した関数はゲッターであり、そのC++コードがコンパイルされると、関数呼び出しはありません。その値を取り出すコードは、適切にコンパイルされます。

ヘッダーファイルとソースファイルの区別を持たず、コンパイラが理解する実際の「モジュール」だけを持つコンピューター言語を作成することは可能です。 (C++はそれをしませんでした。ソースファイルとテキストに含まれるヘッダーファイルの成功したCモデルの上に構築されただけです。)ソースファイルがモジュールの場合、コンパイラーはモジュールからコードを引き出してから、そのコードをインライン化します。しかし、C++のやり方は実装が簡単です。

3
steveha

私の知る限り、ヘッダーファイル内に安全に実装できる2種類のメソッドがあります。

  • インラインメソッド-実装は使用される場所にコピーされるため、二重定義リンカーエラーの問題はありません。
  • テンプレートメソッド-それらはテンプレートのインスタンス化の瞬間に実際にコンパイルされます(例えば、誰かがテンプレートの代わりに型を入力したとき)ので、再び二重定義の問題の可能性はありません。

あなたの例は最初のケースに当てはまると思います。

1
Spook

C++標準引用符

C++ 17 N4659標準ドラフト 10.1.6「インライン指定子」は、メソッドが暗黙的にインラインであることを示しています。

4クラス定義内で定義された関数はインライン関数です。

そしてさらに下に行くと、インラインメソッドだけでなく、すべての翻訳単位でmustを定義する必要があることがわかります。

6インライン関数またはインライン変数は、odrが使用されるすべての翻訳単位で定義され、すべての場合でまったく同じ定義を持つものとします(6.2)。

これは、12.2.1「メンバー関数」の注記にも明示的に記載されています。

1メンバー関数は、そのクラス定義で定義(11.4)できます。この場合、インラインメンバー関数(10.1.6)[...]

3 [注:プログラムには、非インラインメンバー関数の定義が1つしかありません。プログラムには複数のインラインメンバー関数定義が存在する場合があります。 6.2および10.1.6を参照してください。 —終了ノート]

GCC 8.3の実装

main.cpp

struct MyClass {
    void myMethod() {}
};

int main() {
    MyClass().myMethod();
}

シンボルのコンパイルと表示:

g++ -c main.cpp
nm -C main.o

出力:

                 U _GLOBAL_OFFSET_TABLE_
0000000000000000 W MyClass::myMethod()
                 U __stack_chk_fail
0000000000000000 T main

その後、man nmMyClass::myMethodシンボルは、ELFオブジェクトファイルでは弱いとマークされています。これは、複数のオブジェクトファイルに表示される可能性があることを意味します。

"W" "w"シンボルは、弱いオブジェクトシンボルとして特にタグ付けされていない弱いシンボルです。弱い定義シンボルが通常の定義シンボルとリンクされている場合、通常の定義シンボルはエラーなしで使用されます。弱い未定義のシンボルがリンクされ、シンボルが定義されていない場合、シンボルの値はエラーなしにシステム固有の方法で決定されます。一部のシステムでは、大文字はデフォルト値が指定されていることを示します。

コードをコンパイルしたかどうかを知っていると確信しているので、クラスヘッダーファイルに実装を保持します。 constキーワードは、メンバーを変更しないことを保証します。メソッド呼び出しの間、インスタンスを保持します immutable

0
Jonas Byström