Таким образом, до C++11 инициализация членов класса неконстантных и нестатических переменных не допускалась.
Аргументация Бьярна Страуструпа заключалась в том, что правило одного определения «было бы нарушено, если бы C++ позволял определять внутри класса сущности, которые необходимо хранить в памяти как объекты».
Что он имел в виду?
Я понимаю, что правило одного определения требует, чтобы для каждой программы было разрешено только одно определение объекта, существующего в памяти. Но я до сих пор не понимаю, почему это правило может быть нарушено.
Мой вопрос связан с этим вопросом, но я задаю другой вопрос, специально для разъяснения того, как могло быть нарушено правило одного определения.
Я смотрел доклад Боба Стигалла «Назад к основам» — «Структура программы». И рассмотрел некоторые другие связанные вопросы. Но я до сих пор не могу понять утверждения Страуструпа.
контекстуальная инициализация может происходить по-разному, что приведет к созданию разных конструкторов в каждом модуле. Обратите внимание, что класс может быть вложенным типом шаблона, а инициализация будет зависимым контекстом на месте создания экземпляра.
@AlanBirtles - я все еще не понимаю. Моя путаница заключается в том, чем определение члена класса int x;
отличается от int x = 5;
. Это оба определения, верно? То есть это не декларации. Или... первое — декларация, а второе — определение? В литературе, которую я читаю, указывается, что объявление класса становится определением, если за ним следуют фигурные скобки. Итак... разве все, что находится внутри этих фигурных скобок, не является частью определения? (Я отсылаю вас к Приложению A к книге «Шаблоны C++: полное руководство», где есть очень подробное описание ODR)
Похоже, это один из тех вопросов, на который, вероятно, можно полностью ответить только стандартному члену комитета или самому Бьярну.
В случае встроенной инициализации неконстантной нестатической переменной, как предложено:
struct A {
int x = 42;
};
int x = 42;
изменяет определение class A
, а не переменной-члена x
. Это эквивалент добавления инициализации к каждому конструктору A
:
struct A {
int x;
A() : x(42) {}
// ...and any other constructor of A() gets x(42)
};
Это простая часть, если инициализатор — это просто целочисленный литерал. Что, если у нас есть инициализатор, зависящий или что-то определенное в модуле? Даже у строкового буквального ночи есть проблема, потому что он где-то материализован, мы могли бы, по крайней мере, получить несколько экземпляров одной и той же строки.
Не должно быть никаких иллюзий, это ВСЕ ЕЩЕ может нарушить ODR, если вы так напишете, но технически вы можете ввести ODR в любое определение класса. Мы просто полагаемся на компилятор и программист, чтобы этого не сделать.
В основном нам не хватало конструкторов и нового определения тривиальности.
Я не думаю, что инициализация строковым литералом может нарушить ODR. Это будут одни и те же токены с одинаковым значением в каждом TU.
@JanSchultke В современных компиляторах это правильно. Все написано в контексте оригинального C++98 и более ранних версий. Я смутно помню, что у него была проблема, если только они не определили, что строковый литерал является const char*
и, возможно, статическим, «введенным» значением.
@Swift-FridayPie спасибо за разъяснение, что int x = 42
меняет определение class A
, а не x
. Это невероятно полезно для моего понимания. Однако я не понимаю остальную часть вашего ответа. и я думаю, это потому, что я вообще не задал правильный вопрос. Мне придется изучить ваши слова более глубоко, прежде чем принять этот ответ.
Насколько я читал, Страуструп говорил только об инициализаторах внутри класса для статических членов данных. (И правила там фактически не были сильно смягчены до C++17.) Я не знаю, чтобы он назвал какую-либо причину, по которой в C++03 не было инициализаторов элементов по умолчанию для нестатических элементов данных.
Обычно определения классов помещаются в заголовки, поскольку любая единица перевода, которая хочет вызвать метод класса, должна иметь возможность видеть определение класса (а не просто предварительное объявление). Но для любого класса, скажем C
, и любого статического элемента данных, скажем x
, во всей программе должна быть только одна копия C::x
. Чтобы это было так, нужно было сказать, что объявление x
в классе не является определением x
(фактически не выделяет статическое хранилище для x
). Если бы это было определение, вы бы получили несколько определений (нарушение ODR) из-за нескольких единиц перевода, включая заголовок.
Даже если у вас есть что-то вроде struct C { static const int x = 3; }
, это всё равно не определение C::x
. Если вы хотите дать определение x
, вам необходимо выбрать одну единицу перевода, в которую нужно поместить const int C::x;
. Однако в частном случае этот конкретный тип инициализатора внутри класса (т. е. константное выражение, используемое для инициализации статического элемента данных целочисленного или перечисляемого типа) полезен, даже если нет определения, поскольку компилятор может видеть, что его значение всегда равно 3, поэтому везде, где вы используете const
в программе, компилятор может просто заменить его на 3. (Но если вы возьмете адрес C::x
или передадите его в параметр типа C::x
, или что-то в этом роде, требующее адрес, то где-то в программе у вас должно быть определение.) Если статический член данных не равен const int&
или его инициализатор не является константным выражением, тогда использование инициализатора внутри класса не будет иметь никакого смысла, поскольку объявление внутри класса ( что, опять же, не является определением) фактически не выделяет памяти для инициализации. Поэтому эта конструкция не была разрешена.
В C++11 появилось ключевое слово const
, которое позволило иметь вызовы функций (только для функций constexpr
) в инициализаторах статических элементов данных внутри класса, а также иметь внутриклассовые инициализаторы статических элементов данных для членов constexpr
более широкого диапазона. типы. В целях обратной совместимости было сохранено специальное правило, согласно которому можно было использовать инициализатор внутри класса для статического члена данных const
целочисленного и перечислимого типов (без их объявления const
), но оно не распространялось на другие типы.
Чтобы иметь возможность иметь произвольные инициализаторы статических элементов данных внутри класса, вам нужно каким-то образом сказать, что каждое такое объявление внутри класса является определением, но каждое из этих определений фактически определяет один и тот же объект без нарушения ODR. Эта функция, называемая встроенными переменными, была добавлена в C++17 (хотя компиляторы поддерживали ее форму даже в C++03: локальную статику, объявленную во встроенной функции, или экземпляр статического члена данных шаблона класса). , имеет встроенную семантику). В C++17 вы можете явно объявить статический член данных как constexpr
, что позволяет вам предоставить инициализатор внутри класса. inline
статические элементы данных также были неявно встроены.
Поскольку каждая единица перевода, включающая класс, будет иметь собственное определение переменной.