Я нашел следующий код в каком-то проекте:
std::vector<Base*> objs_;
template <class TDerived>
T* Get() {
auto objIt = std::find_if (objs_.cbegin(), objs_.cend(), [](Base* it) {
return typeid(TDerived).name() == typeid(*it).name();
});
return objIt == objs_.cend() ? nullptr : *objIt;
}
Я пытаюсь выяснить, является ли использование typeid
хорошим выбором?
Потому что в C++ есть dynamic_cast
, например:
template <class TDerived>
T* Get() {
auto objIt = std::find_if (objs_.cbegin(), objs_.cend(), [](Base* it) {
return dynamic_cast<T*>(it) != nullptr;
});
return objIt == objs_.cend() ? nullptr : *objIt;
}
Я знаю, что между приведенными выше решениями есть семантическая разница, потому что dynamic_cast
также будет жаловаться на TDerived
наследника, но это нормально, все используемые TDerived
являются final
.
Является ли typeid
хорошим выбором, dynamic_cast
лучше или есть лучшее решение?
Нет, typeid
не лучший выбор. Ни dynamic_cast
. Весь этот подход следует выбросить и переписать с нуля, используя unique_ptr
или shared_ptr
и виртуальные методы. Избегайте написания кода, похожего на то, что было найдено в «каком-то проекте».
Очевидный вопрос: зачем коду идентифицировать объекты по их типу? Как говорит @BoP, этого обычно легко избежать (и, следовательно, должно быть).
Это не мой код, поэтому я его и спросил. Насчет "зачем искать точный тип" попробую сказать то, что понял из кода. Автор добавляет в коллекцию некоторые производные объекты, а затем проверяет, содержит ли она объект какого-либо типа. Если да, то он вызывает какую-то виртуальную функцию, иначе не вызывает.
@excommunicado Тогда можно легко избежать теста «какой тип этого объекта». Просто добавьте в базовый класс виртуальную функцию «ничего не делать» и вызывайте ее независимо. Я указываю на это, чтобы продемонстрировать свое утверждение о том, что проверка типа (полиморфного) объекта редко, если вообще необходима.
Нет, typeid вообще не очень хорошая идея, потому что подтипирование одного из задействованных типов потребует обогащения частей, где проверяется typeid
. Это противоречит принципу открытости/закрытости.
Кстати, с typeid
есть много тонких проблем, например. нет стандартизации возвращаемых имен типов, и, кроме того, как указано в cppreference:
Нет никакой гарантии, что на один и тот же экземпляр std::type_info будут ссылаться все оценки выражения typeid для одного и того же типа, хотя они будут сравниваться равными, std::type_info::hash_code этих объектов type_info будет идентичным, поскольку будет их std::type_index.
Использование dynamic_cast
немного лучше, потому что это может позволить соблюдать принцип открытости/закрытости, если вы не добавляете новый тип. Но если вы это сделаете, вы снова обязаны обогатить код. Более того, dynamic_cast
требует, чтобы используемый класс/функция много знала об используемых классах. Это может ослабить инкапсуляцию и создать скрытую связь (т. е. вы больше не можете изменять используемые классы по своему усмотрению, потому что вы нарушаете некоторые предположения).
Лучший подход — переписать код полиморфным образом. Основные принципы C++ напоминают в связи с этим, что виртуальные функции следует предпочитать приведению типов . В более общем плане подход должен использовать принцип «говори, не спрашивай» и позволять полиморфному коду делать то, что он должен делать. Или выберите шаблон посетителя.
Вы не можете сравнивать имена, потому что они не должны иметь одинаковые значения указателя для одних и тех же типов. Например, typeid(TDerived).name() == typeid(TDerived).name()
может быть ложным.
Вместо этого вы должны сравнивать typeid напрямую с std::type_info::operator==:
return typeid(TDerived) == typeid(*it);
Что касается того, лучше ли это, чем dynamic_cast
, это зависит. Для окончательного класса, как вы упомянули, семантически нет никакой разницы. Я думаю, что сравнение typeid
делает ваши намерения более ясными.
typeid
не обязательно должен быть уникальным, поэтому вряд ли это хороший выбор. С другой стороны, зачем вам искать точный тип? Не может ли виртуальная функция в Base быть более полезной?