如何正确使用“C++ 核心指南:C.146:在不可避免的类层次结构导航中使用 dynamic_cast”

use*_*677 5 c++ polymorphism dynamic-cast static-polymorphism cpp-core-guidelines

动机

C++ 核心指南建议dynamic_cast在“类层次结构导航不可避免”时使用。这触发铛,整洁抛出以下错误: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根据其类型执行 a 。阅读 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);
Run Code Online (Sandbox Code Playgroud)

我是基准测试的新手,所以我真的不知道我在做什么。我小心地确保virtual和判别版本具有相同的内存布局,并尽我所能防止优化。我选择了优化级别,O1因为更高的级别似乎没有代表性。discr代表歧视或标记。virt代表virtual 这是我的结果:

基准测试结果

问题

所以,我的问题是:当 (1) 我知道派生类型,因为我在进入函数之前检查了它,以及 (2) 当我还不知道派生类型时,我应该如何从基类转换为派生类型。此外,(3)我应该担心这个指南,还是应该禁用警告?性能在这里很重要,但有时并不重要。我应该使用什么?

编辑:

使用dynamic_cast似乎是向下转型的正确答案。但是,您仍然需要知道您正在向下转型并拥有一个virtual功能。在许多情况下,您不知道如何区分诸如派生类kindtag派生类是什么。(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} {}
};
Run Code Online (Sandbox Code Playgroud)

现在在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();
    }
}
Run Code Online (Sandbox Code Playgroud)
<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();

                                 ^
Run Code Online (Sandbox Code Playgroud)

我并不总是需要强制转换为Add_expr. 例如,我可以有一个函数可以打印出任何Binary_expr. 它只需要将其强制转换Binary_exprlhs和 即可rhs。要获取运算符的符号(例如“-”或“+”...),它可以打开kind. 我不知道这dynamic_cast对我有什么帮助,而且我也没有可以使用的虚函数dynamic_cast

编辑2:

我已经发布了一个答案get_kind() virtual,这似乎是一个很好的解决方案。但是,我现在携带大约 8 个vtbl_ptr字节用于标记而不是一个字节。从class派生的 es实例化的对象Expr将远远超过任何其他对象类型。(6) 这是跳过的好时机vtbl_ptr还是我应该更喜欢安全dynamic_cast

Nic*_*las 1

我认为本指南的重要部分是关于“类层次结构导航是不可避免的”部分。这里的基本点是,如果您经常进行这种铸造,那么您的设计很可能有问题。要么你选择了错误的做事方式,要么你把自己逼到了墙角。

过度使用 OOP 就是这样的一个例子。让我们以 为例Expr,它是表达式树中的一个节点。你可以问它一些问题,比如它是二元运算、一元运算还是空运算(仅供参考:文字值是空值,而不是一元。它们不带参数)。

过度使用 OOP 的地方在于尝试为每个运算符提供其自己的类类型。加法运算符和乘法运算符有什么区别?优先?这是语法问题;一旦你构建了表达式树,它就无关紧要了。唯一真正关心特定二元运算符的操作是在评估它时。即使在进行求值时,唯一特殊的部分是当您获取操作数的求值结果并将其输入将产生此操作结果的代码时。对于所有二元运算,其他一切都是相同的。

所以你有一个对于各种二元运算来说不同的函数。如果只有一个函数发生变化,那么您确实不需要为此而使用不同的类型。在通用类中,不同的二元运算符具有不同的值更为合理BinaryOpUnaryOp和s也是如此NullaryOp

因此,在此示例中,任何给定节点只有 3 种可能的类型。作为variant<NullaryOp, UnaryOp, BinaryOp>. 因此 anExpr只能包含其中之一,每个操作数类型都有零个或多个指向其子Expr元素的指针。可能有一个通用接口Expr用于获取子级的数量、迭代子级等。不同的Op类型可以通过简单的访问者提供这些的实现。

大多数情况下,当您开始想要进行向下转型时,这些事情可以使用其他机制更好、更干净地解决。如果您正在构建没有virtual函数的层次结构,其中接收基类的代码已经知道大多数或所有可能的派生类,那么您实际上正在编写variant.