Прежде чем идти дальше, обратите внимание: Это чисто языковой вопрос юриста . Я хочу получить ответы на основе стандартных цитат. Я не ищу совета по написанию кода C ++. Пожалуйста, ответьте, как если бы я был автором компилятора .
Во время конструирования объекта только с эксклюзивными подобъектами (#), особенно с теми, которые не являются виртуальными базами (также те, у которых виртуальный базовый класс назван только один раз), динамический тип lvalue, ссылающийся на подобъект базового класса, «увеличивается» : он переходит от типа базы к типу класса конструктора.
(#) Подобъект является исключительным , когда он является прямым подобъектом ровно одного другого объекта (который может быть другим подобъектом или законченным объектом). Член и не виртуальная база всегда эксклюзивны.
Во время уничтожения тип уменьшается (до конца тела деструктора этого подобъекта, где подобъект исчезает и больше не имеет динамического типа).
[Во время конструирования объекта с общими подобъектами базового класса (то есть в классе с различными базовыми подобъектами, имеющими хотя бы виртуальную базу), динамический тип базового подобъекта может временно «исчезнуть». Я не хочу обсуждать такие занятия здесь.]
Реальный вопрос: Что происходит, если динамический тип объекта увеличивается в другом потоке?
Заголовок вопроса, который является стандартным C ++ вопросом , выражается с использованием нестандартного термина (vptr), что может показаться противоречивым. Причины:
- Нет требования, чтобы полиморфизм был реализован в терминах vptr, но это (почти?) Всегда так. Один (или несколько) vptr в объекте представляет динамический тип полиморфного объекта.
- Гонки данных определяются в терминах операций чтения / записи в ячейку памяти.
- Стандартный текст часто использует нестандартные элементы «только для экспозиции», чтобы определить стандартные функции. (Так почему бы не использовать vptr «только для экспозиции»?)
Стандарт не определяет поведение полиморфных объектов (*) напрямую как функцию их динамического типа; стандарт определяет, какие выражения разрешены в течение так называемого «времени жизни» (после завершения конструктора), внутри тела конструктора самого производного типа (точно такие же выражения допускаются с той же семантикой), также внутри конструкторы подобъектов базового класса ...
(*) Динамическое поведение полиморфных или динамических объектов (**) включает в себя: виртуальные вызовы, производные от базовых преобразований, приведения вниз (static_cast
или dynamic_cast
), typeid
полиморфного объекта.
(**) Динамический объект - это такой объект, в котором его класс использует ключевое слово virtual; по этой причине его конструктор не тривиален.
Итак, в описании говорится: После что-то закончилось, как только что-то началось, до что-то еще и т. Д. Какое-то выражение допустимо и делает то-то и то-то.
Спецификация конструкции и уничтожения была написана до того, как потоки стали частью стандарта C ++. Так что же изменилось со стандартизацией потоков? Есть одно предложение с определением поведения потоков (нормативная часть) [basic.life] / 11 :
В этом подпункте «до» и «после» относятся к «случается до»
отношение ([intro.multithread]).
Таким образом, ясно, что объект рассматривается как полностью построенный объекта и вызов деструктора (если он вызывается вообще).
Но этоне говорится о том, что происходит во время конструирования производных классов, после того, как подобъект базового класса был создан: очевидно, существует условие гонки, если какое-либо динамическое свойство используется для полиморфного строящегося объекта, но условия гонки не являются недопустимыми .
[Условие гонки - это случай недетерминированности, и любое значимое использование мьютекса, переменной условия, rwlocks, многократного использования семафоров, многократного использования других устройств синхронизации и любого использования атомарных примитивов вводит условие гонки. по крайней мере, на уровне порядка модификации на атомном объекте. Результатом того, что недетерминированность низкого уровня приводит к непредсказуемому высокоуровневому поведению, зависит способ использования примитивов.]
Затем стандартный черновик говорит:
[Примечание: следовательно, неопределенное поведение приводит к тому, что объект, который является
на создание в одном потоке ссылаются из другого потока
без адекватной синхронизации. - конец примечания]
Где определяется «адекватная синхронизация»?
Является ли отсутствие «адекватной синхронизации» моральным эквивалентом обычной гонки данных: гонки данных на vptr или, по общему мнению, гонки данных на динамическом типе?
Для простоты я хотел бы ограничить сферу вопроса одним наследованием, по крайней мере, в качестве первого шага. (В любом случае стандарт ужасно смущает конструирование объектов с множественным наследованием.)
Это вопрос языкового адвоката , поэтому я не заинтересован в:
- целесообразно ли использование объекта, который находится в процессе создания в другом потоке (вероятно, не желательно);
- как использовать синхронизацию, чтобы надежно исправить это состояние гонки;
- хотят ли производители компиляторов поддерживать такой вариант использования (вероятно, они не и не будут);
- может ли это работать надежно в любой реальной реализации (вероятно, не надежно работает в нетривиальных случаях с текущей реализацией).
РЕДАКТИРОВАТЬ: Предыдущий пример, вместо иллюстрации проблемы, отвлекал. Это вызвало очень интересную, но совершенно неуместную дискуссию в разделе чата.
Вот более чистый пример, который не вызовет той же проблемы:
atomic<Base1*> shared;
struct Base1 {
virtual void f() {}
};
struct Base2 : Base1 {
virtual void f() {}
Base2 () { shared = (Base1*)this; }
};
struct Der2 : Base2 {
virtual void f() {}
};
void use_shared() {
Base1 *p;
while (! (p = shared.get()));
p->f();
}
С логикой потребителя / производителя:
- Тема A:
new Der2;
- Резьба B:
use_shared();
Для справки, оригинальный пример:
atomic<Base*> shared;
struct Base {
virtual void f() {}
Base () { shared = this; }
};
struct Der : Base {
virtual void f() {}
};
void use_shared() {
Base *p;
while (! (p = shared.get()));
p->f();
}
Логика потребителя / производителя:
- Тема A:
new Der;
- Резьба B:
use_shared();
Неясно, что this
может использоваться другим потоком во время выполнения конструктора Base
, что является интересной проблемой, но не имеет отношения к проблеме использования подобъекта базового класса, в то время как производный конструктор работает в другом нить.
Дополнительная информация
Для справки, DR, который "мотивировал" текущую формулировку (хотя это ничего не объясняет):
Базовый отчет о дефектах языка # 710