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



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

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

5 43

5 ответов:

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

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

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

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

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

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

Файл: gizmo.cpp

#include "gizmo.h"

const int Gizmo::count;

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

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

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

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

Раздел 9.4.2, статические элементы данных, стандарта C++ гласит:

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

Таким образом, возможно, что значение статического члена данных будет включено "в класс" (под которым я предполагаю, что вы подразумеваете объявление класса). Однако, тип статического элемента данных должен быть 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).

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

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