(Javaとは異なり)静的データメンバーをC ++でクラスの外部で個別に定義する必要があるのはなぜですか?


41
class A {
  static int foo () {} // ok
  static int x; // <--- needed to be defined separately in .cpp file
};

A::x.cppファイル(またはテンプレート用の同じファイル)で個別に定義する必要はありません。A::x同時に宣言と定義ができないのはなぜですか?

歴史的な理由で禁止されていますか?

私の主な質問は、staticデータメンバが同時に宣言/定義された場合(Javaと同じ)、機能に影響しますか?


ベストプラクティスとして、一般的には、初期化順序の問題を回避するために、静的メソッドで静的変数をラップする(おそらくローカルスタティックとして)方が適切です。
タマスシェレイ

2
このルールは、C ++ 11では実際に少し緩和されています。通常、const静的メンバーを定義する必要はありません。参照:en.wikipedia.org/wiki/...
mirk

4
@afishwhoswimsaround:すべての状況に一般化されたルールを指定するのは良い考えではありません(ベストプラクティスはコンテキストに適用する必要があります)。ここでは、存在しない問題を解決しようとしています。初期化順序の問題は、コンストラクターを持ち、他の静的ストレージ期間オブジェクトにアクセスするオブジェクトにのみ影響します。「x」はintであるため、「x」はプライベートであるため、1番目は適用されません。2番目は適用されません。第三に、これは質問とは関係ありません。
マーティンヨーク

1
スタックオーバーフローに属していますか?
モニカと軽さのレース

2
C ++ 17では、静的データメンバーのインライン初期化が可能です(非整数型の場合でも)inline static int x[] = {1, 2, 3};。en.cppreference.com/w/cpp/language/static#Static_data_members
ウラジミールレシェトニコフ

回答:


15

あなたが検討した制限はセマンティクスに関係していないと思います(初期化が同じファイルで定義されている場合、なぜ変更する必要がありますか?)複雑すぎる(新しいコンパイルモデルと既存のモデルを同時にサポートする)か、既存のコードをコンパイルできない(新しいコンパイルモデルを導入し、既存のモデルを削除する)。

C ++コンパイルモデルは、(ヘッダー)ファイルを含めることにより宣言をソースファイルにインポートするCのコンパイルモデルに由来します。このようにして、コンパイラは、含まれているすべてのファイルとそれらのファイルから含まれているすべてのファイルを含む、1つの大きなソースファイルを再帰的に認識します。これにはIMOの1つの大きな利点があります。つまり、コンパイラの実装が容易になるということです。もちろん、インクルードファイルには何でも、つまり宣言と定義の両方を書くことができます。宣言をヘッダーファイルに、定義を.cまたは.cppファイルに配置することをお勧めします。

一方、されている場合、コンパイラは非常によく知っているでコンパイルモデル持つことが可能である宣言インポートされているグローバルシンボルの別のモジュールで定義されているのか、それがされている場合の定義コンパイルグローバルシンボルので提供します現在のモジュール。後者の場合にのみ、コンパイラはこのシンボル(変数など)を現在のオブジェクトファイルに配置する必要があります。

たとえば、GNU Pascalaでは、次のa.pasようなファイルにユニットを書き込むことができます。

unit a;

interface

var MyStaticVariable: Integer;

implementation

begin
  MyStaticVariable := 0
end.

ここで、グローバル変数は同じソースファイルで宣言および初期化されます。

次に、aをインポートしてグローバル変数を使用するさまざまなユニットMyStaticVariable、たとえばユニットb(b.pas)を使用できます 。

unit b;

interface

uses a;

procedure PrintB;

implementation

procedure PrintB;
begin
  Inc(MyStaticVariable);
  WriteLn(MyStaticVariable)
end;
end.

およびユニットc(c.pas):

unit c;

interface

uses a;

procedure PrintC;

implementation

procedure PrintC;
begin
  Inc(MyStaticVariable);
  WriteLn(MyStaticVariable)
end;
end.

最後に、メインプログラムでユニットbとcを使用できますm.pas

program M;

uses b, c;

begin
  PrintB;
  PrintC;
  PrintB
end.

これらのファイルを個別にコンパイルできます。

$ gpc -c a.pas
$ gpc -c b.pas
$ gpc -c c.pas
$ gpc -c m.pas

そして、次を使用して実行可能ファイルを生成します。

$ gpc -o m m.o a.o b.o c.o

そしてそれを実行します:

$ ./m
1
2
3

ここでのコツは、コンパイラがプログラムモジュールでusesディレクティブを検出すると(たとえば、b.pasでaを使用する)、対応する.pasファイルを含まず、.gpiファイル、つまりプリコンパイルされたファイルを探すことです。インターフェイスファイル(ドキュメントを参照)。これらの.gpiファイルは.o、各モジュールがコンパイルされるときに、コンパイラーによってファイルとともに生成されます。そのため、グローバルシンボルMyStaticVariableはオブジェクトファイルで一度だけ定義されますa.o

Javaは同様の方法で動作します。コンパイラはクラスAをクラスBにインポートするときに、クラスファイルでAを探し、fileを必要としませんA.java。したがって、クラスAのすべての定義と初期化を1つのソースファイルに入れることができます。

C ++に戻ると、C ++で静的データメンバを別のファイルに定義する必要がある理由は、リンカまたはコンパイラで使用される他のツールによって課せられる制限よりも、C ++コンパイルモデルに関連しています。C ++では、一部のシンボルをインポートすると、現在のコンパイルユニットの一部として宣言を構築することになります。これは、テンプレートがコンパイルされる方法のため、とりわけ重要です。ただし、これは、インクルードファイルにグローバルシンボル(関数、変数、メソッド、静的データメンバー)を定義できない/すべきではないことを意味します。そうしないと、これらのシンボルがコンパイル済みオブジェクトファイルで多重定義される可能性があります。


42

静的メンバーはクラスのすべてのインスタンス間で共有されるため、1つの場所でのみ定義する必要があります。本当に、それらはいくつかのアクセス制限を持つグローバル変数です。

それらをヘッダーで定義しようとすると、そのヘッダーを含むすべてのモジュールで定義され、重複した定義がすべて検出されるため、リンク中にエラーが発生します。

はい、これは少なくとも部分的にはcfrontに由来する歴史的な問題です。ある種の隠された "static_members_of_everything.cpp"を作成し、それにリンクするコンパイラーを作成できます。ただし、下位互換性が失われ、そうすることによる実質的な利点はありません。


2
私の質問は現在の行動の理由ではなく、むしろそのような言語文法の正当化です。言い換えると、static変数が同じ場所(Javaなど)で宣言/定義されている場合、何が間違っているのでしょうか?
iammilind

8
@iammilindこの答えの説明のために文法が必要であることを理解していないと思います。なんで?C(およびC ++)のコンパイルモデルのため:cファイルとcppファイルは、個別のプログラムのように別々にコンパイルされる実際のコードファイルであり、それらをリンクして完全な実行可能ファイルを作成します。ヘッダーは実際にはコンパイラー向けのコードではなく、cおよびcppファイル内にコピーして貼り付けるテキストにすぎません。これで、何かが複数回定義されている場合、同じ名前のローカル変数が複数ある場合にコンパイルできないのと同じように、コンパイルできません。
クライム

1
@Klaim、staticメンバーはtemplateどうですか?表示する必要があるため、すべてのヘッダーファイルで使用できます。私はこの答えに異議を唱えていませんが、私の質問とも一致しません。
iammilind

@iammilindテンプレートは実際のコードではなく、コードを生成するコードです。テンプレートの各インスタンスには、コンパイラーによって提供される各静的宣言の静的インスタンスが1つだけあります。インスタンスを定義する必要がありますが、インスタンスのテンプレートを定義するとき、上記のように実際のコードではありません。テンプレートは、文字通り、コンパイラがコードを生成するためのコードのテンプレートです。
クライム

2
@iammilind:テンプレートは通常、静的変数を含むすべてのオブジェクトファイルでインスタンス化されます。LinuxでELFオブジェクトファイルを使用する場合、コンパイラはインスタンス化を弱い シンボルとしてマークします。これは、リンカーが同じインスタンス化の複数のコピーを結合することを意味します。同じ技術を使用してヘッダーファイルで静的変数を定義できるため、これが行われない理由は、おそらく歴史的な理由とコンパイルパフォーマンスの考慮事項の組み合わせです。次のC ++標準にモジュールが組み込まれると、コンパイルモデル全体が修正されることが期待されます
ハン

6

これの考えられる理由は、これにより、オブジェクトファイルとリンケージモデルが複数のオブジェクトファイルからの複数の定義のマージをサポートしていない環境でC ++言語を実装可能に保つことです。

クラス宣言(正当な理由により宣言と呼ばれる)は、複数の翻訳単位に取り込まれます。宣言に静的変数の定義が含まれている場合、複数の翻訳単位で複数の定義が作成されます(これらの名前には外部リンケージがあることに注意してください)。

そのような状況は可能ですが、リンカが文句を言わずに複数の定義を処理する必要があります。

(また、シンボルの種類または配置するセクションの種類に応じて実行できない限り、これはOne Definition Ruleと競合することに注意してください。)


6

C ++とJavaには大きな違いがあります。

Javaは、すべてを独自のランタイム環境に作成する独自の仮想マシン上で動作します。定義が複数回表示される場合、ランタイム環境が最終的に知っている同じオブジェクトに作用します。

C ++には「究極の知識の所有者」はいません。C++、C、Fortran Pascalなどはすべて、ソースコード(CPPファイル)から中間形式(OBJファイル、または「.o」ファイル)に「翻訳」されます。 OS)では、ステートメントは機械命令に変換され、名前はシンボルテーブルを介した間接アドレスになります。

プログラムはコンパイラーによって作成されるのではなく、別のプログラム(「リンカー」)によって作成され、すべてのOBJを(それらの言語に関係なく)結合します。効果的な定義。

リンカが機能する方法によって、定義(変数の物理空間を作成するもの)は一意でなければなりません。

C ++はそれ自体ではリンクせず、リンカーはC ++仕様によって発行されないことに注意してください。リンカーは、OSモジュールの構築方法のために存在します(通常CおよびASMで)。C ++はそのまま使用する必要があります。

現在:ヘッダーファイルは、いくつかのCPPファイルに「貼り付け」られるものです。すべてのCPPファイルは、他のすべてのCPPファイルとは無関係に翻訳されます。異なるCPPファイルを変換するコンパイラーは、すべて同じ定義を受け取ると、結果のすべてのOBJに定義済みオブジェクトの「作成コード」を配置します。

コンパイラーは、すべてのOBJが単一プログラムを形成するために一緒に使用されるか、別々の独立したプログラムを形成するために別々に使用されるかどうかを知りません(そして決して知りません)。

リンカは、定義がどのように、なぜ存在し、どこから来たのかを知りません(C ++についても知りません:すべての「静的言語」はリンクされる定義と参照を生成できます)。与えられた結果のアドレスで「定義された」与えられた「シンボル」への参照があることを知っているだけです。

特定のシンボルに複数の定義がある場合(定義を参照と混同しないでください)、リンカーはそれらをどうするかについて(言語に依存しない)知識がありません。

それは大きな町を形成するために、都市の数をマージするようなものです:あなたが2「を有する見つかっている場合は時間の広場」と「に行くことを求めて外から来る人の数時間の正方形を」、あなたは純粋な技術的な基準で決めることができません(それらの名前を割り当てた政治についての知識がなく、それらを管理する責任があります)それらを送信する正確な場所。


3
グローバルシンボルに関するJavaとC ++の違いは、仮想マシンを持つJavaではなく、C ++コンパイルモデルに関連しています。この点では、PascalとC ++を同じカテゴリに入れません。むしろ、私はCとC ++を「インポートされた宣言が含まれ、メインソースファイルと一緒にコンパイルされる言語」としてグループ化し、JavaとPascal(およびOCaml、Scala、Adaなど)を「インポートされた宣言は、エクスポートされたシンボルに関する情報を含むプリコンパイル済みファイルでコンパイラによって検索されます。
ジョルジオ

1
@Giorgio:Javaへの参照は歓迎されないかもしれませんが、Emilioの答えは、問題の要点、つまり、個別のコンパイル後のオブジェクトファイル/リンカーフェーズに到達することでほぼ正しいと思います。
ixache

5

そうしないと、コンパイラーは変数をどこに置くかわからないためです。各cppファイルは個別にコンパイルされ、他のファイルについては知りません。リンカは変数、関数などを解決します。個人的にはvtableとstaticメンバーの違いはわかりません(vtableが定義されているファイルを選択する必要はありません)。

私は、コンパイラー作成者がそれをそのように実装する方が簡単だとほとんど思います。クラス/構造の外側に静的変数が存在し、おそらく一貫性の理由か、標準でその制限を定義したコンパイラライターにとって「実装しやすい」ためです。


2

理由を見つけたと思う。static別のスペースで変数を定義すると、変数を任意の値に初期化できます。初期化されていない場合、デフォルトで0になります。

C ++ 11より前は、C ++ではクラス内の初期化は許可されていませんでした。したがって、次のように書くことはできません

struct X
{
  static int i = 4;
};

したがって、変数を初期化するには、クラスの外部で次のように記述する必要があります。

struct X
{
  static int i;
};
int X::i = 4;

他の回答でも説明したように、int X::i現在はグローバルであり、多くのファイルでグローバルを宣言すると、複数のシンボルリンクエラーが発生します。

したがってstatic、別の翻訳単位内でクラス変数を宣言する必要があります。ただし、次の方法ではコンパイラに複数のシンボルを作成しないよう指示する必要があると主張できます

static int X::i = 4;
^^^^^^

0

A :: xは単なるグローバル変数ですが、Aに名前空間が設定されており、アクセス制限があります。

他のグローバル変数と同様に、誰かがそれを宣言する必要があり、それは、Aコードの残りを含むプロジェクトに静的にリンクされたプロジェクトで行われることさえあります。

私はこれらをすべて悪い設計と呼びますが、この方法を活用できる機能がいくつかあります。

  1. コンストラクターの呼び出し順序... intには重要ではありませんが、他の静的変数またはグローバル変数にアクセスする可能性のあるより複雑なメンバーにとっては、重要になる可能性があります。

  2. 静的初期化子-クライアントにA :: xの初期化先を決定させることができます。

  3. c ++およびcでは、ポインタを介してメモリにフルアクセスできるため、変数の物理的な場所は重要です。リンクオブジェクト内の変数の場所に基づいて、悪用できる非常にいたずらなことがあります。

私はこれらがこの状況が「なぜ」生じたのか疑っています。これはおそらく、CがC ++に変わるだけの進化であり、後方互換性の問題であり、今では言語を変更できません。


2
これは作られたポイントを超える大幅な提供の何にも思えるし、前6つの回答で説明していません
ブヨ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.