Может ли стандартно-совместимый компилятор отклонить код, содержащий dynamic_cast downcast из неполиморфного типа?
Этот вопрос вдохновлен комментариями здесь.
Рассмотрим следующий фрагмент кода:
struct X {}; // no virtual members
struct Y : X {}; // may or may not have virtual members, doesn't matter
Y* func(X* x) { return dynamic_cast<Y*>(x); }
Несколько человек предложили, чтобы их компилятор отклонил тело func
.
Однако мне кажется, что независимо от того, определяется ли это стандартом, зависит от времени выполнения x
. Из раздела 5.2.7 ([expr.dynamic.cast]
):
-
Результат выражения dynamic_cast<T>(v)
является результатом преобразуя выражение v
в тип T
. T
должен быть указателем или ссылку на полный тип класса или "указатель на cv void
". Оператор dynamic_cast
не должен отбрасывать константу.
-
Если T
- тип указателя, v
должен быть prvalue указателя на полный класс type, а результатом является значение типа T
. Если T
- значение l ссылочный тип v
должен быть lvalue полного типа класса, а результатом является l-значение типа, обозначаемого T
. Если T
- значение r ссылочный тип v
должен быть выражением, имеющим полный тип класса, и результатом является x значение типа, на которое указывает T
.
-
Если тип v
совпадает с T
, или он совпадает с T
, за исключением того, что тип объекта класса в T
больше CV, чем тип объекта класса в v
результат v
(при необходимости преобразуется).
-
Если значение v
является значением нулевого указателя в случае указателя, результатом является значение нулевого указателя типа T
.
-
Если T является "указателем на cv1 B
" и v
имеет тип 'указатель на cv2 D
", так что B
является базовым классом D
, результатом является указатель на уникальный объект B
объекта D
, на который указывает v
. Аналогично, если T является" ссылкой на cv1 B
" и v
имеет тип cv2 D
такой, что B
является базовым классом D
, результатом является единственный подобъект B
D
объект, на который ссылается v
. Результатом является lvalue, если T
- значение lvalue reference или xvalue, если T
является ссылкой rvalue. Как в указатель и справочные случаи, программа плохо сформирована, если cv2 имеет более высокая cv-квалификация, чем cv1, или если B
является недоступным или неоднозначный базовый класс D
.
-
В противном случае v
должен быть указателем на или значением полиморфного типа.
-
Если T
"указатель на cv void
", то результатом является указатель на наиболее производные объект, на который указывает v
. В противном случае применяется проверка времени выполнения для просмотра если объект, указанный или обозначенный v
, может быть преобразован в тип указана или упомянута T
.) Самый производный объект указал или ссылается на v
, может содержать другие объекты B
в качестве базовых классов, но они игнорируются.
-
Если C
- тип класса, к которому указывает или ссылается T
проверка выполнения выполняется логически следующим образом:
-
Если, в большинстве случаев производный объект, на который указывает (обозначается) на v
, v
указывает (ссылается) на public
подобъект базового класса объекта C
, и если только один объект тип C
выводится из подобъекта, на который указывает (называется), на v
(указывает) на этот объект C
.
-
В противном случае, если v
указывает (ссылается) на подбобку базового класса public
самого производного объекта, и тип самого производного объекта имеет базовый класс, тип C
, это однозначно и public
, точки результата (относится) к C
подобъектом самого производного объекта.
-
В противном случае проверка времени выполнения выходит из строя.
-
Значение неудачного нажатия на тип указателя является нулевым значение указателя требуемого типа результата. Неудачное приведение к ссылке type throws std::bad_cast
.
Как я читал это, требование полиморфного типа применяется только в том случае, если ни одно из вышеперечисленных условий не выполняется, и одно из этих условий зависит от значения времени выполнения.
Конечно, в некоторых случаях компилятор может положительно определить, что вход не может быть должным образом NULL (например, когда это указатель this
), но я все же думаю, что компилятор не может отклонить код, если он не может определить что утверждение будет достигнуто (обычно это вопрос времени выполнения).
Конечно, здесь важна диагностическая диагностика, но совместимо ли это стандартное для компилятора отклонение этого кода с ошибкой?
Ответы
Ответ 1
Очень хорошая точка.
Обратите внимание, что в С++ 03 формулировка 5.2.7/3 и 5.2.7/4 выглядит следующим образом
3 Если тип v совпадает с типом требуемого результата (который для удобство, в этом описании будет называться R), или это то же самое как R, за исключением того, что тип объекта класса в R больше cv-квалифицирован, чем тип объекта класса в v, результат v (при необходимости преобразуется).
4 Если значение v является значением нулевого указателя в случае указателя, результатом является значение нулевого указателя типа R.
Ссылка на тип R
, представленная в 5.2.7/3, по-видимому, подразумевает, что 5.2.7/4 предполагается подпунктом 5.2.7/3. Другими словами, представляется, что 5.2.7/4 предназначено для применения только при условиях, описанных в 5.2.7/3, т.е. Когда типы одинаковы.
Однако формулировка на С++ 11 отличается и больше не включает R
, которая больше не предполагает каких-либо особых отношений между 5.2.7/3 и 5.2.7/4. Интересно, было ли это намеренно изменено...
Ответ 2
Я считаю, что намерение этой формулировки состоит в том, что некоторые приведения могут выполняться во время компиляции, например. upcasts или dynamic_cast<Y*>((X*)0)
, но для других требуется проверка времени выполнения (в этом случае необходим полиморфный тип.)
Если ваш фрагмент кода был хорошо сформирован, ему понадобится проверка времени выполнения, чтобы увидеть, является ли это значением нулевого указателя, что противоречит идее, что проверка времени выполнения должна выполняться только для полиморфного случая.
См. DR 665, в котором уточняется, что определенные роли плохо сформированы во время компиляции, а не отложены до времени выполнения.
Ответ 3
Для меня это кажется довольно четким. Я думаю, что путаница возникает, когда вы делаете неправильную интерпретацию, что перечисление требований - это тип "else if.. else if..".
Точки (1) и (2) просто определяют, какими могут быть статические типы ввода и вывода, с точки зрения cv-квалификации и lvalue-rvalue-prvalue-- и т.д. Таким образом, это тривиально и применяется ко всем случаям.
Точка (3) довольно понятна, если оба типа ввода и вывода одинаковы (добавлены cv-квалификаторы в сторону), тогда преобразование тривиально (нет или просто добавлено cv-квалификаторы).
Точка (4) явно требует, чтобы, если указатель ввода равен нулю, то выходной указатель также имеет значение null. Этот момент должен быть сделан как требование, а не как отказ от принятия или принятия акта (через статический анализ), но в качестве подчеркивания того факта, что если преобразование из указателя ввода в выходной указатель обычно влечет за собой смещение фактическое значение указателя (как это возможно, в иерархиях классов с несколькими наследствами), то это смещение не должно применяться, если указатель ввода имеет значение null, чтобы сохранить "нуль" указателя. Это просто означает, что при выполнении динамического трансляции указатель проверяется на значение nullity, а если оно равно null, результирующий указатель также должен иметь нулевое значение.
Точка (5) просто заявляет, что если она является upcast (от производной до базы), то литье решается статически (эквивалентно static_cast<T>(v)
). Это в основном для обработки дела (как указано в сноске), где upcast хорошо сформирован, но может быть потенциал для плохо сформированного актера, если нужно перейти к самому производному объекту, на который указывает v ( например, если v фактических указывает на производный объект с несколькими базовыми классами, в которых класс T появляется более одного раза). Другими словами, это означает, что если он взлетает, делайте это статически, без механизма времени выполнения (таким образом, избегая потенциального сбоя, когда этого не должно произойти). В этом случае компилятор должен отклонить бросок на той же основе, что и static_cast<T>(v)
.
В точке (6), очевидно, "в противном случае" относится непосредственно к точке (5) (и, конечно же, к тривиальному случаю Точки (3)). Значение (вместе с Point (7)), что если трансляция не является повышением (а не литой-литой (Point (3))), то она является нисходящей, и она должна быть разрешена во время выполнения, с явным требованием, чтобы тип (of v) был полиморфным типом (имеет виртуальную функцию).
Ваш код должен быть отклонен стандартным компилятором. Для меня нет никаких сомнений. Потому что, литой является нисходящий, а тип v не является полиморфным. Он не соответствует требованиям, установленным стандартом. Предложение null-pointer (point (4)) действительно не имеет никакого отношения к тому, является ли он принятым кодом или нет, оно просто связано с сохранением нулевого значения указателя в листинге (в противном случае некоторые реализации могут сделать (глупый), чтобы по-прежнему применять смещение указателя при произнесении, даже если значение равно null).
Конечно, они могли бы сделать другой выбор, и позволили актеру вести себя как статический-отбрасываемый из базы в производный (т.е. без проверки времени выполнения), когда базовый тип не является полиморфным, но я подумайте, что ломает семантику динамического каста, что, очевидно, означает "я хочу проверить время выполнения этого приведения", иначе вы бы не использовали динамический набор!