Законно ли иметь статически выделенные ссылки чистого виртуального родительского класса на статически выделенные объекты дочернего класса?

ОБНОВЛЕНИЕ: я не прошу людей попробовать это и посмотреть, работает ли для них код. Я спрашиваю, является ли шаблон кода допустимым для C ++, независимо от того, работает ли он для вас.

Я исследую то, что я считаю ошибкой в ​​компиляторе IAR C ++ для процессоров Renesas RX. Примеры следующего шаблона кода иногда работают в моей небольшой встроенной системе, в других случаях он дает сбой во время инициализации parentRefToChildInstance при переходе на адрес 0x00000000 (или поблизости; я также видел переход на 0x00000038). Для данной версии исходного кода поведение кажется согласованным между компиляциями, но если код нарушается кажущимися несущественными способами, иногда поведение переключается.

Законно ли иметь ссылки чистого виртуального родительского класса на статически выделенные объекты дочернего класса, или это незаконно, потому что порядок инициализации статически выделенных объектов не может быть гарантирован?

char aGlobalVar = 0;

struct parent
{
   virtual ~parent() {}
   virtual void method1() = 0;
};

struct child : public parent
{
   child(int someValue) : m_someData(someValue) {}
   virtual ~child() {}
   virtual void method1() { ++aGlobalVar; }
   int m_someData;
};

child childInstance(0x1234abcd);
parent &parentRefToChildInstance = childInstance;

В случаях, когда происходит сбой, объект дочернего класса не был создан в момент инициализации ссылки на родительский класс; Я подозреваю, что компилятор каким-то образом использует указатель vtable дочернего объекта для инициализации ссылки на родительский класс, хотя я не подтвердил это наверняка. Но я думал, что компилятор должен иметь возможность инициализировать ссылку, зная только тип объекта, на который он ссылается, и его адрес, которые должны быть известны во время компиляции и времени компоновки соответственно. Если это правда, то, похоже, не имеет значения, какой порядок childInstance и parentRefToChildInstance инициализируется.

Кроме того, мы по-прежнему ограничены C ++ 03, если это имеет значение.

Вот main() в дополнение к приведенному выше коду ...

int main()
{
   printf("aGlobalVar = %u\n", aGlobalVar);
   childInstance.method1();
   printf("aGlobalVar = %u\n", aGlobalVar);
   parentRefToChildInstance.method1();
   printf("aGlobalVar = %u\n", aGlobalVar);
}

Обычно я ожидал, что он напечатает это, а не выйдет из строя во время инициализации статического объекта (даже до запуска main()):

aGlobalVar = 0
aGlobalVar = 1
aGlobalVar = 2

person phonetagger    schedule 23.08.2018    source источник
comment
Можете ли вы предоставить небольшую основную реализацию, которая воспроизводит?   -  person Kenny Ostrom    schedule 24.08.2018
comment
Мог ли ребенок быть уничтожен, оставив болтающуюся ссылку?   -  person Kenny Ostrom    schedule 24.08.2018
comment
Находятся ли две статические переменные в одном файле? Я вспоминаю, как много лет назад были некоторые недетерминированные проблемы со статической инициализацией, если статика находилась в отдельных библиотеках (.so). Может здесь что-то похожее.   -  person Matthew Fisher    schedule 24.08.2018
comment
В шаблоне кода, который использовали другие сотрудники моей компании, childInstance является статическим объектом файла, а parentRefToChildInstance определяется как статический член другого класса, объявленного в файле заголовка и определенного в том же файле, что и childInstance. И я знаю, о чем вы говорите, вы не можете зависеть от порядка инициализации статического объекта. Но в этом случае, я думаю, должно быть законным инициализировать ссылку для ссылки на объект, который еще не был создан. Поэтому я не думаю, что порядок инициализации должен иметь значение.   -  person phonetagger    schedule 24.08.2018
comment
@KennyOstrom Поскольку childInstance выделяется статически (некоторые могут назвать его глобальным объектом), он не будет уничтожен до выхода из программы. А сбой происходит еще до запуска main(), поэтому висячие ссылки не могут быть проблемой.   -  person phonetagger    schedule 24.08.2018
comment
Он должен работать постоянно. Работает на моем archlinux gcc8.1.1 -std = c ++ 03 каждый раз. Может быть, не связанный с ним хайзенбаг вроде переполнения буфера.   -  person KamilCuk    schedule 24.08.2018
comment
@KamilCuk Нет, не переполнение буфера. Я могу это гарантировать. Если я перейду через код инициализации, прямо перед сбоем, отладчик сообщит мне, что он находится в строке, где инициализируется parentRefToChildInstance. Если я посмотрю на сборку и регистры процессора, он попытается перейти к 0x00000000 в точке, содержащей всего несколько инструкций в его инициализации.   -  person phonetagger    schedule 24.08.2018
comment
Можете ли вы опубликовать сгенерированную сборку компилятором, который вызывает сбой? Какой компилятор вы используете? Какие переключатели?   -  person KamilCuk    schedule 24.08.2018
comment
@KamilCuk Мы используем IAR для процессора Renesas RX, версия компилятора 3.10. Та же проблема возникает и с версией 4.10, но мы все равно не можем переключиться из-за других проблем.   -  person phonetagger    schedule 24.08.2018
comment
Код в вашем вопросе, несомненно, является законным. Но он отличается от кода, который вы используете, возможно, во многих важных аспектах. Перемещение вещей в разные файлы имеет значение.   -  person Ben Voigt    schedule 24.08.2018
comment
Мой хрустальный шар говорит, что доступ к parentRefToChildInstance осуществляется во время статической инициализации в другом модуле компиляции, и этот модуль компиляции выполняет код до того, как childInstance или parentRefToChildInstance прошли динамическую инициализацию.   -  person Ben Voigt    schedule 24.08.2018
comment
Было бы интересно увидеть сборку / разборку источника, содержащего определение parentRefToChildInstance.   -  person aschepler    schedule 24.08.2018
comment
@aschepler Поскольку это делается во время инициализации статического объекта, кажется, что это не имеет большого интуитивного смысла. Нет исходного кода, с которым можно было бы сравнить ассемблерный код, по крайней мере, не так легко.   -  person phonetagger    schedule 24.08.2018
comment
@BenVoigt Ваш хрустальный шар дает очень хорошие советы, но я не думаю, что это так. Если я устанавливаю точку останова в строке, где определено parentRrefToChildInstance, отладчик прерывает (во время инициализации статического объекта перед запуском main()) всего несколько инструкций до сбоя.   -  person phonetagger    schedule 24.08.2018
comment
@phonetagger: я никогда не видел отладчика, который может поставить точку останова на определение статической переменной ... Я не думаю, что вы можете с уверенностью предположить, что вы действительно находитесь в точке, где ссылка должна быть инициализирована, в отличие от некоторого доступа из этого.   -  person Ben Voigt    schedule 24.08.2018
comment
Рекомендация: измените parentRefToChildInstance со статического члена данных на функцию, возвращающую ссылку. Тогда не будет его инициализации, которая может произойти в неподходящее время (хотя ее можно использовать до инициализации childInstance). Вы можете исправить это, переместив childInstance в функцию как статический локальный объект.   -  person Ben Voigt    schedule 24.08.2018
comment
@BenVoigt Забавно, ты должен это предложить. Мы сделали это, и это действительно решает проблему, по-видимому, поскольку инициализация ссылки происходит после запуска конструктора дочернего объекта. Я мог быть совершенно неправ в отношении указателя vtable, но, тем не менее, переход на функцию, возвращающую ссылку, устраняет сбой. Но то, что он вообще вылетает, настораживает. Мы также наблюдаем другие странные проблемы со статической инициализацией ссылок и указателей. Иногда статически назначенные указатели, которые явно должны указывать на что-то, статически инициализируются значением NULL.   -  person phonetagger    schedule 24.08.2018
comment
@phonetagger: Конечно, указатели статически инициализируются значением NULL. Инициализация с реальным адресом объекта происходит во время фазы динамической инициализации. См. stackoverflow.com/q/35721031/103167   -  person Ben Voigt    schedule 24.08.2018
comment
@phonetagger: Я гарантирую, что ваша проблема вызвана использованием этих переменных на этапе динамической инициализации. Инициализация ссылки не завершается ошибкой, просто она еще не была сделана к тому времени, когда что-то еще использует ссылку. Итак, у вас неопределенное поведение. То, что показывает отладчик во время неопределенного поведения, к сожалению, не заслуживает доверия.   -  person Ben Voigt    schedule 24.08.2018
comment
@BenVoigt Вы все используете какой-то сайт для публикации кода за пределами SO, где я могу показать вам демонстрацию другого кода, который не работает? Тогда ты мне поверишь.   -  person phonetagger    schedule 24.08.2018
comment
@phonetagger: Я тебе верю. Я думаю, вы точно описали то, что произошло в отладчике, я просто думаю, что вы сделали из этого неправильный вывод.   -  person Ben Voigt    schedule 24.08.2018
comment
@BenVoigt См .: rextester.com/FRUJK67421 Это показывает другую ошибку, тот же компилятор. В моей системе RX, если я прохожу через результирующую сборку функции, после проверки внутреннего bool, отслеживающего, были ли инициализированы статические переменные, код сначала инициализирует указатель на правильный адрес. Затем блок ассемблерного кода копирует другие статически инициализированные данные из ПЗУ в свое место в ОЗУ, перезаписывая уже инициализированный указатель значением ПУСТО (NULL). Теперь это не ошибка пользователя.   -  person phonetagger    schedule 24.08.2018
comment
@phonetagger: Что ж, прискорбно. Мне это кажется несоответствием между компилятором и компоновщиком, оба думают, что это работа другого. То есть, поскольку компоновщик отвечает за размещение статических объектов в памяти, компилятор ожидает, что компоновщик запишет выбранный им адрес в блок ПЗУ, поэтому копия блока будет работать. И компоновщик записывает NULL в блок ПЗУ, думая, что компилятор сгенерирует код для его перезаписи.   -  person Ben Voigt    schedule 24.08.2018
comment
@BenVoigt Раздел данных нашего приложения можно перемещать (мы одновременно запускаем 4 независимых приложения на голом железе с переключателем контекста), поэтому все указатели на данные должны быть инициализированы во время выполнения. На настольных компьютерах / серверах это будет делать загрузчик приложений, который заменяет relocs (также известные как «исправления») в исполняемом образе адресами данных. На «голом железе» нет загрузчика, компилятор выдает код для исправления тех дыр, которые в файлах .elf или .exe рабочего стола назывались бы relocs. В моем случае это было так, но сразу после этого было остановлено из-за ошибки инициализации копирования блока неправильного порядка.   -  person phonetagger    schedule 24.08.2018


Ответы (1)


Показанный код является допустимым.

Верно, что порядок инициализации объектов и ссылок, определенных в области пространства имен или как static членов класса, непредсказуем, когда определения находятся в разных единицах перевода, и это часто может приводить к неприятным проблемам.

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

В параграфе 7 C ++ 17 [basic.life] говорится:

До начала жизненного цикла объекта, но после того, как хранилище, которое будет занимать объект, будет выделено ..., любое значение glvalue, которое относится к исходному объекту, может быть использовано, но только ограниченными способами. Информацию о строящемся или разрушающемся объекте см. В [class.cdtor]. В противном случае такое glvalue относится к выделенному хранилищу, и использование свойств glvalue, которые не зависят от его значения, четко определено. Программа имеет неопределенное поведение, если:

  • glvalue используется для доступа к объекту, или

  • glvalue используется для вызова нестатической функции-члена объекта, или

  • glvalue привязан к ссылке на виртуальный базовый класс, или

  • glvalue используется как операнд dynamic_cast или как операнд typeid.

Ни одна из этих четырех вещей не происходит во время инициализации parentRefToChildInstance, в частности потому, что parent не является виртуальным базовым классом child. Таким образом, код подпадает под случай, упомянутый в цитируемом требовании, как четко определенный.

person aschepler    schedule 24.08.2018