【问题标题】:Virtual inheritance and static inheritance - mixing in C++虚拟继承和静态继承 - C++ 中的混合
【发布时间】:2010-11-19 00:45:03
【问题描述】:

如果你有这样的事情:

#include <iostream>

template<typename T> class A
{
public:
    void func()
    {
        T::func();
    }
};

class B : public A<B>
{
public:
    virtual void func()
    {
        std::cout << "into func";
    }
};

class C : public B
{
};

int main()
{
  C c;
  c.func();

  return 0;
}

func() 是动态调度的吗?
你怎么能实现类 A,如果 B 有一个虚拟覆盖,它是动态调度的,但如果 B 没有,它是静态调度的?

编辑:我的代码没有编译?对不起大家。我现在有点病了。我的新代码也无法编译,但这是问题的一部分。另外,这个问题是给我的,而不是常见问题。

#include <iostream>

template<typename T> class A
{
public:
    void func()
    {
        T::func();
    }
};

class B : public A<B>
{
public:
    virtual void func()
    {
        std::cout << "in B::func()\n";
    }
};

class C : public B
{
public:
    virtual void func() {
        std::cout << "in C::func()\n";
    }
};
class D : public A<D> {
    void func() {
        std::cout << "in D::func()\n";
    }
};
class E : public D {
    void func() {
        std::cout << "in E::func()\n";
    }
};

int main()
{
  C c;
  c.func();
  A<B>& ref = c;
  ref.func(); // Invokes dynamic lookup, as B declared itself virtual
  A<D>* ptr = new E;
  ptr->func(); // Calls D::func statically as D did not declare itself virtual
  std::cin.get();

  return 0;
}

visual studio 2010\projects\temp\temp\main.cpp(8): error C2352: 'B::func' : illegal call of non-static member function
      visual studio 2010\projects\temp\temp\main.cpp(15) : see declaration of 'B::func'
      visual studio 2010\projects\temp\temp\main.cpp(7) : while compiling class template member function 'void A<T>::func(void)'
      with
      [
          T=B
      ]
      visual studio 2010\projects\temp\temp\main.cpp(13) : see reference to class template instantiation 'A<T>' being compiled
      with
      [
          T=B
      ]

【问题讨论】:

  • 添加到常见问题解答中,似乎很有用。
  • 也许通过说明您打算做什么会给您一个更好的解决方案,并且说“更好”我主要是指可维护,您可以阅读和理解您自己的稍后再编码。
  • @the_drow:这个问题需要很多帮助才能成为常见问题解答候选者。代码示例需要更改才能编译,然后发布者需要解释实际行为与期望行为有何不同,然后才能回答,如果看起来足够通用,则进行常见问题解答。
  • @Ben Voigt:似乎这个问题对于那些想要了解 v-talbe 以及 c++ 它是如何在幕后具有魔力的人来说非常有用。可以做些什么来改进此消息?
  • @the_drow:这个问题与虚函数实现的内部无关。我认为这与模板专业化、编译时重载解析和通过虚函数的动态调度之间的交互有关,但很难回答有关无法编译的代码行为的问题。

标签: c++ oop inheritance


【解决方案1】:

我不确定我是否理解您的要求,但您似乎缺少必要的 CRTP 演员:

template<class T>
struct A {
  void func() {
    T& self = *static_cast<T*>(this);  // CRTP cast
    self.func();
  }
};

struct V : A<V> {  // B for the case of virtual func
  virtual void func() {
    std::cout << "V::func\n";
  }
};

struct NV : A<NV> {  // B for the case of non-virtual func
  void func() {
    std::cout << "NV::func\n";
  }
};

如果 T 没有声明自己的 func,这将是无限递归,因为 self.func 会找到 A::func。即使 T 的派生类(例如下面的 DV)声明了自己的 func 但 T 没有声明,也是如此。

使用不同的最终覆盖器进行测试,以显示调度工作与宣传的一样:

struct DV : V {
  virtual void func() {
    std::cout << "DV::func\n";
  }
};
struct DNV : NV {
  void func() {
    std::cout << "DNV::func\n";
  }
};

template<class B>
void call(A<B>& a) {
  a.func();  // always calls A<T>::func
}

int main() {
  DV dv;
  call(dv);   // uses virtual dispatch, finds DV::func
  DNV dnv;
  call(dnv);  // no virtual dispatch, finds NV::func

  return 0;
}

【讨论】:

  • 我本可以明确地写出 DV 和 DNV,也许我应该为了清楚起见,但请注意 D::func 实际上是隐式的,因为它继承的 V::func 是虚拟的.
  • 与简单地不使func 虚拟化相比,这有何改进?您不能使用任何指向超类型的指针调用call()(实际上在示例中没有超类型)。
  • @Ben:在 OP 的代码中,也没有通用的基本类型(“超类型”)。这是对 OP 代码的最小重构,据我所知,它按照他的要求进行(CRTP 注释和下一行是关键),然后通过一个简短的示例进行说明。
  • 我考虑过使用static_cast。但是,我对强制转换有点担心,因为它可能会掩盖错误(例如,class C : public A&lt;B&gt; 否则会失败,其次,我担心它会丢失 CV 和右值/左值限定符。
  • @DeadMG:C++0x 的 static_assert A 是 T 的基类(或非 0x 等效项)解决了第一个问题,但这正是 CRTP 的工作原理,通常在实践。 static_cast 不能删除 CV 限定符;尝试会产生编译错误。 *this 始终是方法中的左值,就像我上面的 self 一样。
【解决方案2】:

你如何实现类 A,如果 B 有一个虚拟覆盖,它是动态调度的,但如果 B 没有,它是静态调度的?

有点矛盾,不是吗? A 类的用户可能对 B 或 C 一无所知。如果您有对 A 的引用,那么了解 func() 是否需要动态调度的唯一方法是查阅 vtable。由于A::func() 不是虚拟的,因此它没有条目,因此无处放置信息。一旦你把它变成虚拟的,你就可以查询 vtable 并且它是动态调度。

获得直接函数调用(或内联)的唯一方法是使用非虚拟函数并且不通过基类指针进行间接调用。

编辑:我认为 Scala 中的成语是 class C: public B, public A&lt;C&gt; (用子类重复特征)但这在 C++ 中不起作用,因为它使 A&lt;T&gt; 的成员在 C 中模棱两可。

【讨论】:

  • 实际上,使用 CRTP 确实可以让您了解 A 中的 B(这就是 CRTP 的全部意义所在)。
  • 我可能对这个问题感到困惑。正如所写,func 是私有的,所以C.func() 是一个编译错误。 B::func() 完全隐藏了继承的A&lt;B&gt;::func()(如所写),所以A&lt;B&gt;::func() 的唯一可能调用者是A&lt;B&gt;,所以是的,它知道B,但它不知道C。所以也许我的第一句话应该是“A 对(任何可能的)C 一无所知”
  • 您可以显式调用隐藏函数,例如c.A::func(),所以 A 不是“唯一可能的调用者”(如果我们忽略两个 func 都是私有的)。的确,A 不知道(任何可能的)C,但问题只询问了 B。
  • 或更可能的情况:A&lt;B&gt;* p = new C(); p-&gt;func(); -- 这将调用A&lt;B&gt;::func -- 如果它当然会编译。
【解决方案3】:

在您的特定示例中,不需要动态调度,因为 c 的类型在编译时是已知的。对B::func 的调用将被硬编码。

如果您通过B* 调用func,那么您将调用虚函数。但在您高度人为的示例中,这会让您再次访问B::func

谈论来自A* 的动态调度没有多大意义,因为A 是一个模板类——你不能创建一个通用的A,它只能绑定到一个特定的子类。

【讨论】:

  • 你可以有一个A&lt;B&gt;*
【解决方案4】:

你如何实现类 A,如果 B 有一个虚拟覆盖,它是动态调度的,但如果 B 没有,它是静态调度的?

正如其他人所注意到的,这个问题真的很难理解,但它让我想起了我很久以前学到的东西,所以这里有一个很长的机会来回答你的问题:

template<typename Base> class A : private Base
{
public:
    void func()
    {
        std::count << "A::func";
    }
};

鉴于此,func() 是否为虚拟取决于A 的基数。如果Base 将其声明为virtual,那么它在A 中也将是虚拟的。否则不会。看到这个:

class V
{
public:
    virtual void func() {}
};
class NV
{
};

class B : public A<V>  // makes func() virtual
{
public:
    void func()
    {
        std::count << "B::func";
    }
};

class C : public A<NV>  // makes func() non-virtual
{
public:
    void func()
    {
        std::count << "C::func";
    }
};

这会回答您的问题吗?

【讨论】:

  • +1 表示template&lt;typename Base&gt; class A : private Base,这很有趣。但是,我在这里看到的致命缺陷是没有合适的基类指针类型(A&lt;V&gt; 不能转换为A&lt;NV&gt;)。鉴于它似乎并没有以问题似乎提出的方式真正“隐藏”虚拟性。
  • @Ben:从模板参数派生对于 CRTP 类来说并不少见。我不确定您指出的有关基类转换的问题。而且我已经说过我不知道 OP 真正要求什么。
  • @sbi:我醒了,让我的问题(和代码)更有意义。
  • @sbi:从模板参数派生与CRTP相反,CRTP将派生类作为模板参数。 (前者仍然有用,甚至可以与 CRTP 结合使用,但它不是 CRTP。)
  • @Fred:我不知道给人的印象是从模板参数派生 CRTP。
【解决方案5】:

函数是否动态调度取决于两件事:

a) 对象表达式是引用类型还是指针类型

b) 函数(重载解析解析为)是否为虚函数。

现在进入你的代码:

  C c; 
  c.func();   // object expression is not of pointer/reference type. 
              // So static binding

  A <B> & ref = c; 
  ref.func(); // object expression is of reference type, but func is 
              // not virtual. So static binding


  A<D>* ptr = new D; 
  ptr->func(); // object expression is of pointer type, but func is not 
               // virtual. So static binding 

简而言之,'func' 不是动态调度的。

注意 :: 抑制了虚函数调用机制。

$10.3/12-“明确限定 范围运算符(5.1)抑制 虚拟的“调用机制”。

OP2 中的代码出错,因为只有当 'Y' 是 'X' 范围内的静态成员时,语法 X::Y 才能用于调用 'X' 范围内的 'Y'。

【讨论】:

  • (a) 模棱两可:它可能被误解为引用行为与指针不同。
【解决方案6】:

似乎您只需要添加一些跟踪和用法即可回答您自己的问题...

#include <iostream>

template<typename T> struct A { 
    void func() { 
        T::func(); 
    } 
}; 

struct B1 : A<B1> { 
    virtual void func() { 
        std::cout << "virtual void B1::func();\n";
    } 
}; 

struct B2 : A<B2> { 
    void func() { 
        std::cout << "void B2::func();\n";
    } 
}; 

struct C1 : B1 { }; 
struct C2 : B2 { }; 

struct C1a : B1 {
    virtual void func() {
        std::cout << "virtual void C1a::func();\n";
    }
};

struct C2a : B2 {
    virtual void func() {
        std::cout << "virtual void C2a::func();\n";
    }
};

int main()
{
    C1 c1; 
    c1.func(); 

    C2 c2; 
    c2.func(); 

    B1* p_B1 = new C1a;
    p_B1->func();

    B2* p_B2 = new C2a;
    p_B2->func();
}

输出:

virtual void B1::func();
void B2::func();
virtual void C1a::func();
void B2::func();

结论:A 确实具有 B 的虚函数。

【讨论】:

  • A::func() 甚至没有被使用(如果被使用也不会编译)。
  • 错误信息将是 ` In member function 'void A::func() [with T = B1]': error: cannot call member function 'virtual void B1::func( )' 没有对象`
  • @Ben:问题是“你如何实现类 A,如果 B 有一个虚拟覆盖,它是动态分派的,但如果 B 没有,则静态分派?”。上面显示了取决于 B 的成员的虚拟与静态调度 - A 的版本是隐藏的。如果您想使用 A 的版本而不需要 B 中的版本,那么只需将 A 更改为可以编译并从 B2 中删除 func() 的东西 - 这个概念成立。
猜你喜欢
  • 1970-01-01
  • 2021-11-25
  • 1970-01-01
  • 1970-01-01
  • 2016-05-20
  • 2011-12-30
  • 2017-03-31
  • 2016-03-26
  • 2011-01-08
相关资源
最近更新 更多