До C++11 «правило одного определения» нарушалось при инициализации членов класса нестатических и неконстантных переменных. Почему?

Таким образом, до C++11 инициализация членов класса неконстантных и нестатических переменных не допускалась.

Аргументация Бьярна Страуструпа заключалась в том, что правило одного определения «было бы нарушено, если бы C++ позволял определять внутри класса сущности, которые необходимо хранить в памяти как объекты».

Что он имел в виду?

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

Мой вопрос связан с этим вопросом, но я задаю другой вопрос, специально для разъяснения того, как могло быть нарушено правило одного определения.

Я смотрел доклад Боба Стигалла «Назад к основам» — «Структура программы». И рассмотрел некоторые другие связанные вопросы. Но я до сих пор не могу понять утверждения Страуструпа.

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

Alan Birtles 20.05.2024 08:57

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

Swift - Friday Pie 20.05.2024 08:57

@AlanBirtles - я все еще не понимаю. Моя путаница заключается в том, чем определение члена класса int x; отличается от int x = 5;. Это оба определения, верно? То есть это не декларации. Или... первое — декларация, а второе — определение? В литературе, которую я читаю, указывается, что объявление класса становится определением, если за ним следуют фигурные скобки. Итак... разве все, что находится внутри этих фигурных скобок, не является частью определения? (Я отсылаю вас к Приложению A к книге «Шаблоны C++: полное руководство», где есть очень подробное описание ODR)

centauri 20.05.2024 10:48

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

user12002570 20.05.2024 11:10
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
4
154
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

В случае встроенной инициализации неконстантной нестатической переменной, как предложено:

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.

Jan Schultke 20.05.2024 11:17

@JanSchultke В современных компиляторах это правильно. Все написано в контексте оригинального C++98 и более ранних версий. Я смутно помню, что у него была проблема, если только они не определили, что строковый литерал является const char* и, возможно, статическим, «введенным» значением.

Swift - Friday Pie 20.05.2024 11:32

@Swift-FridayPie спасибо за разъяснение, что int x = 42 меняет определение class A, а не x. Это невероятно полезно для моего понимания. Однако я не понимаю остальную часть вашего ответа. и я думаю, это потому, что я вообще не задал правильный вопрос. Мне придется изучить ваши слова более глубоко, прежде чем принять этот ответ.

centauri 20.05.2024 15:27
Ответ принят как подходящий

Насколько я читал, Страуструп говорил только об инициализаторах внутри класса для статических членов данных. (И правила там фактически не были сильно смягчены до 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 статические элементы данных также были неявно встроены.

Другие вопросы по теме

Что произойдет, если я вызову allocate_at_least(0) согласно стандарту C++23?
Почему изменяемая лямбда преобразуется в указатель на функцию вместо вызова оператора()?
Могут ли `::` и `*`, образующие тип указателя-члена, происходить из разных расширений макроса или они должны появляться как один токен?
Определено ли вычисление смещений указателей байтов между элементами композиции, не являющимися массивами?
Как проверить, является ли конструктор явно дефолтным
Требуется ли создание экземпляра для неиспользуемого, но инициализированного статического элемента данных const int шаблона класса?
Когда типы замыканий наконец стали структурными типами?
Почему существует разница между оператором и соответствующей функцией-членом?
Каковы фактические правила для ожидания Final_suspend в сопрограммах C++?
Может ли семантика единицы перевода зависеть от объявления функции, которая никогда не используется?