双调度生成“隐藏虚拟函数”警告,为什么? [英] Double dispatch produces 'hides virtual function' warnings, why?

查看:147
本文介绍了双调度生成“隐藏虚拟函数”警告,为什么?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

  #include< iostream> 

class A
{
public:
virtual void PostCompose(A * other)
{
other-> PreCompose(this) ;
}
virtual void PreCompose(A * other)
{
std :: cout< 使用A对象预处理< std :: endl;
}
};

class B:public A
{
public:
virtual void PostCompose(A * other)//这一个需要出现以防止警告
{
other-> PreCompose(this);
}
virtual void PreCompose(A * other)//这需要存在以防止错误
{
std :: cout< 使用A对象预处理< std :: endl;
}
virtual void PostCompose(B * other)
{
other-> PreCompose(this);
}
virtual void PreCompose(B * other)
{
std :: cout< 使用B对象预合成< std :: endl;
}
};

int main()
{
A a;
B b;
a.PostCompose(& a); // - > Precomposing with a A object
a.PostCompose(& b); // - > Precomposing with a A object
b.PostCompose(& a); // - > Precomposing with a A object
b.PostCompose(& b); // - > 使用B对象预处理
}

我有两个,此代码:


  1. 您认为这是一种合理的方法吗? $ 如果我省略前两个 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 ,谢谢。我的第一个问题怎么样?对此方法的任何评论?

解决方案

双调度通常在C ++中实现不同,基类具有所有不同的版本使它成为一个维护噩梦,但这是语言是如何)。你尝试双重分派的问题是动态分派将找到最多派生类型 B 的对象,你正在调用该方法,但是参数有静态类型 A * 。由于 A 没有以 B * 为参数的重载,则调用 other - > PreCompose(this)将隐式上传 A *



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



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



一旦找到第一个标识符,lookup将停止并尝试将函数调用与可用的重载匹配,如果调用不能匹配,则将触发错误。这里重要的是,如果函数调用不能匹配,查找将不会在层次结构中向上看。通过添加使用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 ,并且查找将检查 / code>,标识符 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你); 对象的静态类型是 base (注意实际对象是派生,但是引用的类型是 base& ),因此查找不会搜索 derived base 中启动。它找到两个重载,其中一个是 good 匹配,因此它将调用 base :: foo(const char *)。对于 b.foo(5)也是如此。



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



如果你添加了使用base :: foo 声明到派生类,你将添加所有重载 foo 中的可用重载的中的,并且调用 d.foo(Hi); 会考虑 base 中的重载,发现最好的重载是 :foo(const char *); ,所以它实际上会被执行为 d.base :: foo(Hi); p>

在很多情况下,开发人员并不总是在考虑查找规则是如何工作的,并且可能会感到惊讶的是,调用 d.foo(5 ); > 失败,而不是使用base :: foo 声明,或者更糟的是, d.foo Hi); 被分派到 derived :: foo(std :: string const& )明显是比 base :: foo(const char *)这是编译器在您隐藏成员函数时发出警告的原因之一。这个警告的另一个好的原因是,在许多情况下,当你实际想要覆盖一个虚拟函数,你可能会错误地改变签名:

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

尝试覆盖函数 name (忘记了 const 限定符)意味着你实际上创建了一个不同的函数签名。 derived :: name 不是覆盖 base :: name 通过引用 base name 将不会分派到 derived :: name !!!


I would like to implement interactions between two objects whose types are derived from a common base class. There is a default interaction and specific things may happen once objects of the same type interact. This is implemented using the following double dispatch scheme:

#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"
}

I have two, unfortunately quite different questions regarding this code:

  1. Do you think this is a reasonable approach? Would you suggest something different?
  2. If I omit the first two B methods, I get compiler warnings and errors that the last two B methods hide the A methods. Why is that? An A* pointer should not be cast to a B* pointer, or should it?

Update: I just found out that adding

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

makes the errors and warnings vanish, but why is this necessary?

Update 2: This is neatly explained here: http://www.parashift.com/c++-faq-lite/strange-inheritance.html#faq-23.9, thank you. What about my first question? Any comments on this approach?

解决方案

Double dispatch is usually implemented differently in C++, with the base class having all different versions (which makes it a maintenance nightmare, but that is how the language is). The problem with your attempt to double dispatch is that dynamic dispatch will find the most derived type B of the object on which you are calling the method, but then the argument has static type A*. Since A does not have an overload that takes B* as argument, then the call other->PreCompose(this) will implicitly upcast this to A* and you are left with single dispatch on the second argument.

As of the actual question: why is the compiler producing the warnings? why do I need to add the using A::Precompose directives?

The reason for that are the lookup rules in C++. Then the compiler encounters a call to obj.member(), it has to lookup the identifier member, and it will do so starting from the static type of obj, if it fails to locate member in that context it will move up in the hierarchy and lookup in the bases of the static type of obj.

Once the first identifier is found, lookup will stop and try to match the function call with the available overloads, and if the call cannot be matched it will trigger an error. The important bit here is that lookup will not look further up in the hierarchy if the function call cannot be matched. By adding the using base::member declaration, you are bringing the identifier member from the base class into the current scope.

Example:

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" );
}

When the compiler encounters the expression d.foo( "Hi" ); the static type of the object is derived, and lookup will check all member functions in derived, the identifier foo is located there, and lookup does not proceed upwards. The argument to the only available overload is std::string const&, and the compiler will add an implicit conversion, so even if there could be a best potential match (base::foo(const char*) is a better match than derived::foo(std::string const&) for that call) it will effectively call:

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

The next expression d.foo( 5 ); is processed similarly, lookup starts in derived and it finds that there is a member function there. But the argument 5 cannot be converted to std::string const & implicitly and the compiler will issue an error, even if there is a perfect match in base::foo(int). Note that this is an error in the call, not an error in the class definition.

When processing the third expression, b.foo( "you" ); the static type of the object is base (note that the actual object is derived, but the type of the reference is base&), so lookup will not search in derived but rather start in base. It finds two overloads, and one of them is a good match, so it will call base::foo( const char* ). The same goes for b.foo(5).

Finally, while adding the different overloads in the most derived class hide the overloads in the base, it does not remove them from the objects, so you can actually call the overload that you need by fully qualifying the call (which disables lookup and has the added side effect of skipping dynamic dispatch if the functions were virtual), so d.base::foo( "there" ) will not perform any lookup at all and just dispatch the call to base::foo( const char* ).

If you had added a using base::foo declaration to the derived class, you would add all the overloads of foo in base to the available overloads in derived, and the call d.foo( "Hi" ); would consider the overloads in base and find that the best overload is base::foo( const char* );, so it will actually be executed as d.base::foo( "Hi" );

In many cases, developers are not always thinking on how the lookup rules actually work, and it might be surprising that the call to d.foo( 5 ); fails without the using base::foo declaration, or worse, that the call to d.foo( "Hi" ); is dispatched to derived::foo( std::string const & ) when it is clearly a worse overload than base::foo( const char* ). That is one of the reasons why compilers warn when you hide member functions. The other good reason for that warning is that in many cases when you actually intended to override a virtual function you might end up mistakenly changing the signature:

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" ????
}

A small mistake while trying to override the member function name (forgetting the const qualifier) means that you are actually creating a different function signature. derived::name is not an override to base::name and thus a call to name through a reference to base will not be dispatched to derived::name!!!

这篇关于双调度生成“隐藏虚拟函数”警告,为什么?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆