Статическая членная переменная С++ и ее инициализация


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

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

Ответ 1

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

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

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

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

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

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

file: gizmo.cpp

#include "gizmo.h"

const int Gizmo::count;

Ответ 2

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

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

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

Ответ 3

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

Ответ 4

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

Если член данных static имеет тип const integer или const перечисления, его объявление в определении класса может указывать инициализатор const, который должен быть интегральным постоянным выражением.

Следовательно, значение статического члена данных может быть включено "внутри класса" (с помощью которого я предполагаю, что вы имеете в виду в объявлении класса). Однако тип элемента статических данных должен быть типом перечисления const integer или const. Причина, по которой значения статических элементов данных других типов не могут быть указаны в объявлении класса, заключается в том, что требуется нетривиальная инициализация (т.е. Должен выполняться конструктор).

Представьте себе, были ли следующие правила:

// my_class.hpp
#include <string>

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

Каждый объектный файл, соответствующий файлам CPP, которые включают этот заголовок, будет иметь не только копию пространства хранения для my_class::str (состоящего из sizeof(std::string) bytes), но также и "секцию 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).

Ответ 5

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

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