【问题标题】:Redeclaring pure virtual function重新声明纯虚函数
【发布时间】:2020-10-19 11:48:50
【问题描述】:

我最近遇到了以下类型的代码:

struct A {
    virtual void foo() = 0;
};

struct B : public A {
    virtual void foo() = 0;
};

struct C : public B {
    virtual void foo() override {
    }
};

重新声明一个纯虚方法有什么用吗?

更重要的是:这会改变语义吗?即B::foo 遮蔽A::foo 并引入新的vftable?

如果A::foo 实际上不是纯虚拟的但提供了一个实现(仍然为我编译),上面的例子会发生什么?

【问题讨论】:

  • 即使A::foo() 不是纯虚拟的,struct B 中的virtual void foo() = 0; 也会使struct B 抽象化,即它可能不会被实例化。 Demo on coliruB 中重复纯虚拟的感觉(因为它目前在公开的代码中)可能是为了记录。即使不是真的必要,也不会造成任何伤害。
  • 这能回答你的问题吗? Pure virtual methods in derived abstract class
  • 它不会改变任何事情,但我会在子类中跳过virtual 并将override 放在它们两个上。您甚至可以将A::foo 设为纯虚拟并且 具有默认实现。这对于基类析构函数很常见。

标签: c++


【解决方案1】:

cppreference的例子:

struct Abstract {
    virtual void f() = 0; // pure virtual
}; // "Abstract" is abstract
 
struct Concrete : Abstract {
    void f() override {} // non-pure virtual
    virtual void g();     // non-pure virtual
}; // "Concrete" is non-abstract
 
struct Abstract2 : Concrete {
    void g() override = 0; // pure virtual overrider
}; // "Abstract2" is abstract
 
int main()
{
    // Abstract a; // Error: abstract class
    Concrete b; // OK
    Abstract& a = b; // OK to reference abstract base
    a.f(); // virtual dispatch to Concrete::f()
    // Abstract2 a2; // Error: abstract class (final overrider of g() is pure)
}

重新声明一个纯虚方法有什么用吗?

在派生类中将虚方法声明为纯方法是有目的的:Abstract2 可以将 g 标记为纯虚方法,即使它在 Concrete 中不是纯方法。

这会改变语义吗?即是 B::foo 遮蔽 A::foo 并引入一个新的 vftable?

没有。

如果 A::foo 实际上不是纯虚拟的,但提供了一个实现(仍然为我编译),上面的例子会发生什么?

查看示例。

您不必在派生类中将方法删除为virtual。当它在基类中声明为virtual 时,它在派生类中也是virtual。简而言之,=0 只是告诉编译器不一定有定义并且类是抽象的。您的示例与以下内容相同:

struct AA {                       // abstract
    virtual void foo() = 0;         
};

struct BB : public AA { };         // abstract

struct CC : public BB {            // concrete
    void foo() override {}
};

我知道的唯一区别是BB::foo 的类型:

// above
std::cout << std::is_same< decltype(&BB::foo), void (AA::*)()>::value;
std::cout << std::is_same< decltype(&BB::foo), void (BB::*)()>::value;
std::cout << std::is_same< decltype(&BB::foo), void (CC::*)()>::value << "\n";
// your example
std::cout << std::is_same< decltype(&B::foo), void (A::*)()>::value;
std::cout << std::is_same< decltype(&B::foo), void (B::*)()>::value;
std::cout << std::is_same< decltype(&B::foo), void (C::*)()>::value << "\n";

输出是:

100
010

即在B 中重新声明foo 会直接在B 中引入名称fooB::fooB 的成员函数。另一方面,在我的示例中,BB::foo 是来自AA 的方法。但是,这不会影响C::foo

【讨论】:

  • 澄清:将非纯函数重新声明为纯函数将“摆脱”父级提供的原始实现,但不会改变任何事情(除了保证类将是抽象的)?
  • @SebastianHoffmann 不确定“否则”是什么意思,它还会影响什么?
  • @SebastianHoffmann 以防您引用 vtable .. 那是一个实现细节。 Afaik 标准没有提到 vtable。相反,它指定了虚拟方法和覆盖应该如何工作(并且大多数/所有编译器都使用 vtable 来实现)
  • 确定它是一个实现细节,但如果我正确理解你的答案,foo() 将始终引用相同的虚函数(即A::foo),无论它是否已被重新声明为纯一个子类。所以对它的调用总是会通过 A 的 vftable 发生(在任何理智的编译器中)。我认为我们彼此之间有些交谈,但是无论如何,您的 cmets 帮助我澄清了情况。谢谢。
  • @SebastianHoffmann 等一下.. 我刚刚发现有区别;) 将更新答案
【解决方案2】:

我发现了一个需要重新声明纯虚方法的示例。 gcc 10 和 clang 11 都需要在纯虚方法用于实现后代类本身抽象时重复声明。这是一个例子:

class Transition {
  public: 
  virtual int* apply(int*p, double*e) = 0;
  virtual long* apply(double*h, double*e) = 0;
  virtual long* apply(long* l, double *e) { return l;}
};

class MTransition : public Transition {
  public:
  //virtual int* apply(int*p, double*e) = 0; // duplicate from upper class but required
  virtual long* apply(double*h, double*e) {
    int a= *(Transition::apply((int*)h, e)); // not resolved correctly without the duplicate declaration
    return a?nullptr:nullptr;
  }
};

class TransitionTest : public MTransition {
  public: 
  virtual int* apply(int*p, double*e) { return p; }
};

int main() {
  TransitionTest t;
  double /*d1=1.0,*/ d2=2.0;
  int i1=1;
  /*int *pi = t.apply(&d1, &d2); */
  int *pi = t.apply(&i1, &d2);
  return pi?(*pi):0;
}

因此,此程序不链接。我收到以下错误:

clang++ -Wall yo.cpp 
yo.cpp:19:16: warning: 'TransitionTest::apply' hides overloaded virtual function [-Woverloaded-virtual]
  virtual int* apply(int*p, double*e) { return p; }
               ^
yo.cpp:11:17: note: hidden overloaded virtual function 'MTransition::apply' declared here: type mismatch at 1st parameter ('double *' vs 'int *')
  virtual long* apply(double*h, double*e) { 
                ^
1 warning generated.
/usr/bin/ld: /tmp/yo-cb0979.o: in function `MTransition::apply(double*, double*)':
yo.cpp:(.text._ZN11MTransition5applyEPdS0_[_ZN11MTransition5applyEPdS0_]+0x27): undefined reference to `Transition::apply(int*, double*)'
clang: error: linker command failed with exit code 1 (use -v to see invocation)

如果我在 main 中尝试对apply 进行其他调用,则最糟糕的是(在这种情况下是编译错误,与警告相关联)。

为了让它工作,我需要像这样编写MTransition 类:

class MTransition : public Transition {
  public:
  virtual int* apply(int*p, double*e) = 0; // duplicate from upper class but required
  virtual long* apply(double*h, double*e) {
    int a= *(apply((int*)h, e)); // not resolved correctly without the duplicate declaration
    return a?nullptr:nullptr;
  }
};

但我没有成功在 main 中调用 t.apply(&amp;d1, &amp;d2);。在我看来,TransitionTest 中的 apply(int*p, double*e) 的定义隐藏了(如警告所述)另一个应该被继承的 apply

编辑(再次):我在c++ overloaded virtual function warning by clang? 中找到了解决方案 在派生类中重载(或部分覆盖)方法时,需要使用using Base::method 来保留其他重载的方法。

它给出:

class Transition {
  public:
  virtual int* apply(int*p, double*e) = 0;
  virtual int* apply(double*h, double*e) = 0;
  virtual int* apply(long* l, double *e) { std::cerr << "Transition::apply(long*,double*)" << std::endl; return nullptr;}
};

class MTransition : public Transition {
  public:
  using Transition::apply;
  virtual int* apply(double*h, double*e) {
    std::cerr << "MTransition::apply(double*,double*)" << std::endl;
    int a= *(apply((int*)h, e)); // not resolved correctly without the duplicate declaration
    return a?nullptr:nullptr;
  }
};

class TransitionTest : public MTransition {
  public:
  using MTransition::apply;
  virtual int* apply(int*p, double*e) { std::cerr << "TransitionTest::apply(int*,double*)" << std::endl; return p; }
};

int main() {
  TransitionTest t;
  double d1=1.0, d2=2.0;
  //int i1=1;
  int *pi = t.apply(&d1, &d2);
  //int *pi = t.apply(&i1, &d2); 
  return pi?(*pi):0;
}

【讨论】:

  • 仅供参考 - 重新声明是必要的,因为基类中的 apply(int*, double*) 是私有的。将其更改为受保护,并将其引用为 MTransition 中的 Transition::apply() 并且不再需要副本。 godbolt.org/z/bsWGK3jos
  • 我更新了我的代码。我仍然收到错误(现在在链接时)。查看编辑后的帖子。
猜你喜欢
  • 2018-02-24
  • 2019-07-28
  • 2011-09-04
  • 1970-01-01
  • 1970-01-01
  • 2021-03-09
  • 1970-01-01
  • 2013-03-27
相关资源
最近更新 更多