Странное использование кода `?:` В `typeid`
В одном из проектов, над которыми я работаю, я вижу этот код
struct Base {
virtual ~Base() { }
};
struct ClassX {
bool isHoldingDerivedObj() const {
return typeid(1 ? *m_basePtr : *m_basePtr) == typeid(Derived);
}
Base *m_basePtr;
};
Я никогда не видел typeid
. Почему он делает этот странный танец с ?:
вместо того, чтобы просто делать typeid(*m_basePtr)
? Может ли быть какая-то причина? Base
- это полиморфный класс (с виртуальным деструктором).
EDIT: В другом месте этого кода я вижу это, и он выглядит эквивалентно "лишним"
template<typename T> T &nonnull(T &t) { return t; }
struct ClassY {
bool isHoldingDerivedObj() const {
return typeid(nonnull(*m_basePtr)) == typeid(Derived);
}
Base *m_basePtr;
};
Ответы
Ответ 1
Я думаю, что это оптимизация! Немного известная и редко (вы могли бы сказать "никогда" ) функция typeid
заключалась в том, что нулевое разыменование аргумента typeid
генерирует исключение вместо обычного UB.
Что? Ты серьезно? Вы пьяны?
Действительно. Да. Нет.
int *p = 0;
*p; // UB
typeid (*p); // throws
Да, это уродливо, даже по стандарту С++ для уродства языка.
OTOH, это не работает нигде внутри аргумента typeid
, поэтому добавление любого беспорядка отменяет эту "функцию":
int *p = 0;
typeid(1 ? *p : *p); // UB
typeid(identity(*p)); // UB
Для записи: я не утверждаю в этом сообщении, что автоматическая проверка компилятором того, что указатель не имеет значения null перед выполнением разыменования, является безумной вещью. Я только говорю, что делать эту проверку, когда разыменование является непосредственным аргументом typeid
, и не в другом месте, абсолютно сумасшедшим. (Возможно, это была шутка, вставленная в какой-то черновик, и никогда не удалялась.)
Для записи: я не утверждаю в предыдущем "Записи", что для компилятора имеет смысл вставлять автоматические проверки, что указатель не является нулевым, и бросать исключение (как на Java), когда null разыменовывается: в общем случае бросание исключения на нулевое разыменование является абсурдным. Это ошибка программирования, поэтому исключение не поможет. Вызывается ошибка утверждения.
Ответ 2
Единственный эффект, который я вижу, заключается в том, что 1 ? X : X
дает вам X
как rvalue вместо plain X
, который будет lvalue. Это может иметь значение для typeid()
для таких вещей, как массивы (разлагающиеся на указатели), но я не думаю, что было бы важно, если Derived
известен как класс. Может быть, он был скопирован откуда-то, где значение имеет значение? Это поддержало бы комментарий о "программировании культовых грузов"
Что касается комментария ниже, я сделал тест и, конечно, typeid(array) == typeid(1 ? array : array)
, поэтому в некотором смысле я ошибаюсь, но мое недоразумение может по-прежнему соответствовать недоразумениям, которые приводят к исходному коду!
Ответ 3
Это поведение распространяется на [expr.typeid]/2 (N3936):
Когда typeid
применяется к выражению glvalue, тип которого является типом полиморфного класса, результат относится к объекту std::type_info
, представляющему тип самого производного объекта (то есть динамического типа), к которому относится значение glvalue относится. Если выражение glvalue получается, применяя унарный оператор *
к указателю, а указатель - значение нулевого указателя, выражение typeid
генерирует исключение типа, который будет соответствовать обработчику исключения типа std::bad_typeid
.
Выражение 1 ? *p : *p
всегда является lvalue. Это связано с тем, что *p
является lvalue, а [expr.cond]/4 говорит, что если второй и третий операнды для тернарного оператора имеют один и тот же тип и категорию значений, тогда результат оператора имеет такую категорию типов и значений также.
Следовательно, 1 ? *m_basePtr : *m_basePtr
является lvalue с типом Base
. Поскольку Base
имеет виртуальный деструктор, он является полиморфным типом класса.
Поэтому этот код действительно является примером "Когда typeid
применяется к выражению glvalue, тип которого является полиморфным типом класса".
Теперь мы можем прочитать остальную часть приведенной выше цитаты. Выражение glvalue было не ", полученное путем применения унарного оператора *
к указателю" - оно было получено с помощью тернарного оператора. Поэтому стандарт не требует, чтобы исключение вызывалось, если m_basePtr
равно null.
Поведение в случае, когда m_basePtr
равно null, будет охватываться более общими правилами о разыменовании нулевого указателя (которые на самом деле являются немного мутными на С++, но для практических целей мы предположим, что он вызывает undefined поведение здесь).
Наконец: зачем кому-то писать это? Я думаю, что curiousguy ответ является наиболее правдоподобным предположением: с этой конструкцией компилятору не нужно вставлять нулевой указатель и код для генерации исключения, поэтому это микро-оптимизация.
Предположительно, программист либо доволен, что это никогда не будет вызываться с помощью нулевого указателя, либо с удовольствием полагается на конкретную обработку реализации разыменования нулевого указателя.
Ответ 4
Я подозреваю, что какой-то компилятор был для простого случая
typeid(*m_basePtr)
возвращает typeid (Base) всегда, независимо от типа среды выполнения. Но превращение его в выражение/временное/rvalue сделало компилятор RTTI.
Вопрос: какой компилятор, когда и т.д. Я думаю, что у GCC были проблемы с typeid на ранней стадии, но это смутная память.