c++ - 如何正确使用 "C++ Core Guidelines: C.146: Use dynamic_cast where class hierarchy navigation is unavoidable"

标签 c++ polymorphism dynamic-cast static-polymorphism cpp-core-guidelines

动机
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:

Like other casts, dynamic_cast is overused. Prefer virtual functions to casting. Prefer static polymorphism to hierarchy navigation where it is possible (no run-time resolution necessary) and reasonably convenient.


我一直只使用 enum命名 Kind嵌套在我的基类中,并执行了 static_cast基于它的种类。阅读 C++ 核心指南,“...即便如此,根据我们的经验,诸如“我知道我在做什么”的情况仍然是一个已知的错误来源。”建议我不应该这样做。通常,我没有任何 virtual函数,因此 RTTI 不存在以使用 dynamic_cast (例如,我会得到 error: 'Base_discr' is not polymorphic )。我可以随时添加 virtual功能,但这听起来很傻。该指南还说在考虑使用我与 Kind 一起使用的判别方法之前先进行基准测试。 .
Benchmark

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这是我的结果:
Benchmark Results
问题
所以,我的问题是:当 (1) 我知道派生类型,因为我在进入函数之前检查了它,以及 (2) 当我还不知道派生类型时,我应该如何从基类转换为派生类型。此外,(3)我应该担心这个指南,还是应该禁用警告?性能在这里很重要,但有时并不重要。我应该使用什么?
编辑:
使用 dynamic_cast好像是correct低头的答案。但是,您仍然需要知道您正在向下转型并拥有一个 virtual功能。在很多情况下,你不知道没有歧视,比如 kindtag派生类是什么。 (4) 在我已经要检查什么的情况下 kind我正在查看的对象,我是否应该仍然使用 dynamic_cast ?这不是两次检查同一件事吗? (5) 在没有 tag 的情况下,是否有合理的方法可以做到这一点? ?
Example
考虑 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获取 lhsrhs .要获取运算符的符号(例如“-”或“+”...),它可以打开 kind .我不明白 dynamic_cast会在这里帮助我,而且我也没有可以使用的虚函数 dynamic_cast在。
编辑2:
我已经发布了一个答案 get_kind() virtual ,这似乎是一个很好的解决方案。但是,我现在为 vtbl_ptr 携带了大约 8 个字节。而不是标签的字节。从 class 实例化的对象es 源自 Expr将远远超过任何其他对象类型。 (6) 现在是跳过vtbl_ptr 的好时机吗?或者我应该更喜欢dynamic_cast的安全性?

最佳答案

如果您在编译时知道实例的类型,您可能对这里的 Curious Recursing Template Pattern 感兴趣,以避免对虚方法的需要

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);
抱歉上面的伪代码,但你会明白的。
请注意,像上面这样混合使用动态继承和静态继承会使代码维护成为负担(如果添加新类型,则需要修复所有 开关表 ),因此必须保留用于非常小的代码的性能敏感部分。

关于c++ - 如何正确使用 "C++ Core Guidelines: C.146: Use dynamic_cast where class hierarchy navigation is unavoidable",我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/63520261/

相关文章:

c++ - 我应该为索引变量使用什么类型

c++ - dynamic_cast vs static_cast 无效*

c++ - 什么可能导致 dynamic_cast 崩溃?

C++ OpenGL 用鼠标拖动多个对象

c++ - 将空范围传递给采用一对迭代器的函数的简洁方法是什么?

c++ - 如果找到给定的单词,则保存下一个单词 (C++)

scala - 反编译 Scala 代码 : why there are two overridden methods in the derived class?

c# - 多态和虚方法

java - Java 中的转换(接口(interface)和类)

c# - 我应该使用 dynamic_cast<T> 进行复制吗?