推导纯虚函数的实现

10

考虑下面的例子

#include <iostream>

struct PureVirtual {
    virtual void Function() = 0;
};

struct FunctionImpl {
    virtual void Function() {
        std::cout << "FunctionImpl::Function()" << std::endl;
    }   
};

struct NonPureVirtual : public FunctionImpl, public PureVirtual {
    using FunctionImpl::Function;
};

int main() {
    NonPureVirtual c;
    c.Function();
}

编译器 (GCC 4.9, Clang 3.5) 报错退出

test.cpp:18:20: error: variable type 'NonPureVirtual' is an abstract class
    NonPureVirtual c;
                   ^
test.cpp:4:18: note: unimplemented pure virtual method 'Function' in 'NonPureVirtual'
    virtual void Function() = 0;
                 ^

但是,如果我不从PureVirtual继承,一切都正常。这很奇怪,因为标准中 10.4.4 讲到:

一个类是抽象的,如果它包含或继承至少一个纯虚函数,该纯虚函数的最终覆盖者是纯虚函数。

他们没有说明什么是最终覆盖者,但我认为它应该是FunctionImpl::Function(),特别是当我通过using指令将其提供时。那么为什么NonPureVirtual仍然是抽象类,并且如何修复它。


3
PureVirtual 不是 FunctionImpl 的基类,因此... - Constructor
public FunctionImpl, public PureVirtual -> Function 在两个类中都可见,尤其是从 PureVirtual 中,没有虚继承,因此它是一个抽象类。猜测而已。 - DumbCoder
@Simple - 我不这么认为,或者至少 - 在许多版本的gcc中存在错误。我遇到过同样的情况,我绕过了它,但我这里没有答案。 - Kiril Kirov
2
当您从PureVirtual继承时,它无法编译的原因是您只使用using影响了FunctionImpl :: Function的范围/可访问性;您没有覆盖任何内容。 - Simple
Sutter's Mill/GOTW上有一篇很棒的文章,涉及在覆盖虚函数中使用override关键字。它还强调了基类和派生类中默认参数不同所导致的一些意想不到的陷阱。 - Rob
显示剩余7条评论
3个回答

11

FunctionImpl::FunctionPureVirtual::Function是来自不同类的不同函数。

它们各自的类型分别为void (FunctionImpl::*)()void (PureVirtual::*)()。由于PureVirtualFunctionImpl是无关的类,因此这些函数类型也是无关的。

它们恰好具有相同的名称、参数和返回类型,但由于它们是不同的,using FunctionImpl::Function行不能使该函数成为PureVirtual中那个函数的覆盖。

如果您声明了一个void (PureVirtual::*)()类型的变量,则不能将FunctionImpl::Function赋值给它。

换句话说,PureVirtual::Function的最终覆盖是在PureVirtual中原始的纯虚函数。

为了实现所需的功能,Matthieu M.的答案(使用转发调用)似乎是正确的方法。


@KirilKirov 我怀疑它可能无法修复,但我不确定。更名可能是最好的方法。 - molbdnilo
是的,这就是我在遇到这个问题时“绕过”的方法。但我仍然抱有希望,肯定有更好的解决办法 :) - Kiril Kirov
那么,我们如何区分两个函数呢?我一直认为在C++中,签名是区分两个函数的标志。这两个函数显然具有相同的签名。 - tomas789
@tomas789,我添加了一些关于函数类型的内容。 - molbdnilo
@tomas789,一个(成员)函数的签名不仅包括名称和参数类型,还包括cv限定符、ref限定符以及该函数所属类。 其他一些类从PureVirtualFunctionImpl完全不知道的事实派生出来是无关紧要的。 - jrok
显示剩余4条评论

5
您不能使用using指令,而必须使用转发调用:
struct NonPureVirtual : public FunctionImpl, public PureVirtual {
    virtual void Function() override {
        return FunctionImpl::Function();
    }
};

没错,它像预期一样工作


我认为 override 并没有改变任何东西,仍然不清楚它覆盖了哪个基本函数,或者我错了?而且我不会依赖测试来处理这种情况。 - Kiril Kirov
我不明白您在@suspectus删除的答案下的评论。我的意思是,您的回答如何解决suspectus中的歧义问题? - Kiril Kirov
3
override关键字并没有改变什么,它只是在C++11中一种好的方式,用来标记那些实际上是重载了基类函数的函数;无论有没有使用override关键字,NonPureVirtual::Function都成功地重载了FunctionImpl::FunctionPureVirtual::Function两个函数。至于模棱两可的问题:@suspectus的回答中没有模棱两可,但是它使用了复制/粘贴来复现行为,这应该避免以方便维护。 - Matthieu M.
+1,我不知道你可以同时覆盖两个函数。 - molbdnilo

2
您正在将使用声明与其不实施的内容相关联。它的作用是(来自草案n3337,7.3.3/1):
...使用声明引入一个名称到使用声明所在的声明区域中。
同一段落后面说:
如果使用声明命名了一个构造函数(3.4.3.1),则它会隐式地在类中声明一组构造函数(12.9); 否则,在使用声明中指定的名称是某个已在别处声明的实体的同义词。
在您的情况下,FunctionImpl :: Function首先在FunctionImpl类中声明,并且它是该类的成员。使用声明不会改变这个事实,因为第16段进一步说:
对于重载决议的目的,由使用声明引入派生类中的函数将被视为它们是派生类的成员。特别地,隐式的this参数应视为指向派生类而不是基类的指针。这对函数的类型没有影响,在所有其他方面,函数仍然是基类的成员。
现在转到覆盖的定义(10.3/2):
如果在类Base和类Derived中分别声明了具有相同名称、参数类型列表(8.3.5)、cv-限定符以及ref-限定符(或缺少相同)的虚拟成员函数vf,则Derived::vf也是虚拟的(无论是否如此声明)并且它将覆盖Base::vf。为方便起见,我们说任何虚拟函数都覆盖自身。
同一段落还有关于最终覆盖程序的定义。
类对象S的虚拟成员函数C :: vf是最终覆盖程序,除非S是其最派生类(1.8)的基类子对象声明或继承另一个覆盖vf的成员函数。在派生类中,如果基类子对象的虚拟成员函数具有多个最终覆盖程序,则程序无效。[...]

我认为这清楚地表明,您不能使用using声明来覆盖虚函数。可能的解决方案在Matthieu的答案中。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接