Подтвердить что ты не робот

Странное использование кода `?:` В `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;
};
4b9b3361

Ответ 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 на ранней стадии, но это смутная память.