C ++ статическая переменная-член и ее инициализация - PullRequest
47 голосов
/ 28 декабря 2010


Для статических переменных-членов в классе C ++ - инициализация выполняется вне класса.Интересно, почему?Любые логические рассуждения / ограничения для этого?Или это чисто устаревшая реализация, которую стандарт не хочет исправлять?

Я думаю, что инициализация в классе более "интуитивна" и менее запутана. Это также дает ощущение статичности и глобальности переменной.Например, если вы видите статический член const.

Ответы [ 5 ]

37 голосов
/ 28 декабря 2010

По сути, это потому, что статические члены должны быть определены ровно в одной единице перевода, чтобы не нарушать Правило единого определения .Если бы язык позволял что-то вроде:

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

, тогда name было бы определено в каждой единице перевода, которая #include s этот заголовочный файл.

C ++ позволяет вам определять встроенные статические члены в объявлении, но вы все равно должны включить определение в одну единицу перевода, но это всего лишь ярлык или синтаксический сахар.Итак, это разрешено:

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

До тех пор, пока a) выражение будет const целочисленным или перечислимым, b) выражение может быть вычислено во время компиляции, и c) все еще существуетопределение где-то, что не нарушает одно правило определения:

file: gizmo.cpp

#include "gizmo.h"

const int Gizmo::count;
12 голосов
/ 28 декабря 2010

В C ++ с начала времен присутствие инициализатора было исключительным атрибутом объекта определение , то есть объявление с инициализатором всегда является определением (почти всегда).

Как вы должны знать, каждый внешний объект, используемый в программе на C ++, должен быть определен один раз и только один раз только в одной единице перевода. Разрешение инициализаторов в классе для статических объектов немедленно пошло бы вразрез с этим соглашением: инициализаторы будут помещаться в файлы заголовков (где обычно находятся определения классов) и, таким образом, генерировать несколько определений одного и того же статического объекта (по одному для каждой единицы перевода, которая включает файл заголовка). ). Это, конечно, недопустимо. По этой причине подход к объявлению для статических членов класса остается совершенно «традиционным»: вы только объявляете в заголовочном файле (т. Е. Инициализатор не разрешен), а затем вы определяете это в выбранной вами единице перевода (возможно, с инициализатором).

Одно исключение из этого правила было сделано для статических членов класса const целочисленных или перечислимых типов, поскольку такие записи могут использоваться для выражений с интегральными константами (ICE). Основная идея ICE заключается в том, что они оцениваются во время компиляции и, таким образом, не зависят от определений участвующих объектов. Вот почему это исключение было возможно для целочисленных или перечислимых типов. Но для других типов это будет просто противоречить основным принципам объявления / определения C ++.

3 голосов
/ 28 декабря 2010

Это из-за способа компиляции кода. Если бы вы инициализировали его в классе, который часто находится в заголовке, каждый раз, когда заголовок включается, вы получите экземпляр статической переменной. Это определенно не намерение. Его инициализация вне класса дает вам возможность инициализировать его в файле cpp.

2 голосов
/ 28 декабря 2010

Раздел 9.4.2, Статические члены данных, стандартных состояний C ++:

Если 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) байтов), но также и «секцию ctor», которая вызывает 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

Просматривая файл SO4547660.s, сгенерированный g ++, вы можете увидеть, что для такого небольшого исходного файла имеется много кода.

__ZN8my_class3strE - это метка места для хранения my_class::str. Существует также источник сборки функции __static_initialization_and_destruction_0(int, int) с меткой __Z41__static_initialization_and_destruction_0ii. Эта функция является особенной для g ++, но просто знайте, что g ++ будет вызывать ее до того, как будет выполнен любой неинициализаторский код. Обратите внимание, что реализация этой функции вызывает __ZNSsC1EPKcRKSaIcE. Это искаженный символ для std::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(char const*, std::allocator<char> const&).

Возвращаясь к приведенному выше гипотетическому примеру и используя эти детали, каждый объектный файл, соответствующий файлу CPP, который включает my_class.hpp, будет иметь метку __ZN8my_class3strE для sizeof(std::string) байт, а также код сборки для вызова __ZNSsC1EPKcRKSaIcE в рамках реализации функции __static_initialization_and_destruction_0(int, int). Компоновщик может легко объединить все вхождения __ZN8my_class3strE, но он не может изолировать код, вызывающий __ZNSsC1EPKcRKSaIcE в реализации объектного файла __static_initialization_and_destruction_0(int, int).

0 голосов
/ 28 декабря 2010

Я думаю, что основной причиной инициализации вне блока class является возможность инициализации с помощью возвращаемых значений других функций-членов класса. Если вы хотите инициализировать a::var с помощью b::some_static_fn(), вам нужно убедиться, что каждый файл .cpp, который включает a.h, сначала включает b.h. Это будет беспорядок, особенно когда (рано или поздно) вы натолкнетесь на циклическую ссылку, которую вы могли бы разрешить только с помощью ненужного interface. Та же проблема является основной причиной наличия реализаций функций-членов класса в файле .cpp вместо помещения всего в основной класс '.h.

По крайней мере, для функций-членов у вас есть возможность реализовать их в заголовке. С переменными вы должны выполнить инициализацию в файле .cpp. Я не совсем согласен с этим ограничением и не думаю, что для этого есть все основания.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...