2010-12-28 3 views
42


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

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

ответ

36

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

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

тогда name будет определяться в каждом блоке перевода, #include ей этот заголовок файла.

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

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

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

файл: gizmo.cpp

#include "gizmo.h" 

const int Gizmo::count; 
+0

Правило с одним определением: «Никакая единица перевода не должна содержать более одного определения любой переменной, функции, типа класса, типа перечисления или шаблона». Если ваш первый пример «Gizmo» был законным, я не думаю, что он нарушил бы правило определения, потому что каждая единица перевода * имела бы одно определение «Gizmo :: name». –

+0

@ Daniel Trebbien: Это не весь ODR. Это всего лишь 3.2/1 - первый грубый «слой» ODR (чтобы позаботиться о самых очевидных нарушениях). Полный ODR имеет более подробный набор требований для каждого типа сущности. Для объектов внешней связи (а также функций внешней связи) ODR дополнительно ограничено в 3.2/3 одним и единственным определением * для всей программы *. – AnT

+1

@ Daniel Trebbien: Причина, по которой требование 3.2/1 было отделено от остальных, заключается в том, что нарушение 3.2/1 требует диагностики от компилятора, тогда как для нарушений 3.2/3 диагностика не требуется. – AnT

2

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

+7

Это то, что современная комбинация компилятора/компоновщика может легко разрешиться, а не достаточно хорошая причина для такого громоздкого ограничения. – martona

+0

@martona является правильным. Компилятор C++ может разрешать несколько определений функций-членов, поэтому почему бы не статические переменные-члены? Думаю, это то, о чем спрашивает ОП. –

+0

Я предполагаю, что только современные C++-линкеры могут разрешать множество определений методов (функции-члены). (Т.е. в последний раз я пытался иметь несколько определений метода несколько лет назад, и ссылка не удалась). До этого все методы, определенные в заголовке, должны были быть встроенными или статическими, а последний приводил к множественным копиям в связанном файл. –

0

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

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

11

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

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

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

1

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

Если член static данные из 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 Поучительно посмотреть на выходе сборщика г ++ для следующего кода:

// 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

Почему тогда недопустимо следующее: 'class my_class {public: static const double pi = 3.14; }; ' –

+0

@John: Я думаю, что это должно быть разрешено по той же причине, почему значения статических элементов данных типа' const' integer или 'const' перечисляются в объявлении. Я не знаю, почему это не так. –

+0

Это говорит о том, что «нетривиальная» инициализация может быть не единственной причиной, по которой она не допускается для нецелых типов. –

Смежные вопросы