Почему статические члены данных должны быть определены вне класса отдельно в C ++ (в отличие от Java)?

41
class A {
  static int foo () {} // ok
  static int x; // <--- needed to be defined separately in .cpp file
};

Я не вижу необходимости A::xопределять отдельно в файле .cpp (или тот же файл для шаблонов). Почему нельзя A::xобъявить и определить одновременно?

Было ли это запрещено по историческим причинам?

Мой главный вопрос: повлияет ли это на какую-либо функциональность, если staticэлементы данных были объявлены / определены одновременно (так же, как Java )?

iammilind
источник
Лучше всего, как правило, лучше обернуть вашу статическую переменную в статический метод (возможно, как локальный статический), чтобы избежать проблем порядка инициализации.
Тамас Селеи
2
Это правило немного смягчено в C ++ 11. Постоянные статические члены обычно больше не нужно определять. См .: en.wikipedia.org/wiki/…
mirk
4
@afishwhoswimsaround: Указывать на обобщенные правила для всех ситуаций не очень хорошая идея (лучшие практики должны применяться с контекстом). Здесь вы пытаетесь решить проблему, которая не существует. Проблема порядка инициализации затрагивает только те объекты, которые имеют конструкторы и имеют доступ к другим статическим объектам продолжительности хранения. Поскольку «x» является int, первое не применяется, поскольку «x» является приватным, второе не применяется. В-третьих, это не имеет ничего общего с вопросом.
Мартин Йорк,
1
Принадлежит переполнению стека?
Легкость гонок с Моникой
2
С ++ 17 позволяет инлайн инициализации статических элементов данных (даже для нецелых типов): inline static int x[] = {1, 2, 3};. См. En.cppreference.com/w/cpp/language/static#Static_data_members
Владимир Решетников,

Ответы:

15

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

Модель компиляции C ++ основана на модели C, в которой вы импортируете объявления в исходный файл, включая файлы (заголовков). Таким образом, компилятор видит ровно один большой исходный файл, содержащий все включенные файлы и все файлы, включенные из этих файлов, рекурсивно. Это имеет одно большое преимущество IMO, а именно то, что он облегчает реализацию компилятора. Конечно, вы можете написать что-нибудь во включенных файлах, то есть как объявления, так и определения. Хорошей практикой является размещение объявлений в заголовочных файлах и определений в файлах .c или .cpp.

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

Например, в GNU Pascal вы можете записать модуль aв файл a.pasследующим образом:

unit a;

interface

var MyStaticVariable: Integer;

implementation

begin
  MyStaticVariable := 0
end.

где глобальная переменная объявлена ​​и инициализирована в том же исходном файле.

Тогда у вас могут быть разные модули, которые импортируют a и используют глобальную переменную MyStaticVariable, например, единица b ( b.pas):

unit b;

interface

uses a;

procedure PrintB;

implementation

procedure PrintB;
begin
  Inc(MyStaticVariable);
  WriteLn(MyStaticVariable)
end;
end.

и блок с ( c.pas):

unit c;

interface

uses a;

procedure PrintC;

implementation

procedure PrintC;
begin
  Inc(MyStaticVariable);
  WriteLn(MyStaticVariable)
end;
end.

Наконец, вы можете использовать блоки b и c в основной программе m.pas:

program M;

uses b, c;

begin
  PrintB;
  PrintC;
  PrintB
end.

Вы можете скомпилировать эти файлы отдельно:

$ gpc -c a.pas
$ gpc -c b.pas
$ gpc -c c.pas
$ gpc -c m.pas

и затем создайте исполняемый файл с:

$ gpc -o m m.o a.o b.o c.o

и запустить его:

$ ./m
1
2
3

Хитрость здесь в том , что когда компилятор видит использование директивы в программном модуле (например , использует в b.pas), она не включает в себя соответствующий .pas файл, но выглядит для .gpi файла, т.е. для предварительно скомпилированных файл интерфейса (см. документацию ). Эти .gpiфайлы генерируются компилятором вместе с .oфайлами при компиляции каждого модуля. Таким образом, глобальный символ MyStaticVariableопределяется только один раз в объектном файле a.o.

Java работает аналогичным образом: когда компилятор затем импортирует класс A в класс B, он ищет файл класса для A и не нуждается в этом файле A.java. Таким образом, все определения и инициализации для класса A могут быть помещены в один исходный файл.

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

Джорджио
источник
42

Поскольку статические члены являются общими для ВСЕХ экземпляров класса, они должны быть определены в одном и только одном месте. Действительно, это глобальные переменные с некоторыми ограничениями доступа.

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

Да, это, по крайней мере, частично историческая проблема, связанная с cfront; может быть написан компилятор, который создаст некий скрытый «static_members_of_everything.cpp» и ссылку на него. Однако это нарушило бы обратную совместимость, и никакой реальной выгоды от этого не было бы.

mjfgates
источник
2
Мой вопрос не является причиной нынешнего поведения, а скорее оправдывает такую ​​грамматику языка. Другими словами, предположим, что если staticпеременные объявлены / определены в одном месте (например, в Java), то что может пойти не так?
Iammilind
8
@iammilind Я думаю, вы не понимаете, что грамматика необходима из-за объяснения этого ответа. Теперь почему? Из-за модели компиляции C (и C ++): файлы c и cpp являются реальным файлом кода, который скомпилирован отдельно, как отдельные программы, затем они связаны вместе, чтобы сделать полный исполняемый файл. Заголовки на самом деле не являются кодом для компилятора, это всего лишь текст для копирования и вставки в файлы c и cpp. Теперь, если что-то определено несколько раз, оно не может скомпилировать его, так же, как оно не скомпилируется, если у вас есть несколько локальных переменных с одним и тем же именем.
Klaim
1
@ Klaim, а как насчет staticучастников template? Они разрешены во всех заголовочных файлах, так как они должны быть видны. Я не оспариваю этот ответ, но он также не соответствует моему вопросу.
Iammilind
Шаблоны @iammilind - это не настоящий код, это код, который генерирует код. Каждый экземпляр шаблона имеет один и только один статический экземпляр каждого статического объявления, предоставляемого компилятором. Вы все еще должны определить экземпляр, но, как вы определяете шаблон экземпляра, это не реальный код, как сказано выше. Шаблоны, в буквальном смысле, шаблоны кода для компилятора для генерации кода.
Klaim
2
@iammilind: шаблоны обычно создаются в каждом объектном файле, включая их статические переменные. В Linux с объектными файлами ELF компилятор помечает экземпляры как слабые символы , что означает, что компоновщик объединяет несколько копий одного экземпляра. Эту же технологию можно использовать для определения статических переменных в заголовочных файлах, поэтому причина, по которой это не сделано, вероятно, является комбинацией исторических причин и соображений производительности компиляции. Надеемся, что вся модель компиляции будет исправлена, как только следующий стандарт C ++ будет включать модули .
хань
6

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

Объявление класса (называемое объявлением по уважительным причинам) помещается в несколько единиц перевода. Если бы в объявлении содержались определения для статических переменных, то вы бы получили несколько определений в нескольких единицах перевода (и помните, что эти имена имеют внешнюю связь).

Такая ситуация возможна, но требует, чтобы компоновщик обрабатывал несколько определений без жалоб.

(И обратите внимание, что это противоречит Правилу Одного Определения, если это не может быть сделано в соответствии с типом символа или типом раздела, в котором он размещен.)

Kaz
источник
6

Существует большая разница между C ++ и Java.

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

В C ++ нет «конечного владельца знаний»: C ++, C, Fortran Pascal и т. Д. Все являются «переводчиками» из исходного кода (файла CPP) в промежуточный формат (файл OBJ или файл «.o», в зависимости от ОС), где операторы переводятся в машинные инструкции, а имена становятся косвенными адресами, опосредованными таблицей символов.

Программа создается не компилятором, а другой программой («компоновщиком»), которая объединяет все OBJ (независимо от языка, с которого они происходят), перенаправляя все адреса, которые относятся к символам, к их эффективное определение.

По тому, как работает компоновщик, определение (что создает физическое пространство для переменной) должно быть уникальным.

Обратите внимание, что C ++ сам по себе не связывает, и что компоновщик не выпускается спецификациями C ++: компоновщик существует из-за способа сборки модулей ОС (обычно в C и ASM). С ++ должен использовать это так, как есть.

Теперь: заголовочный файл - это то, что нужно «вставить» в несколько файлов CPP. Каждый файл CPP переводится независимо от любого другого. Компилятор, переводящий разные CPP-файлы, все получающие одно и то же определение, поместит « код создания » для определенного объекта во все результирующие OBJ.

Компилятор не знает (и никогда не узнает), будут ли когда-либо использоваться все эти OBJ для формирования единой программы или отдельно для формирования разных независимых программ.

Компоновщик не знает, как и почему существуют определения и откуда они берутся (он даже не знает о C ++: каждый «статический язык» может создавать определения и ссылки, которые должны быть связаны). Он просто знает, что есть ссылки на данный «символ», который «определен» по данному результирующему адресу.

Если для данного символа существует несколько определений (не путайте определения со ссылками), компоновщик не знает (не зависит от языка), что с ними делать.

Это похоже на объединение нескольких городов в один большой город: если у вас есть два « квадрата времени » и несколько людей, приходящих извне с просьбой перейти на « квадрат времени », вы не можете выбрать чисто техническую основу. (без каких-либо знаний о политике, которая назначила эти имена и будет отвечать за их управление) в каком именно месте их отправлять.

Эмилио Гаравалья
источник
3
Разница между Java и C ++ по отношению к глобальным символам связана не с наличием виртуальной машины в Java, а с моделью компиляции C ++. В этом отношении я бы не поставил Pascal и C ++ в одну категорию. Скорее я бы сгруппировал C и C ++ вместе как «языки, в которые импортированные объявления включены и скомпилированы вместе с основным исходным файлом», в отличие от Java и Pascal (и, возможно, OCaml, Scala, Ada и т. Д.) Как «языки, в которых импортированные объявления ищутся компилятором в предварительно скомпилированных файлах, содержащих информацию об экспортированных символах ".
Джорджио
1
@ Джорджио: ссылка на Java может не приветствоваться, но я думаю, что ответ Эмилио в основном правильный, если разобраться в сути проблемы, а именно в фазе объектного файла / компоновщика после отдельной компиляции.
ixache
5

Это необходимо, потому что в противном случае компилятор не знает, куда поместить переменную. Каждый файл cpp индивидуально компилируется и не знает о другом. Компоновщик разрешает переменные, функции и т. Д. Я лично не вижу, в чем разница между членами vtable и static (нам не нужно выбирать, в каком файле определяется vtable).

Я в основном предполагаю, что авторам компиляторов легче реализовать это таким образом. Статические переменные вне класса / структуры существуют и, возможно, либо по соображениям согласованности, либо потому, что авторам компиляторов было бы «легче реализовать», что они определили это ограничение в стандартах.


источник
2

Я думаю, что нашел причину. Определение staticпеременной в отдельном пространстве позволяет инициализировать ее любым значением. Если не инициализировано, то по умолчанию будет 0.

До C ++ 11 инициализация в классе не была разрешена в C ++. Поэтому нельзя писать так:

struct X
{
  static int i = 4;
};

Таким образом, теперь для инициализации переменной нужно написать ее вне класса как:

struct X
{
  static int i;
};
int X::i = 4;

Как уже говорилось в других ответах, int X::iв настоящее время глобальный и объявление глобального во многих файлах вызывает ошибку ссылки на несколько символов.

Таким образом, нужно объявить staticпеременную класса внутри отдельной единицы перевода. Тем не менее, все же можно утверждать, что следующий способ должен указывать компилятору не создавать несколько символов

static int X::i = 4;
^^^^^^
iammilind
источник
0

A :: x является просто глобальной переменной, но namespace'd для A, и с ограничениями доступа.

Кто-то еще должен объявить это, как и любую другую глобальную переменную, и это может даже быть сделано в проекте, который статически связан с проектом, содержащим остальную часть кода А.

Я бы назвал все это плохим дизайном, но есть несколько функций, которые вы можете использовать таким образом:

  1. порядок вызова конструктора ... Не важно для int, но для более сложного члена, который может обращаться к другим статическим или глобальным переменным, это может быть критично.

  2. статический инициализатор - вы можете позволить клиенту решить, к чему должен быть инициализирован A :: x.

  3. в c ++ и c, поскольку у вас есть полный доступ к памяти через указатели, физическое расположение переменных является значительным. Есть очень плохие вещи, которые вы можете использовать в зависимости от того, где находится переменная в объекте ссылки.

Я сомневаюсь, что это «почему» возникла такая ситуация. Вероятно, это просто эволюция C, превращающаяся в C ++, и проблема обратной совместимости, которая мешает вам изменить язык сейчас.

Джеймс Подеста
источник
2
это , кажется, не предлагает ничего существенного по точкам сделанных и разъяснено в предыдущих 6 ответов
комар