寻找一种模式来避免在实现虚函数时使用dynamic_cast

Looking for a pattern to avoid using dynamic_cast in implementation of a virtual function

本文关键字:函数 实现 cast dynamic 一种 模式 寻找      更新时间:2023-10-16

我的问题是:我有一个接口根类,其中包含几个具体的分支类。在应用程序代码中,存在指向根类的指针向量。在几个地方,我需要遍历向量中的所有元素,并将它们与给定的实例进行比较:

// Application Code
void compare_loop(Root *r, std::vector<Root*> vec) {
    for (auto v : vec) {
        if (r->compare(v)) {
            // Do something to v
        }
    }
}

我最初的方法是使"比较"根类中的虚函数:

// Class Definition
class Root {
public:
    Root(double bar) : Bar(bar) {};
    virtual bool compare(Root *r) = 0;
protected:
    double Bar;
};
class BranchA : public Root {
public:
    BranchA(double bar, double baz) : Root(bar), BazA(baz) {};
    bool compare(Root *r) override;
protected:
    double BazA;
};
class BranchB : public Root {
public:
    BranchB(double bar, int baz, bool foo) : Root(bar), BazB(baz), Foo(foo) {};
    bool compare(Root *r) override;
protected:
    int BazB;
    bool Foo;
};

问题是,如果参数不是相同的具体类型,则"compare"函数的实现应始终计算为 false,否则取决于特定于 BranchA/BranchB 的成员变量。对于单个虚拟成员函数,我能想到实现它的唯一方法是尝试dynamic_cast:

// Implementation
bool BranchA::compare(Root *r) {
    BranchA* branch = dynamic_cast<BranchA*>(r);
    if (branch == nullptr) {
        return false;
    }
    else {
        // Do some complicated comparison based on BranchA members
        return (Bar < branch->Bar) && (BazA < branch->BazA);
    }
}
bool BranchB::compare(Root *r) {
    BranchB* branch = dynamic_cast<BranchB*>(r);
    if (branch == nullptr) {
        return false;
    }
    else {
        // Do some complicated comparison based on BranchB members
        return (Bar > branch->Bar) && (BazB > branch->BazB) && (Foo == branch->Foo);
    }
}

这对我来说似乎不是最优雅的方法,但我不确定。我想知道在类定义和实现中是否可以采用不同的方法,该方法将在不更改应用程序代码的情况下产生相同的结果。或者,这是适合使用dynamic_cast的情况吗?

我通常使用一种模式来使用 NVI 习语1,2。仍然需要演员阵容,但这是static_cast而不是dynamic_cast。该static_cast避免了与dynamic_cast相关的额外费用,并保证安全(请参阅代码注释)。

但是,我并不是说这个解决方案比OP的代码快,因为它仍然使用typeid检查以及isEqual函数的动态调度。

与问题中的代码相比,这里的主要优点是基类的比较逻辑的更改不会影响派生类的实现,其中可能有很多派生类。

示例代码

#include <iostream>
#include <memory>
#include <vector>
class Root
{
public:
    explicit Root(double bar) : Bar(bar) {}
    // Base class must have a virtual destructor for deletion through
    // the base pointer to work properly
    virtual ~Root() {}
    bool operator==(const Root& other) const
    {
        // Make sure the two types being compared are the same derived type
        return (typeid(*this) == typeid(other)) &&
            // Compare all state associated with the base class
            (Bar == other.Bar) &&
            // Dispatch comparison to the derived implementation to finish
            // the comparison
            isEqual(other);
    }
private:
    // Guaranteed to only be dispatched by operator== if 'other' is the
    // same type as '*this'
    virtual bool isEqual(const Root &other) const = 0;
    double Bar;
};
class BranchA : public Root
{
public:
    BranchA(double bar, double baz) : Root(bar), BazA(baz) {}
private:
    virtual bool isEqual(const Root& other) const override
    {
        // static_cast is safe since the Base class guarantees it won't
        // call this function unless 'other' and '*this' are the same type
        const BranchA& branch = static_cast<const BranchA&>(other);
        return (BazA == branch.BazA);
    }
    double BazA;
};
class BranchB : public Root
{
public:
    BranchB(double bar, int baz, bool foo) : Root(bar), BazB(baz), Foo(foo) {}
private:
    virtual bool isEqual(const Root& other) const override
    {
        // static_cast is safe since the Base class guarantees it won't
        // call this function unless 'other' and '*this' are the same type
        const BranchB& branch = static_cast<const BranchB&>(other);
        return (BazB == branch.BazB) && (Foo == branch.Foo);
    }
    int BazB;
    bool Foo;
};
void compare_loop(const Root &r, const std::vector<std::unique_ptr<Root>>& vec)
{
    for (const auto& v : vec)
    {
        if (r == *v)
        {
            std::cout << "Equivalentn";
        }
        else
        {
            std::cout << "Differentn";
        }
    }
}
int main()
{
    BranchA root(1.0, 2.0);
    std::vector<std::unique_ptr<Root>> branches;
    branches.push_back(std::make_unique<BranchA>(root));
    branches.push_back(std::make_unique<BranchA>(1.0, 1.0));
    branches.push_back(std::make_unique<BranchB>(1.0, 2.0, true));
    compare_loop(root, branches);
    return 0;
}

示例输出

Equivalent
Different
Different

现场示例


1 非虚拟接口模式 - 维基百科
2 虚拟 - 赫伯·萨特

Bjarne Stroustrup有一篇关于如何产生相当于dynamic_cast<>的东西的优秀出版物,可以像模一样快。可在此处获得:http://www.stroustrup.com/fast_dynamic_casting.pdf

基本思想是,为每个类分配一对整数。第一个是每个类的不同素数,第二个是每个基类的第二个乘以当前类的第一个的乘积。如果source::second % target::first == 0,则dynamic_cast<>将成功。它对很多事情都有好处:快速dynamic_cast<>,钻石检测,一组元素的最大公共子类型,抽象访问者和多重调度等。

一种粗略的替代方法是简单地让每个派生类提供您自己的类型标识符并比较它们。

#include <iostream>
#include <fstream>
#include <string>
class Base {
    virtual const char* type_name() const noexcept = 0;
public:
    Base() {}
    bool is_same_type(const Base* rhs) const noexcept {
        return type_name() == rhs->type_name();
    }
};
class D1 : public Base {
    const char* type_name() const noexcept override { return "D1"; }
public:
    D1() {}
};
class D1D1 : public D1 {
    const char* type_name() const noexcept override { return "D1D1"; }
public:
    D1D1() {}
};
void test(const Base* lhs, const Base* rhs)
{
    std::cout << lhs->is_same_type(rhs) << 'n';
}
int main()
{
    D1 d1;
    D1D1 d1d1;
    test(&d1, &d1d1);
}

缺点是,如果我忘记在 D1D1 中显式添加覆盖,它将继承其父级的type_name并虚假地声称是同一类型。

您在 LSP 上失败了,其中基类的虚拟行为可以通过基类的属性来描述。

实际上,接口"图面"是一个动态接口,可以无限地扩展到基类的所有可能的派生类型。 例如,某人不能创建您的一种类型的独立"克隆":基类的确切动态类型是其行为的一部分。

有许多替代方法。

您可以在(可能处于活动状态的)动态字典中存储和公开属性,并比较这些属性。

您可以对子类型进行有限的枚举,并显式地将基本子类型与其接口一起作为其接口的一部分,以及快速强制转换接口。

您可以从 COM 窃取页面,并拥有一个允许第三方克隆的引用计数查询接口。 然后,类型可以通过模拟该接口而不是实现来模拟派生类。

或者,您可以接受您的界面表面是无限且未指定。 继续使用动态强制转换或类型id(基本等效)。

问题的名称是双重调度问题,或者双重动态调度问题,如果你想进一步研究它。