2

我想实现两个对象之间的交互,其类型派生自一个公共基类。存在默认交互,一旦相同类型的对象交互,可能会发生特定的事情。这是使用以下双重调度方案实现的:

#include <iostream>

class A
{
public:
  virtual void PostCompose(A* other)
    {
      other->PreCompose(this);
    }
  virtual void PreCompose(A* other)
    {
      std::cout << "Precomposing with an A object" << std::endl;
    }
};

class B : public A
{
public:
  virtual void PostCompose(A* other) // This one needs to be present to prevent a warning
    {
      other->PreCompose(this);
    }
  virtual void PreCompose(A* other) // This one needs to be present to prevent an error
    {
      std::cout << "Precomposing with an A object" << std::endl;
    }
  virtual void PostCompose(B* other)
    {
      other->PreCompose(this);
    }
  virtual void PreCompose(B* other)
    {
      std::cout << "Precomposing with a B object" << std::endl;
    }
};

int main()
{
  A a;
  B b;
  a.PostCompose(&a); // -> "Precomposing with an A object"
  a.PostCompose(&b); // -> "Precomposing with an A object"
  b.PostCompose(&a); // -> "Precomposing with an A object"
  b.PostCompose(&b); // -> "Precomposing with a B object"
}

不幸的是,关于这段代码,我有两个完全不同的问题:

  1. 你认为这是一个合理的方法吗?你会建议一些不同的东西吗?
  2. 如果我省略前两种B方法,我会收到编译器警告和最后两种B方法隐藏A方法的错误。这是为什么?指针不A*应该转换为B*指针,还是应该转换?

更新:我刚刚发现添加

using A::PreCompose;
using A::PostCompose;

使错误和警告消失,但为什么这是必要的?

更新 2:这在此处得到了巧妙的解释:http: //www.parashift.com/c++-faq-lite/strange-inheritance.html#faq-23.9,谢谢。我的第一个问题呢?对此方法有何评论?

4

3 回答 3

4

双分派通常在 C++ 中以不同的方式实现,基类具有所有不同的版本(这使其成为维护的噩梦,但这就是语言的样子)。您尝试双重分派的问题在于,动态分派会找到B您正在调用该方法的对象的最派生类型,但随后参数具有静态类型A*。由于A没有B*作为参数的重载,因此调用other->PreCompose(this)将隐式向上this转换,A*并且您在第二个参数上只剩下单个调度。

至于实际问题:为什么编译器会产生警告?为什么我需要添加using A::Precompose指令?

原因是 C++ 中的查找规则。然后编译器遇到对 的调用obj.member(),它必须查找标识符member,它会从 的静态类型开始obj查找,如果member在该上下文中定位失败,它将在层次结构中向上移动并在静态的基础中查找的类型obj

一旦找到第一个标识符,查找将停止并尝试将函数调用与可用的重载匹配,如果调用无法匹配,则会触发错误。这里重要的一点是,如果函数调用无法匹配,则查找将不会在层次结构中进一步向上查找。通过添加using base::member声明,您将member基类中的标识符带入当前范围。

例子:

struct base {
   void foo( const char * ) {}
   void foo( int ) {}
};
struct derived : base {
   void foo( std::string const & ) {};
};
int main() {
   derived d;
   d.foo( "Hi" );
   d.foo( 5 );
   base &b = d;
   b.foo( "you" );
   b.foo( 5 );
   d.base::foo( "there" );
}

当编译器遇到d.foo( "Hi" );对象的静态类型为的表达式derived时,查找将检查 中的所有成员函数derived,标识符foo位于那里,并且查找不会向上进行。唯一可用重载的参数是std::string const&,并且编译器将添加一个隐式转换,因此即使可能存在最佳潜在匹配(base::foo(const char*)derived::foo(std::string const&)该调用更好的匹配),它也会有效地调用:

d.derived::foo( std::string("Hi") );

下一个表达式d.foo( 5 );处理类似,查找开始,derived它发现那里有一个成员函数。但是参数5不能std::string const &隐式转换,编译器会报错,即使base::foo(int). 请注意,这是调用中的错误,而不是类定义中的错误。

在处理第三个表达式时,b.foo( "you" );对象的静态类型是base(注意实际对象是derived,但是引用的类型是base&),所以lookup 不会搜索 inderived而是从 开始base。它找到了两个重载,其中一个是很好的匹配,所以它会调用base::foo( const char* ). 也是如此b.foo(5)

最后,虽然在最派生类中添加不同的重载会隐藏基类中的重载,但它不会从对象中删除它们,因此您实际上可以通过完全限定调用来调用您需要的重载(禁用查找并具有如果函数是虚拟的,则添加跳过动态调度的副作用),因此d.base::foo( "there" )根本不会执行任何查找,而只是将调用调度到base::foo( const char* ).

如果您using base::foo向类添加了声明derived,您会将 in 的所有重载添加foobase可用的重载 in 中derived,并且调用d.foo( "Hi" );会考虑 in 的重载base并发现最佳重载是base::foo( const char* );,因此它实际上将被执行为d.base::foo( "Hi" );

在许多情况下,开发人员并不总是考虑查找规则实际上是如何工作的,如果没有声明调用失败d.foo( 5 );,或者更糟糕的是using base::foo,调用显然比. 这就是当您隐藏成员函数时编译器会发出警告的原因之一。该警告的另一个充分理由是,在许多情况下,当您实际上打算覆盖虚函数时,您最终可能会错误地更改签名:d.foo( "Hi" );derived::foo( std::string const & )base::foo( const char* )

struct base {
   virtual std::string name() const {
      return "base";
   };
};
struct derived : base {
   virtual std::string name() {        // missing const!!!!
      return "derived";
   }
}
int main() {
   derived d; 
   base & b = d;
   std::cout << b.name() << std::endl; // "base" ????
}

尝试覆盖成员函数name(忘记const限定符)时的一个小错误意味着您实际上正在创建不同的函数签名。derived::name不是对的覆盖,因此不会将通过对引用base::name的调用发送到!!!namebasederived::name

于 2011-04-14T10:06:59.193 回答
1
using A::PreCompose;
using A::PostCompose;
makes the errors and warnings vanish, but why is this necessary?

如果您将新函数添加到与基类包含的名称相同的派生类中,并且如果您不覆盖基类中的虚函数,则新名称会隐藏基类中的旧名称。

这就是为什么您需要通过显式编写来取消隐藏它们:

using A::PreCompose;
using A::PostCompose;

取消隐藏它们的其他方法(在这种特殊情况下)是,覆盖您在发布的代码中完成的基类中的虚函数。我相信代码会编译得很好。

于 2011-04-14T09:28:21.957 回答
0

类是作用域,在基类中查找被描述为在封闭作用域中查找。

在查找函数的重载时,如果在嵌套函数中找到函数,则不会在封闭范围内查找。

这两个规则的结果是您实验的行为。添加 using 子句从封闭范围导入定义,这是正常的解决方案。

于 2011-04-14T09:28:37.863 回答