Мотивация
Основные рекомендации C++ рекомендуют использовать dynamic_cast
, когда навигация по иерархии классов неизбежна. Это приводит к тому, что clang-tidy выдает следующую ошибку: Do not use static_cast to downcast from a base to a derived class; use dynamic_cast instead [cppcoreguidelines-pro-type-static-cast-downcast]
.
Далее в руководстве говорится:
Примечание.
Как и другие приведения,
dynamic_cast
используется слишком часто. Предпочитайтеvirtual
функции кастингу. Предпочитайте статический полиморфизм навигации по иерархии там, где это возможно (не требуется разрешение во время выполнения) и достаточно удобно.
Я всегда просто использовал enum
с именем Kind
, вложенным в мой базовый класс, и выполнял static_cast
на основе его типа. Чтение основных рекомендаций C++, ... Тем не менее, по нашему опыту, такие ситуации, как я знаю, что делаю, по-прежнему являются известным источником ошибок. предполагает, что я не должен этого делать. Часто у меня нет никаких функций virtual
, поэтому RTTI не используется для использования dynamic_cast
(например, я получу error: 'Base_discr' is not polymorphic
). Я всегда могу добавить функцию virtual
, но это звучит глупо. В руководстве также говорится о проведении сравнительного анализа, прежде чем рассматривать возможность использования дискриминантного подхода, который я использую с Kind
.
Эталонный показатель
enum class Kind : unsigned char {
A,
B,
};
class Base_virt {
public:
Base_virt(Kind p_kind) noexcept : m_kind{p_kind}, m_x{} {}
[[nodiscard]] inline Kind
get_kind() const noexcept {
return m_kind;
}
[[nodiscard]] inline int
get_x() const noexcept {
return m_x;
}
[[nodiscard]] virtual inline int get_y() const noexcept = 0;
private:
Kind const m_kind;
int m_x;
};
class A_virt final : public Base_virt {
public:
A_virt() noexcept : Base_virt{Kind::A}, m_y{} {}
[[nodiscard]] inline int
get_y() const noexcept final {
return m_y;
}
private:
int m_y;
};
class B_virt : public Base_virt {
public:
B_virt() noexcept : Base_virt{Kind::B}, m_y{} {}
private:
int m_y;
};
static void
virt_static_cast(benchmark::State& p_state) noexcept {
auto const a = A_virt();
Base_virt const* ptr = &a;
for (auto _ : p_state) {
benchmark::DoNotOptimize(static_cast<A_virt const*>(ptr)->get_y());
}
}
BENCHMARK(virt_static_cast);
static void
virt_static_cast_check(benchmark::State& p_state) noexcept {
auto const a = A_virt();
Base_virt const* ptr = &a;
for (auto _ : p_state) {
if (ptr->get_kind() == Kind::A) {
benchmark::DoNotOptimize(static_cast<A_virt const*>(ptr)->get_y());
} else {
int temp = 0;
}
}
}
BENCHMARK(virt_static_cast_check);
static void
virt_dynamic_cast_ref(benchmark::State& p_state) {
auto const a = A_virt();
Base_virt const& reff = a;
for (auto _ : p_state) {
benchmark::DoNotOptimize(dynamic_cast<A_virt const&>(reff).get_y());
}
}
BENCHMARK(virt_dynamic_cast_ref);
static void
virt_dynamic_cast_ptr(benchmark::State& p_state) noexcept {
auto const a = A_virt();
Base_virt const& reff = a;
for (auto _ : p_state) {
benchmark::DoNotOptimize(dynamic_cast<A_virt const*>(&reff)->get_y());
}
}
BENCHMARK(virt_dynamic_cast_ptr);
static void
virt_dynamic_cast_ptr_check(benchmark::State& p_state) noexcept {
auto const a = A_virt();
Base_virt const& reff = a;
for (auto _ : p_state) {
if (auto ptr = dynamic_cast<A_virt const*>(&reff)) {
benchmark::DoNotOptimize(ptr->get_y());
} else {
int temp = 0;
}
}
}
BENCHMARK(virt_dynamic_cast_ptr_check);
class Base_discr {
public:
Base_discr(Kind p_kind) noexcept : m_kind{p_kind}, m_x{} {}
[[nodiscard]] inline Kind
get_kind() const noexcept {
return m_kind;
}
[[nodiscard]] inline int
get_x() const noexcept {
return m_x;
}
private:
Kind const m_kind;
int m_x;
};
class A_discr final : public Base_discr {
public:
A_discr() noexcept : Base_discr{Kind::A}, m_y{} {}
[[nodiscard]] inline int
get_y() const noexcept {
return m_y;
}
private:
int m_y;
};
class B_discr : public Base_discr {
public:
B_discr() noexcept : Base_discr{Kind::B}, m_y{} {}
private:
int m_y;
};
static void
discr_static_cast(benchmark::State& p_state) noexcept {
auto const a = A_discr();
Base_discr const* ptr = &a;
for (auto _ : p_state) {
benchmark::DoNotOptimize(static_cast<A_discr const*>(ptr)->get_y());
}
}
BENCHMARK(discr_static_cast);
static void
discr_static_cast_check(benchmark::State& p_state) noexcept {
auto const a = A_discr();
Base_discr const* ptr = &a;
for (auto _ : p_state) {
if (ptr->get_kind() == Kind::A) {
benchmark::DoNotOptimize(static_cast<A_discr const*>(ptr)->get_y());
} else {
int temp = 0;
}
}
}
BENCHMARK(discr_static_cast_check);
Я новичок в бенчмаркинге, поэтому я действительно не знаю, что делаю. Я позаботился о том, чтобы virtual
и дискриминантные версии имели одинаковую структуру памяти, и изо всех сил старался предотвратить оптимизацию. Я выбрал уровень оптимизации O1
, так как все, что выше, не представлялось репрезентативным. discr
означает распознанный или помеченный. virt
означает virtual
Вот мои результаты:
Вопросы
Итак, мои вопросы: как мне преобразовать базовый тип в производный, когда (1) я знаю производный тип, потому что я проверил его перед входом в функцию и (2) когда я еще не знаю производный тип. Кроме того, (3) Должен ли я вообще беспокоиться об этом руководстве или мне следует отключить предупреждение? Здесь важна производительность, но иногда это не так. Что я должен использовать?
РЕДАКТИРОВАТЬ:
Использование dynamic_cast
кажется правильным ответом на понижение рейтинга. Тем не менее, вам все равно нужно знать, к чему вы понижаетесь, и иметь функцию virtual
. Во многих случаях вы не знаете без разбора, такого как kind
или tag
, что такое производный класс. (4) В случае, когда мне уже нужно проверить, на какой kind
объекта я смотрю, следует ли мне по-прежнему использовать dynamic_cast
? Разве это не проверка одного и того же дважды? (5) Есть ли разумный способ сделать это без tag
?
Пример
Рассмотрим иерархию class
:
class Expr {
public:
enum class Kind : unsigned char {
Int_lit_expr,
Neg_expr,
Add_expr,
Sub_expr,
};
[[nodiscard]] Kind
get_kind() const noexcept {
return m_kind;
}
[[nodiscard]] bool
is_unary() const noexcept {
switch(get_kind()) {
case Kind::Int_lit_expr:
case Kind::Neg_expr:
return true;
default:
return false;
}
}
[[nodiscard]] bool
is_binary() const noexcept {
switch(get_kind()) {
case Kind::Add_expr:
case Kind::Sub_expr:
return true;
default:
return false;
}
}
protected:
explicit Expr(Kind p_kind) noexcept : m_kind{p_kind} {}
private:
Kind const m_kind;
};
class Unary_expr : public Expr {
public:
[[nodiscard]] Expr const*
get_expr() const noexcept {
return m_expr;
}
protected:
Unary_expr(Kind p_kind, Expr const* p_expr) noexcept :
Expr{p_kind},
m_expr{p_expr} {}
private:
Expr const* const m_expr;
};
class Binary_expr : public Expr {
public:
[[nodiscard]] Expr const*
get_lhs() const noexcept {
return m_lhs;
}
[[nodiscard]] Expr const*
get_rhs() const noexcept {
return m_rhs;
}
protected:
Binary_expr(Kind p_kind, Expr const* p_lhs, Expr const* p_rhs) noexcept :
Expr{p_kind},
m_lhs{p_lhs},
m_rhs{p_rhs} {}
private:
Expr const* const m_lhs;
Expr const* const m_rhs;
};
class Add_expr : public Binary_expr {
public:
Add_expr(Expr const* p_lhs, Expr const* p_rhs) noexcept :
Binary_expr{Kind::Add_expr, p_lhs, p_rhs} {}
};
Сейчас в main()
:
int main() {
auto const add = Add_expr{nullptr, nullptr};
Expr const* const expr_ptr = &add;
if (expr_ptr->is_unary()) {
auto const* const expr = static_cast<Unary_expr const* const>(expr_ptr)->get_expr();
} else if (expr_ptr->is_binary()) {
// Here I use a static down cast after checking it is valid
auto const* const lhs = static_cast<Binary_expr const* const>(expr_ptr)->get_lhs();
// error: cannot 'dynamic_cast' 'expr_ptr' (of type 'const class Expr* const') to type 'const class Binary_expr* const' (source type is not polymorphic)
// auto const* const rhs = dynamic_cast<Binary_expr const* const>(expr_ptr)->get_lhs();
}
}
<source>:99:34: warning: do not use static_cast to downcast from a base to a derived class [cppcoreguidelines-pro-type-static-cast-downcast]
auto const* const expr = static_cast<Unary_expr const* const>(expr_ptr)->get_expr();
^
Не всегда мне нужно приводить к Add_expr
. Например, у меня может быть функция, которая выводит любые Binary_expr
. Нужно только привести его к Binary_expr
, чтобы получить lhs
и rhs
. Чтобы получить символ оператора (например, «-» или «+» ...), он может включить kind
. Я не вижу, как dynamic_cast
поможет мне здесь, и у меня также нет виртуальных функций, на которых можно использовать dynamic_cast
.
РЕДАКТИРОВАТЬ 2:
Я отправил ответ, сделав get_kind()
virtual
, в целом это кажется хорошим решением. Однако теперь я ношу около 8 байтов для vtbl_ptr
вместо байта для тега. Объект, созданный из class
es, производного от Expr
, будет намного превосходить любые другие типы объектов. (6) Это хорошее время, чтобы пропустить vtbl_ptr
или я должен предпочесть безопасность dynamic_cast
?
virtual
и выstatic_cast
от родителя к ребенку, я бы подозрительно. Похоже, композиция была бы лучше, и актерский состав также можно было бы избежать. Если вы знаете тип перед входом в функцию, приведите его перед входом в функцию. если вы еще не знаете производный тип, тогда RTTI (с виртуальным...)/dynamic_cast является решением, вы также можете использовать static_cast, так как он быстрее. Вот где вы должны провести линию. - person Matthieu Brucher   schedule 21.08.2020Expr
кBinary_expr
илиAdd_expr
. Нетvirtual
функций в иерархии. - person user1032677   schedule 21.08.2020dynamic_cast
. - person MSalters   schedule 21.08.2020virtual
функций, но я хочу понизить уровень. Какие-нибудь мысли? - person user1032677   schedule 22.08.2020virtual
, но вы хотите сделать понижение, то у вас проблемы с дизайном. Возможно, есть сложная причина, по которой вы хотите это сделать, но если вам приходится задавать этот вопрос, это означает, что у вас нет необходимых навыков для этого, и поэтому вам не следует использовать этот дизайн. Состав кажется более адекватным. - person Matthieu Brucher   schedule 22.08.2020Expr
, гдеvtbl_ptr
занимает 50% макета памяти объектов дляUnary_expr
? Классы, производные отExpr
(или аналогичные, такие какType
), будут наиболее инстанцируемыми объектами. CRTP, похоже, решает эту проблему, но мне все равно нужно будет использовать указатель на базовый случай. Ответ @ xryl669 предлагает использовать функциюvirtual
, чтобы пометить его, но это как бы портит суть (или я что-то упустил?). Поскольку все объекты, которые я выделяю, будут занимать всего несколько байтов, я не уверен, чтоvptr
является хорошим компромиссом, даже если он быстрый. - person user1032677   schedule 22.08.2020Expr
является базовым классом, а не какой-то формой вариантного типа, где возможности четко указано? Вероятно, это то, о чем Матье говорил с проблемой дизайна: есть лучшие способы сделать то, что вы пытаетесь сделать, чем использование наследования. Ваш классExpr
вполне может бытьstd::variant<BinaryOp, Literal>
, гдеBinaryOp
— это структура, содержащая два других подвыражения и значение, указывающее, какую операцию она выполняет. - person Nicol Bolas   schedule 22.08.2020kind
.gtoe_expr
,modulo_assignment_expr
,double_lit_expr
. Я действительно должен упаковать все это вstd::variant
? - person user1032677   schedule 23.08.2020enum class
дляUnary_expr
, другой дляBinary_expr
, ...). Это приемлемый ответ. Спасибо! - person user1032677   schedule 23.08.2020