动机
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
and rhs
。要获取运算符的符号(例如“-”或“+”...),它可以打开kind
. 我不知道如何dynamic_cast
在这里帮助我,而且我也没有可使用dynamic_cast
的虚拟功能。
编辑2:
我已经发布了一个答案get_kind()
virtual
,这似乎是一个很好的解决方案。但是,我现在为 a 携带大约 8 个字节,vtbl_ptr
而不是为标签携带一个字节。class
从es 派生的对象实例化Expr
将远远超过任何其他对象类型。(6) 这是跳过的好时机vtbl_ptr
还是我应该更喜欢安全dynamic_cast
?