5

动机

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即可获得lhsand rhs。要获取运算符的符号(例如“-”或“+”...),它可以打开kind. 我不知道如何dynamic_cast在这里帮助我,而且我也没有可使用dynamic_cast的虚拟功能。

编辑2:

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

4

4 回答 4

2

如果您在编译时知道实例的类型,您可能对此处的好奇递归模板模式感兴趣,以避免需要虚拟方法

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 表),因此必须为代码中非常小的性能敏感部分保留它.

于 2020-08-21T10:43:38.697 回答
2

所有这些论点都很精彩,但在某些情况下这些解决方案无法应用。一个例子是经验丰富的 JNI 规范。作为事后的想法,索尼将 C++ 包装器添加到官方 Java 原生接口中。例如,他们定义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"

对于文件或块

于 2021-01-17T18:55:39.757 回答
1

一个可能的解决方案是创建get_kind()一个virtual函数。然后您可以使用dynamic_cast. 如果要调用很多virtual函数,可以将其向下转换为最派生的类,以便优化器可以优化virtual调用。您还需要使用virtual继承(例如class Unary_expr : public virtual Expr {};,如果您在基类中没有任何数据成员来正确使用内存。在vtable64 位机器上,指向 的指针占用 8 个字节,因此您可能被迫使用区分以减少每个对象的大小(但这显然只有在绝对不virtual使用任何函数时才有意义)。

此方法解决了指南中提出的以下问题:

...即便如此,根据我们的经验,这种“我知道我在做什么”的情况仍然是一个已知的错误来源。

@xryl669 指出,如果您知道要处理的类型,可以使用“奇怪的递归模板模式”或 CRTP 来消除在运行时检查类型的需要。他也涵盖了该方法的一个问题和解决方案,因此您也一定要检查他的答案。

这是我发现有用的另一个关于 CRTP 的资源:C++ 中动态(虚拟调用)与静态(CRTP)调度的成本

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();
    }
}
于 2020-08-22T05:44:52.507 回答
1

我认为本指南的重要部分是关于“类层次结构导航不可避免”的部分。这里的基本点是,如果你想做很多这种铸造,那么很有可能你的设计有问题。要么你选择了错误的方式来做某事,要么你把自己设计成一个角落。

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

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

因此,对于各种二进制操作,您有一个不同的功能。如果只有一个功能发生了变化,那么您真的不需要为此而使用不同的类型。BinaryOp不同的二元运算符在一个通用类中是不同的值更合理。UnaryOpNullaryOps也是如此。

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

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

于 2020-08-23T15:11:21.253 回答