【问题标题】:Double dispatch and template class双调度和模板类
【发布时间】:2025-08-13 07:20:01
【问题描述】:

我有一个 C++ 代码,我在其中比较了从一个共同的母类 Foo 派生的不同类。如果两个类的类型不同,则比较总是false。否则,它会比较特定于类的一些内部数据。

我的代码如下所示:

class Bar;
class Baz;

class Foo
{
public:
    virtual bool isSame( Foo* ) = 0;
    virtual bool isSameSpecific( Bar* ){ return false; }
    virtual bool isSameSpecific( Baz* ){ return false; }
};

class Bar : public Foo
{
public:
    bool isSame( Foo* foo){ return foo->isSameSpecific(this); }
    bool isSameSpecific( Bar* bar){ return bar->identifier == identifier; }

    int identifier;
};

// and the same for Baz...

这很好用(我认为这是双重调度),我可以将 BarBaz 与仅指向 Foo 的指针进行比较。

但现在问题来了。我必须添加一个模板类:

template< typename T>
class Qux : public Foo
{
//...
};

问题是在Foo 中,我不能为Qux* 声明方法isSameSpecific,因为它是虚拟的和模板的。

问题:有什么巧妙的方法可以解决这个问题吗?

【问题讨论】:

  • 也只制作Foo类模板,或者为所有需要的Qux&lt;T&gt;编写不同的isSameSpecific重载。
  • @ForEveR 感谢您的评论!模板参数在 Foo 上没有意义,未来类可能有其他模板参数,所以我不想在 Foo 上得到所有可能的模板参数。对于不同的重载,这可能是一个可接受的解决方案,但 T 可能有很多可能性,所以要编写这么多重载(不是很整洁)。
  • 看看 Andrei Alexandrescu 的 Loki Library,尤其是 Visitor 模式。
  • @PeterWood 有意思,我去看看!

标签: c++ templates double-dispatch


【解决方案1】:

这个问题没有真正的解决方案:你需要 一个isSameSpecific 函数,用于每个实例化 您使用的模板。 (换句话说,在Foo

template <typename T>
virtual bool isSameSpecific( Qux<T>* );

是非法的,但是:

virtual bool isSameSpecific( Qux<int>* );
virtual bool isSameSpecific( Qux<double>* );
//  etc.

不是。)

您也许可以通过创建摘要而侥幸成功 QuxBase,并让 Qux&lt;T&gt; 派生自它。最有可能的, 这只会将问题转移到QuxBase,但如果 isSameSpecific 不依赖于 T 的类型,例如 因为您可以定义一些规范的包含类型,所以它可能 可行。不知道更多关于QuxisSameSpecific,很难说。 (如果 Qux&lt;T&gt;::isSameSpecific 应该总是返回 false 如果 实例化类型不同,例如,您可以键入 签到QuxBase::isSameSpecific,然后转发给另一个 如果类型相同,则为虚函数。)

请注意,类似问题会影响所有替代方法 也实现了多次调度。最后,你是 要求对一组开放的类型进行调度,这意味着 可能有无数种不同的功能。

编辑:

明确一点:我假设您的 isSame 只是一个 例如,实际操作可能更复杂。 您显示的实际代码显然符合我的建议 第二段;事实上,它甚至可以实现 无需多次调度。只需定义一个规范的“标识符” 输入,定义一个虚拟的getCanonicalIdentifier 函数,然后 在isSame 中使用它:

bool Foo::isSame( Foo const* other ) const
{
    return getCanonicalIdentifier() 
        == other->getCanonicalIdentifier(); 
}

就此而言,如果不同的类型意味着isSame 返回 false (通常情况下,如果 isSame 表示它的外观 像),你也不需要双重调度:

bool Foo::isSame( Foo const* other ) const
{
    return typeid( *this ) == typeid( *other )
        && isSameSpecific( other );
}

派生的isSameSpecific 必须转换类型 指针,但因为它们保证它是相同的 作为this的类型,操作简单安全。

最后:如果类没有值语义(以及 如果涉及多态性,几乎肯定不应该), 像这样简单的东西:

bool Foo::isSame( Foo const* other ) const
{
    return this == other;
}

可能就够了。

所有这些都适用于isSame,然而。 如果您有其他功能受到影响,您会回来 和我最初说的一样。

【讨论】:

  • 感谢非常有趣的回答!当您说实现相当复杂时,您是对的,但这是我问题的核心。我认为实际上您对 QuxBase 的想法解决了我的问题!实际上,类型 T 通常是一个包含数据的类(我可以完全控制它)。所以,我可以让所有潜在的 T 派生自一个类,并有一个方法(再次双分派)来比较它们。然后,在 QuxBase 中,我可以比较来自 Qux 的特定数据,并通过比较方法比较类型 T。
【解决方案2】:

编译器在解析class Foo 定义时必须知道isSameSpecific virtuals 的(有限)集合。虚拟对象都在 vtable 中保留了条目。模板Qux 可以被无限次覆盖,需要Foo 中无限数量的虚拟。显然,即使不尝试描述一种定义它们的方法,这也是行不通的。

你也许可以使用 typeinfo 来做你想做的事,但它不会与类型多态性有关。

【讨论】:

  • 感谢您的回答!这就是我的想法,这就是每个“模板虚拟”方法的问题。 Typeinfo 是我的默认解决方案,但它并不认为它是 neat。所以,也许解决方案确实是改变我对整洁的定义。
【解决方案3】:

你是对的,这是双重调度,你是对的,不幸的是,一个方法不能同时是 virtualtemplate(后者是一个实现问题)。

恐怕设计不可能做到这一点;但是您可以在Qux 中作弊。

template <typename T>
class Qux: public Foo {
    virtual bool isSame( Foo* foo ) {
        if (Qux* q = dynamic_cast<Qux*>(foo)) {
            return *this == *q;
        }
        return false;
    }
}; // class Qux

当然,dynamic_cast 有点作弊(就像所有针对儿童的演员一样),但是嘿它有效

注意:isSame 方法应该是 const 并采用 const 参数,也就是 virtual bool isSame(Foo const* foo) const;

【讨论】:

  • 感谢您的回答!您的解决方案可能是我将实施的解决方案(实际上,我可能会在所有 isSame 方法中这样做并摆脱 isSameSpecific),除非有些人有更好的想法。是的,演员阵容不在我的整洁定义中:-)。对于您的注意:在我的实现中,它实际上是 const,因为它对于理解问题不是必需的,所以我摆脱了它。无论如何感谢您的建议!
  • 一点都不作弊。这里的演员表对设计来说不是必不可少的,它只是为了克服语言限制。
  • 如果这是一个足够的解决方案,您可以在基类中进行类型检查,如果两种类型相同,则仅转发到isSameSpecific。如果isSame 不应该总是返回false,那么在基类中进行检查可能是更好的解决方案(即使它涉及RTTI),因为您在基类中强制执行后置条件。
  • @JamesKanze:不过会略有不同,因为我提出的解决方案允许将 Qux&lt;int&gt;class K: public Qux&lt;int&gt; {}; 进行比较(无论好坏)。在您的情况下,isSameSpecific 的签名是什么? Foo* 也是?
  • @MatthieuM。签名必须是Foo*,因为要覆盖工作,签名都必须相同。因此,每个派生类都必须将参数转换为其类型。但是,因为这是函数的先决条件,所以它可以使用static_cast。 (当然,虚函数应该是私有的,以确保除了基类中的isSame 之外永远不会调用它。)
【解决方案4】:

如何使用 RTTI:

#include <typeinfo>

struct Foo
{
    virtual ~Foo() { }
    virtual bool compare_with_same(Foo const & rhs) = 0;
};

struct Bar : Foo
{
    int thing;

    virtual bool compare_with_same(Foo const & rhs)
    {
        assert(dynamic_cast<Bar const *>(&rhs) != nullptr);

        return static_cast<Bar const &>(rhs).thing == thing;
    }
}

bool operator==(Foo const & lhs Foo const & rhs)
{
    return typeid(lhs) == typeid(rhs) && lhs.compare_with_same(rhs);
}

或者,您可以将typeid 代码放入每个compare_with_same 覆盖。这样可能更安全一些。

【讨论】:

  • 感谢您的回答! RTTI 不符合我对 neat 的定义,但我认为我别无选择... 可能更多的是我对 neat 的定义是错误的 :-)
  • @Dr_Sam 这可能是你对整洁的定义。如果有后置条件typeid(lhs) != typeid(rhs) 暗示false,那么这应该在基类中强制执行;此处提出的解决方案,typeid 实际上比依赖于每个派生类正确执行此操作更干净(即仅为自己的类覆盖isSameSpecific)。
  • @Dr_Sam 请注意,如果您有一个所有实例都有不同标识符的不变量,那么bool Foo::isSame( Foo const* other ) const { return this == other; } 是迄今为止最简单的解决方案。
  • @JamesKanze 不幸的是,没有标识符。母班里的对比,你是对的!