双重调度产生“隐藏虚拟功能”警告,为什么?

时间:2011-04-14 09:22:55

标签: c++ double-dispatch

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

#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*指针,还是应该?
  3. 更新:我刚发现添加了

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

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

    更新2 :这里有一个很好的解释:http://www.parashift.com/c++-faq-lite/strange-inheritance.html#faq-23.9,谢谢。我的第一个问题呢?对这种方法有何评论?

3 个答案:

答案 0 :(得分: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& }),因此查找不会在derived中搜索,而是从base开始。它找到两个重载,其中一个是 good 匹配,因此它将调用base::foo( const char* )b.foo(5)也是如此。

最后,在大多数派生类中添加不同的重载隐藏基础中的重载时,它不会从对象中删除,所以你可以实际调用通过完全限定调用所需的重载(如果函数是虚拟的,则禁用查找并具有跳过动态调度的附加副作用),因此d.base::foo( "there" )将不会执行任何查找,只需将调用分派给base::foo( const char* )

如果您向using base::foo课程添加了derived声明,则会将foobase的所有重载添加到derived中的可用重载中调用d.foo( "Hi" );会调用base中的重载并发现最佳重载为base::foo( const char* );,因此它实际上会以d.base::foo( "Hi" );

执行

在许多情况下,开发人员并不总是在考虑查找规则的实际工作方式,如果没有d.foo( 5 );声明,对using base::foo的调用失败可能会令人惊讶,或者更糟糕的是,调用当{em>明显比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的覆盖,因此通过对name的引用对base的调用不会被分发到derived::name } !!!

答案 1 :(得分:1)

using A::PreCompose;
using A::PostCompose;
makes the errors and warnings vanish, but why is this necessary?

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

这就是为什么你需要通过明确的写作取消隐藏它们:

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

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

答案 2 :(得分:0)

类是范围,基类中的查找被描述为在封闭范围内查找。

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

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