Почему ODR не нарушается, если взять адрес встроенной статической константной целочисленной переменной-члена?

Нечто подобное было бы очевидным нарушением правила одного определения С++, если бы оно скомпилировалось:

// Case 1
// something.h

struct S {};

struct A
{
   static const S val = S();
};

потому что, если что-то.h будет включено более чем в один модуль, определение A::val будет повторяться. Однако это разрешено:

// Case 2    
// someOtherThing.h

struct B
{
   static const int val = 3;
};

Насколько я понимаю, причина, по которой этот случай в порядке, заключается в том, что B::val является константой времени компиляции целочисленного типа, поэтому компилятор, по сути, может выполнять поиск и замену всех ссылок на B::val на литерал 3 (и проверку разборка показывает, что это именно то, что он делает). Следовательно, в конечном продукте в некотором смысле ноль определений B::val, поэтому ODR не применяется. Однако учтите это:

// Case 3
// yetAnotherThing.h

struct C
{
   static const int val = 3;

   const int* F()
   {
      return &val;
   }
};

Это разрешено, и дизассемблирование показывает, что в этом случае некоторая ячейка памяти фактически была выделена для хранения значения C::val. На первый взгляд, это означает, что теперь мы нарушаем ODR, если ещеAnotherThing.h включен в несколько модулей, так как static const int val = 3 теперь вызывает «испускание» хранилища. При этом ни компилятор, ни компоновщик (VC++2012) не жалуются.

Почему? Является ли это просто неприятным частным случаем, с которым приходится иметь дело авторам компиляторов/линкеров? И если да, то почему ту же систему нельзя использовать и для случая № 1?

(Соответствующие цитаты из стандарта приветствуются, но они не обязательно отвечают на вопрос. Если бы в стандарте говорилось, что любое использование ключевого слова pink_elephants должно приводить к замене каждого экземпляра числа 42 на 666, то это было бы так, но мы бы все еще задавались вопросом, почему существует такое странное правило.)


person dlf    schedule 27.05.2014    source источник
comment
Какой у Вас вопрос? Почему разумные и удобные вещи разрешены стандартом? Потому что они разумны и удобны.   -  person n. 1.8e9-where's-my-share m.    schedule 27.05.2014
comment
@н.м. Я бы поставил под сомнение разумность # 3, учитывая, что, по-видимому, компилятору нужно пройти через кучу обручей, чтобы заставить его работать в обмен на (IMO) очень небольшое удобство. Но если это так, то № 1, безусловно, тоже разумен и удобен, но запрещен.   -  person dlf    schedule 27.05.2014
comment
Это не работает. Если вы используете не встроенную функцию F (поэтому все это не удаляется), вы заметите (с помощью gcc), что символ _ZN1C3valE не определен.   -  person Marc Glisse    schedule 27.05.2014
comment
Компилятору уже приходится выполнять эту работу для статических членов классов шаблонов, поэтому удаление этой возможности не избавляет автора компилятора от работы. Разница между этим и случаем № 1 заключается в том, что в случае № 1 компилятор не может доказать, что несколько определений val идентичны.   -  person Raymond Chen    schedule 27.05.2014
comment
@MarcGlisse Вышеприведенный пример кода. У меня есть реальный проект, который делает что-то вроде # 3 и имеет несколько вызовов F в нескольких файлах, и все строится и работает нормально с VS2012. Может быть, это еще одно расширение, специфичное для Microsoft?   -  person dlf    schedule 27.05.2014
comment
@RaymondChen Я согласен, но как насчет (например) static const double val = 3.0? Это также запрещено, так как double не является целочисленным, но в этом случае компилятор может сделать вывод, что все определения идентичны, верно?   -  person dlf    schedule 27.05.2014
comment
Я неправильно понял ваш вопрос. Однако здесь нет ничего запрещенного. Член по-прежнему должен быть определен в области пространства имен, если он используется odr (9.4.2/3). Так что, если вы этого не сделали, компилятор Microsoft позволит вам это сделать.   -  person n. 1.8e9-where's-my-share m.    schedule 27.05.2014
comment
@н.м. Если он не определен, это неопределенное поведение. Так что нет ничего плохого в том, что компилятор позволил ему это сойти с рук.   -  person James Kanze    schedule 27.05.2014
comment
Как компилятор узнает, что какой-то другой файл не был скомпилирован с другим заголовочным файлом, который говорит static const double val = 3.14;? Это неизвестно до момента компоновки, после чего решение компилятора о том, выдавать ли COMDAT, уже давно не принимается.   -  person Raymond Chen    schedule 27.05.2014


Ответы (1)


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

S const A::val;

в исходном файле (не в заголовке).

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

Я думаю, что С++ 11 несколько расширил это и позволяет некоторым неинтегральным типам также иметь инициализатор в определении класса. Но он по-прежнему требует определения вне класса.

Что касается вашего последнего примера, который, как вы утверждаете, работает: он не является допустимым в версиях до C++11 и вызывает ошибки во многих компиляторах. (У меня сложилось впечатление, что С++ 11 сделал это допустимым и оставил компилятору генерировать экземпляр, но я не могу найти подходящие слова навскидку. Если это было разрешено в С++ 11 , то это просто функция C++11, реализованная в VC++2012.)

person James Kanze    schedule 27.05.2014
comment
Ой; вы правы в случае №1; Я отредактировал его, чтобы он содержал задание, поскольку это то, что я имел в виду. - person dlf; 27.05.2014
comment
@dlf c++11 допускает случай 1, если вы замените const на constexpr. Законно это или нет, это не является нарушением ODR, поскольку здесь нет определения, просто объявление, даже с инициализатором. - person n. 1.8e9-where's-my-share m.; 27.05.2014
comment
@н.м. У меня было (ошибочное) понимание, что случаи два и три связаны с поиском определения члена внутри определения класса. На самом деле, как вы и Джеймс Канце ясно дали понять, все, что происходит, это то, что инициализация находится с тем, что все еще является просто объявлением. Это осознание и стандартный раздел, который вы процитировали ниже вопроса, проясняют для меня ситуацию. - person dlf; 27.05.2014
comment
@JamesKanze: Комментарий в скобках в вашем последнем абзаце кажется неверным на основании 9.4.2/3 черновика C++11, как цитирует n.m. ниже вопроса. Разве это не изменилось после драфта? - person dlf; 27.05.2014
comment
@dlf Ни в одном из ваших примеров нет присваивания; нет контекста, в котором оператор присваивания был бы допустимым. Все они просто указывают инициализацию статического члена данных. - person James Kanze; 27.05.2014
comment
@dlf Я тоже туда смотрел. Я только что вспомнил дискуссию в комитете по стандартам о разрешении отсутствия определения. Судя по всему, он не попал в окончательный вариант (если только кто-то не укажет на другое место в стандарте, в котором говорится, что это произошло). Таким образом, отсутствие определения остается неопределенным поведением. - person James Kanze; 27.05.2014
comment
@JamesKanze Однако даже в этом случае вам формально требовалось определение (без инициализатора) в одном и только в одном исходном файле. Если я помещу static const int val = 3; в определение MyClass в заголовке, а также поставлю const int MyClass::val; в cpp, я получу ошибку компоновщика (MyClass::val уже определен в [каком-то другом cpp, который включает заголовок]). Что тут происходит? Microsoft явно нарушает стандарт? - person dlf; 28.05.2014
comment
Очевидно, что отсутствие внеклассового определения является специфичным для Microsoft расширением: msdn .microsoft.com/en-us/library/34h23df8.aspx. Чего они не говорят вам, так это того, что если вы включите эту функцию, вы НЕ ДОЛЖНЫ включать определение вне класса, иначе вы получите ошибку компоновщика. - person dlf; 28.05.2014
comment
@dlf Интересно. В этом случае у нас есть ряд статических данных; Я только что проверил, и мы не определяем ни одного из них, поэтому у нас не было проблем с Microsoft. Ни с g++, потому что, как это бывает, все они используются в контекстах, где компилятор может просто подставить фактическое значение и не нуждается в объекте. - person James Kanze; 28.05.2014
comment
Давайте продолжим обсуждение в чате. - person dlf; 28.05.2014