问题描述
动机
《 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]
。
指南继续说:
Note:
与其他强制类型转换一样,
dynamic_cast
被过度使用。优先使用virtual
函数 铸造。首选静态多态性而不是层次结构导航 有可能(无需运行时解析)并且合理 方便。
我一直只使用嵌套在基类中的名为enum
的{{1}},并根据其种类执行Kind
。阅读C ++核心准则,“ ...即使如此,根据我们的经验,诸如“我知道自己在做什么”的情况仍然是已知的bug来源。”建议我不应该这样做。通常,我没有任何static_cast
函数,因此无法使用RTTI来使用virtual
(例如,我将得到dynamic_cast
)。我总是可以添加一个error: 'Base_discr' is not polymorphic
函数,但这听起来很愚蠢。该指南还要求先进行基准测试,然后再考虑使用与virtual
一起使用的判别方法。
Benchmark
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
,这是我的结果:
问题
所以,我的问题是:当(1)我知道派生类型是因为我在输入函数之前检查过它,而(2)我还不知道派生类型时,我应该如何从基本类型转换为派生类型? 。此外,(3)我是否应该为此准则担心,还是应该禁用该警告?此处的性能很重要,但有时并不重要。我应该使用什么?
编辑:
使用virtual
似乎是向下转换的correct答案。但是,您仍然需要知道要向下转换的内容并具有dynamic_cast
函数。在许多情况下,如果没有诸如virtual
或kind
之类的区别,就不会知道派生类是什么。 (4)在已经必须检查正在查看的对象tag
的情况下,我是否仍应使用kind
?这不是两次检查同一件事吗? (5)有没有dynamic_cast
的合理方法吗?
Example
考虑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
的虚函数。
编辑2:
我已经发布了一个答案,dynamic_cast
get_kind()
,这似乎是一个很好的解决方案。但是,我现在为virtual
携带了8个字节,而不是标签的一个字节。从vtbl_ptr
派生的class
实例化的对象将远远超过任何其他对象类型。 (6)现在是跳过Expr
的好时机还是我更喜欢vtbl_ptr
的安全性?
解决方法
如果您在编译时知道实例的类型,您可能会对此处的好奇递归模板模式感兴趣,从而根本不需要使用虚拟方法
template <typename Impl>
class Base_virt {
public:
Base_virt(Kind p_kind) noexcept : m_kind{p_kind},m_x{} {}
[[nodiscard]] inline Kind
get_kind() const noexcept { return Impl::kind(); }
[[nodiscard]] inline int
get_x() const noexcept {
return m_x;
}
[[nodiscard]] inline int get_y() const noexcept {
return static_cast<const Impl*>(this)->get_y();
}
private:
int m_x;
};
class A_virt final : public Base_virt<A_virt> {
public:
A_virt() noexcept : Base_virt{Kind::A},m_y{} {}
[[nodiscard]] inline static Kind kind() { return Kind::A; }
[[nodiscard]] inline int
get_y() const noexcept final {
return m_y;
}
private:
int m_y;
};
// Copy/paste/rename for B_virt
在那种情况下,根本不需要dynamic_cast,因为在编译时就已经知道了一切。您失去了存储指向Base_virt
的指针的可能性(除非您创建BaseTag
派生的Base_virt
基类)
然后,调用该方法的代码必须是模板:
template <typename Impl>
static void
crtp_cast_check(benchmark::State& p_state) noexcept {
auto const a = A_virt();
Base_virt<Impl> const* ptr = &a;
for (auto _ : p_state) {
benchmark::DoNotOptimize(ptr->get_y());
}
}
BENCHMARK(crtp_static_cast_check<A_virt>);
这很可能被编译为对for(auto _ : p_state) b::dno(m_y)
的海峡调用。
这种方法的不便之处在于二进制空间过大(您将拥有与子类型一样多的函数实例),但这将是最快的,因为编译器将在编译时推断出该类型。
使用BaseTag
方法,它看起来像:
class BaseTag { virtual Kind get_kind() const = 0; };
// No virtual destructor here,since you aren't supposed to manipulate instance via this type
template <typename Impl>
class Base_virt : BaseTag { ... same as previous definition ... };
// Benchmark method become
void virt_bench(BaseTag & base) {
// This is the only penalty with a virtual method:
switch(base.get_kind()) {
case Kind::A : static_cast<A_virt&>(base).get_y(); break;
case Kind::B : static_cast<B_virt&>(base).get_y(); break;
...etc...
default: assert(false); break; // At least you'll get a runtime error if you forget to update this table for new Kind
}
// In that case,there is 0 advantage not to make get_y() virtual,but
// if you have plenty of "pseudo-virtual" method,it'll become more
// interesting to consult the virtual table only once for get_kind
// instead of for each method
}
template <typename Class>
void static_bench(Class & inst) {
// Lame code:
inst.get_y();
}
A_virt a;
B_virt b;
virt_bench(a);
virt_bench(b);
// vs
static_bench(a);
static_bench(b);
很抱歉上面的伪代码,但是您会明白的。
请注意,将上述动态继承与静态继承混合使用会使代码维护成为负担(如果添加新类型,则需要修复所有 switch表),因此必须保留给代码中非常小的性能敏感部分。
,可能的解决方案是使get_kind()
成为virtual
函数。然后,您可以使用dynamic_cast
。如果要调用许多virtual
函数,则可以将其转换为最派生的类,以便优化程序可以优化virtual
调用。您还将需要使用virtual
继承(例如,如果您在基类中没有任何数据成员可以正确使用内存,则使用class Unary_expr : public virtual Expr {};
。拥有指向vtable
的指针占用8个字节在64位计算机上,因此您可能被迫使用区分项来缩小每个对象的大小(但这显然仅在绝对不使用virtual
函数的情况下才有意义)。
此方法解决了准则中提出的以下问题:
...即使如此,根据我们的经验,“我知道我在做什么”的情况仍然是已知的错误来源。
@ xryl669指出,如果您知道要处理的是哪种类型,则可以使用“好奇的递归模板模式”或CRTP来消除在运行时检查类型的需要。他也介绍了该方法的问题和解决方案,因此您也应该彻底检查一下他的答案。
以下是我发现有用的关于CRTP的另一资源:The cost of dynamic (virtual calls) vs. static (CRTP) dispatch in C++
class Expr {
public:
enum class Kind : unsigned char {
Int_lit_expr,Neg_expr,Add_expr,Sub_expr,};
[[nodiscard]] virtual Kind get_kind() const noexcept = 0;
[[nodiscard]] virtual bool
is_unary() const noexcept {
return false;
}
[[nodiscard]] virtual bool
is_binary() const noexcept {
return false;
}
};
class Unary_expr : public virtual Expr {
public:
[[nodiscard]] bool
is_unary() const noexcept final {
return true;
}
[[nodiscard]] Expr const*
get_expr() const noexcept {
return m_expr;
}
protected:
explicit Unary_expr(Expr const* p_expr) noexcept : m_expr{p_expr} {}
private:
Expr const* const m_expr;
};
class Binary_expr : public virtual Expr {
public:
[[nodiscard]] bool
is_binary() const noexcept final {
return true;
}
[[nodiscard]] Expr const*
get_lhs() const noexcept {
return m_lhs;
}
[[nodiscard]] Expr const*
get_rhs() const noexcept {
return m_rhs;
}
protected:
Binary_expr(Expr const* p_lhs,Expr const* p_rhs) noexcept : m_lhs{p_lhs},m_rhs{p_rhs} {}
private:
Expr const* const m_lhs;
Expr const* const m_rhs;
};
class Add_expr final : public Binary_expr {
public:
Add_expr(Expr const* p_lhs,Expr const* p_rhs) noexcept : Binary_expr{p_lhs,p_rhs} {}
[[nodiscard]] Kind get_kind() const noexcept final {
return Kind::Add_expr;
}
};
int main() {
auto const add = Add_expr{nullptr,nullptr};
Expr const* const expr_ptr = &add;
if (expr_ptr->is_unary()) {
// NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores): it is just an example
auto const* const expr = dynamic_cast<Unary_expr const* const>(expr_ptr)->get_expr();
} else if (expr_ptr->is_binary()) {
// NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores): it is just an example
auto const* const lhs = dynamic_cast<Binary_expr const* const>(expr_ptr)->get_lhs();
// NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores): it is just an example
auto const* const rhs = dynamic_cast<Binary_expr const* const>(expr_ptr)->get_lhs();
}
}
,
我认为本指南的重要部分是关于“不可避免地进行类层次结构导航”的部分。这里的基本要点是,如果您想进行很多此类转换,那么很可能是您的设计有问题。要么您选择了错误的方法来做某事,要么您将自己设计成一个角落。
OOP的过度使用就是这种情况的一个例子。让我们以Expr
为例,它是表达式树中的一个节点。您可以问它类似是二进制操作,一元操作还是空操作(仅供参考:文字值是空值,而不是一元。它们不带参数)之类的问题。
您过度使用OOP的地方是试图为每个运算符赋予自己的类类型。加法运算符和乘法运算符有什么区别?优先?这是语法问题;构建表达式树后,它就无关紧要了。真正关心特定二进制运算符的唯一操作是在评估它时。甚至在进行评估时,唯一与众不同的部分是当您获得操作数的评估结果并将其馈送到将产生此操作结果的代码中。所有二进制操作的其他所有内容都相同。
因此,您拥有一个针对各种二进制操作而有所不同的功能。如果只有一个功能会发生变化,那么您实际上并不需要仅使用其他类型。对于不同的二进制运算符,在常规BinaryOp
类中使用不同的值要合理得多。 UnaryOp
和NullaryOp
也是如此。
因此,在此示例中,任何给定节点只有3种可能的类型。将其作为variant<NullaryOp,UnaryOp,BinaryOp>
处理是非常合理的。因此,Expr
只能包含其中之一,每个操作数类型都具有零个或多个指向其子Expr
元素的指针。 Expr
上可能有一个通用接口,用于获取子代数,遍历子代等。并且不同的Op
类型可以通过简单的访问者为这些子代提供实现。
大多数情况下,当您开始希望进行向下转换时,可以使用其他机制更好,更干净地解决这些问题。如果您要构建不带virtual
函数的层次结构,而接收基类的代码已经知道大多数或所有可能的派生类,那么您实际上是在编写variant
的粗体形式是很可能的。
所有这些论点都很棒,但在某些情况下,这些解决方案无法应用。一个例子是经验丰富的 JNI 规范。作为事后的想法,索尼在官方 Java 本机接口中添加了 C++ 包装器。例如,他们定义了返回 GetObjectField()
的 jobject
方法。但是如果字段是一个数组,你必须强制转换到jbyteArray
,例如能够使用GetArrayLength()
。
不可能将 dynamic_cast 与 JNI 一起使用。替代方案是C 风格强制转换或static_cast,我相信后者比
更安全,或者至少更干净。(jbyteArray)env->CallObjectMethod(myObject,toByteArray_MethodID);
要在 Android Studio 中抑制单行警告,请使用 NOLINT
:
auto byteArray = static_cast<jbyteArray>(env->CallObjectMethod(myObject,toByteArray_MethodID)); // NOLINT(cppcoreguidelines-pro-type-static-cast-downcast)
或者,设置
#pragma ide diagnostic ignored "cppcoreguidelines-pro-type-static-cast-downcast"
用于文件或块