web-dev-qa-db-ja.com

ヘッダーにプライベートメンバーを配置する必要があるのはなぜですか?

プライベート変数は、複雑さと実装の詳細をクラスのユーザーに隠す方法です。これはかなりいい機能です。しかし、c ++でそれらをクラスのヘッダーに配置する必要がある理由がわかりません。これには2つの迷惑な欠点があります。

  • それはユーザーからヘッダーを乱雑にします
  • 内部が変更されるたびに、すべてのクライアントライブラリの再コンパイルを強制します

この要件の背後に概念的な理由はありますか?コンパイラーからの作業を容易にするためだけですか?

67
Simon Bergot

これは、インスタンス化時に適切な量のメモリを割り当てるために、C++コンパイラがクラスの実際のサイズを知っている必要があるためです。サイズにはすべてのメンバーが含まれ、プライベートメンバーも含まれます。

これを回避する1つの方法は、Pimplイディオムを使用することです。これは、Herb Sutterが彼のGuru of the Weekシリーズ #24 で説明しています。 #28

更新

実際、これ(より一般的には、ヘッダー/ソースファイルの区別と#includes)は、Cから継承されたC++の主要なハードルです。 C++ Cが作成されましたが、大規模なソフトウェア開発の経験はまだありませんでした。それ以来学んだ教訓は新しい言語の設計者に注意を払われましたが、C++には下位互換性の要件があるため、言語のこのような根本的な問題に対処するのは非常に困難です。

75
Péter Török

クラス定義は、クラスのオブジェクトを使用した場所にかかわらず、コンパイラーがメモリー内に同じレイアウトを生成するのに十分である必要があります。たとえば、次のようなものが与えられたとします。

class X { 
    int a;
public:
    int b;
};

コンパイラーは通常、オフセット0にa、オフセット4bを持ちます。コンパイラがこれを単に見た場合:

class X { 
public:
    int b;
};

bがオフセット4ではなくオフセット0にあると "考え"ます。その定義を使用してコードをbに割り当てた場合、最初の定義を使用したコードはa getになります。変更、およびその逆。

クラスのプライベート部分に変更を加えることの影響を最小限に抑える通常の方法は、通常、pimplイディオムと呼ばれます(これについては、Googleが大量の情報を提供できると確信しています)。

17
Jerry Coffin

いくつかの理由が考えられます。プライベートメンバーは他のほとんどのクラスからはアクセスできませんが、フレンドクラスからはアクセスできます。したがって、少なくともこの場合、それらはヘッダーで必要になる可能性があるため、friendクラスはそれらが存在することを確認できます。

依存ファイルの再コンパイルは、インクルード構造によって異なる場合があります。別のヘッダーの代わりに.cppファイルに.hファイルを含めると、再コンパイルの長いチェーンを防ぐことができる場合があります。

3

これが必要な主な理由は、クラスを使用するコードは、それを処理できるコードを生成するために、プライベートクラスメンバーについて知る必要があるためです。

次のクラスについて考えてみます。

_//foo.h
class foo {
    char private_member[0x100];
public:
    void do_something();
};
_

これは次のコードで使用されます。

_#include "foo.h"
void f() {
    foo x;
    x.do_something();
}
_

このコードを32ビットLinuxでgccを使用してコンパイルし、分析を簡略化するためにいくつかのフラグを指定すると、関数fは(コメント付きで)コンパイルされます。

_;allocate 256 bytes on the stack for a foo, plus 4 bytes for a foo*
   0:   81 ec 04 01 00 00       sub    esp,0x104
;the trivial constructor for foo is elided here
;compute the address of x
   6:   8d 44 24 04             lea    eax,[esp+0x4]
;pass the foo* to the function being called (the implicit first argument, this)
   a:   89 04 24                mov    DWORD PTR [esp],eax
;call x.do_something()
   d:   e8 fc ff ff ff          call   e <_Z1fv+0xe>
;deallocate the stack space used for this function
  12:   81 c4 04 01 00 00       add    esp,0x104
;return
  18:   c3                      ret    
_

ここで注目すべき点が2つあります。

  • f()のコードは、適切な量のスペースを割り当てるためにsizeof(foo)を知る必要があります。
  • Fooのコンストラクターへの呼び出しはありません。これは、コンストラクターは取るに足らないものですが、fooがそのプライベートクラスメンバーを知らないと、自明なコンストラクターがあるかどうかを知ることは不可能だからです。

基本的に、プログラマーはクラスを使用するためにその実装について知っている必要はありませんが、コンパイラーは知っています。 C++設計者は、いくつかのレベルの間接参照を導入することにより、プライベートクラスメンバーがクライアントコードに認識されないようにすることができましたが、場合によっては、パフォーマンスに深刻な影響を与える可能性があります。代わりに、プログラマは、トレードオフに価値があると判断した場合、この間接指定を自分で実装することを決定できます(たとえば pImpl idiom を使用)。

2
Chris

根本的な問題は、C++のヘッダーファイルにコンパイラが必要とする情報が含まれている必要があることですが、クラスの人間のユーザーのための参照としても使用されます。

クラスの人間のユーザーとして、私は多くのことを気にしません。プライベートフィールドは1つで、関数とメソッドのインライン実装は別のものです。一方、コンパイラーは多くのことを気にします。

Swiftのようなより最近の言語では、少しCPU時間を使ってこれを解決します。永続的に保存されるファイルは1つだけであり、それがソースファイルです。コンパイラは、クラスを使用して他のソースファイルをコンパイルするために必要なすべてのものです。このアイデアにより、C++ではヘッダーファイルとなるものを人間のユーザーに示すことができ、ユーザーが望むものだけが含まれます。つまり、プライベートメソッドではなく、すべてのコメントが含まれます型、定数、メソッドなどが存在します。人間のユーザーがまさに望んでいるものです(Xcode IDEオプションで、メソッドが完全に異なる言語から呼び出される方法をオプションで示します)。

0
gnasher729