web-dev-qa-db-ja.com

C ++静的メンバー変数とその初期化


C++クラスの静的メンバー変数の場合-初​​期化はクラス外で行われます。なぜだろうか?これに対する論理的な推論/制約はありますか?それとも、純粋にレガシーな実装ですか?標準が修正したくないものですか?

クラスで初期化を行うと、「直感的」でわかりやすくなります。また、変数の静的性とグローバル性の両方の意味もわかります。たとえば、静的constメンバーが表示されている場合。

47
kumar_m_kiran

これは基本的に、静的メンバーは One-Definition Rule に違反しないように、厳密に1つの変換単位で定義する必要があるためです。言語が次のようなものを許可する場合:

struct Gizmo
{
  static string name = "Foo";
};

nameは、#includesこのヘッダーファイル。

C++では、宣言内にintegral静的メンバーを定義できますが、単一の翻訳単位内に定義を含める必要がありますが、これは単なるショートカット、または構文糖。したがって、これは許可されています:

struct Gizmo
{
  static const int count = 42;
};

A)式がconst整数型または列挙型である限り、b)式はコンパイル時に評価でき、c)1つの定義ルールに違反しない定義がまだどこかにあります。

ファイル:gizmo.cpp

#include "gizmo.h"

const int Gizmo::count;
37
John Dibling

C++では、最初からinitializerの存在がオブジェクトdefinitionの排他属性であったため、初期化子を使用した宣言は常にdefinitionです=(ほぼ常に)。

ご存じのとおり、C++プログラムで使用される各外部オブジェクトは、1つの翻訳単位で1回だけ定義する必要があります。静的オブジェクトのクラス内初期化子を許可すると、この規則にすぐに反します。初期化子はヘッダーファイル(クラス定義が通常存在する場所)に入り、同じ静的オブジェクトの複数の定義(ヘッダーファイルを含む各翻訳単位に1つ)を生成します)。もちろん、これは受け入れられません。このため、静的クラスメンバの宣言アプローチは完全に「伝統的」なままです。ヘッダーファイルでdeclareのみ(つまり、イニシャライザは許可されません)、そしてdefine =選択した翻訳単位で(おそらく初期化子を使用して)。

整数型または列挙型のconst静的クラスメンバについては、この規則の例外が1つ作成されました。これは、そのようなエントリが整数定数式(ICE)の場合があるためです。 ICEの主な考え方は、コンパイル時に評価されるため、関連するオブジェクトの定義に依存しないということです。整数型または列挙型でこの例外が発生したのはそのためです。しかし、他の型については、C++の基本的な宣言/定義の原則と矛盾するだけです。

12
AnT

コードのコンパイル方法が原因です。クラス(ヘッダーにあることが多い)で初期化すると、ヘッダーが含まれるたびに静的変数のインスタンスが取得されます。これは間違いなく意図ではありません。クラスの外部で初期化すると、cppファイルで初期化することができます。

3
RedX

C++標準状態のセクション9.4.2、静的データメンバー:

staticデータメンバーがconst整数型またはconst列挙型の場合、クラス定義での宣言はconst-initializerを指定できますこれは整数定数式でなければなりません。

したがって、静的データメンバの値を「クラス内」に含めることができます(これは、クラスの宣言内であると想定しています)。ただし、静的データメンバーの型は、const整数型またはconst列挙型である必要があります。他の型の静的データメンバーの値をクラス宣言内で指定できない理由は、非自明な初期化が必要になる可能性が高いためです(つまり、コンストラクターを実行する必要があります)。

以下が合法であると想像してください:

_// my_class.hpp
#include <string>

class my_class
{
public:
  static std::string str = "static std::string";
//...
_

このヘッダーを含むCPPファイルに対応する各オブジェクトファイルには、_my_class::str_(sizeof(std::string)バイトで構成される)のストレージスペースのコピーだけでなく、 _std::string_コンストラクターはC文字列を取得します。 _my_class::str_のストレージスペースの各コピーは共通のラベルで識別されるため、リンカーは理論的にはストレージスペースのすべてのコピーを単一のコピーにマージできます。ただし、リンカーは、オブジェクトファイルのctorセクション内のコンストラクターコードのすべてのコピーを分離することはできません。次のコンパイルでstrを初期化するためにすべてのコードを削除するようリンカーに要求するようなものです。

_std::map<std::string, std::string> map;
std::vector<int> vec;
std::string str = "test";
int c = 99;
my_class mc;
std::string str2 = "test2";
_

[〜#〜] edit [〜#〜]次のコードについては、g ++のアセンブラ出力を確認することをお勧めします。

_// SO4547660.cpp
#include <string>

class my_class
{
public:
    static std::string str;
};

std::string my_class::str = "static std::string";
_

アセンブリコードは、次を実行することで取得できます。

_g++ -S SO4547660.cpp
_

G ++が生成する_SO4547660.s_ファイルを見ると、このような小さなソースファイルには多くのコードがあることがわかります。

___ZN8my_class3strE_は、_my_class::str_のストレージスペースのラベルです。ラベル___Z41__static_initialization_and_destruction_0ii_を持つ__static_initialization_and_destruction_0(int, int)関数のAssemblyソースもあります。その関数はg ++にとって特別ですが、g ++が初期化子以外のコードが実行される前に呼び出されることを確認することを知っているだけです。この関数の実装は___ZNSsC1EPKcRKSaIcE_を呼び出すことに注意してください。これは、std::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(char const*, std::allocator<char> const&)のマングル記号です。

上記の仮想の例に戻り、これらの詳細を使用すると、_my_class.hpp_を含むCPPファイルに対応する各オブジェクトファイルには、sizeof(std::string)バイトのラベル___ZN8my_class3strE_とアセンブリコードが含まれます。 __static_initialization_and_destruction_0(int, int)関数の実装内で___ZNSsC1EPKcRKSaIcE_を呼び出します。リンカは___ZN8my_class3strE_のすべてのオカレンスを簡単にマージできますが、オブジェクトファイルの__static_initialization_and_destruction_0(int, int)の実装内で___ZNSsC1EPKcRKSaIcE_を呼び出すコードを分離することはできません。

2
Daniel Trebbien

classブロックの外部で初期化を行う主な理由は、他のクラスメンバー関数の戻り値で初期化できるようにするためだと思います。 _a::var_をb::some_static_fn()で初期化する場合は、_.cpp_を含むすべての_a.h_ファイルが最初に_b.h_を含むことを確認する必要があります。特に(遅かれ早かれ)循環参照に出くわすと、それ以外の場合は不要なinterfaceでしか解決できなかった場合、それは混乱になります。同じ問題は、メインクラスの_.cpp_にすべてを入れるのではなく、クラスメンバー関数の実装を_.h_ファイルに持つ主な理由です。

少なくともメンバー関数では、ヘッダーに実装するオプションがあります。変数を使用すると、.cppファイルで初期化を行う必要があります。私はこの制限にまったく同意しませんし、それには正当な理由もないと思います。

0
martona