从派生类中删除虚函数

39

我有一个虚基类函数,在特定派生类中不应该被使用。有没有一种方法可以“删除”它?我当然可以只给它一个空的定义,但我宁愿让其尝试使用时抛出编译时错误。C++11的delete指示符似乎是我想要的,但是……

class B
{
    virtual void f();
};

class D : public B
{
    virtual void f() = delete; //Error
};

编译不通过; 至少在gcc中,明确禁止我删除具有非删除基本版本的函数。还有其他方法可以实现相同的功能吗?


1
怎么可能呢?你想在 D *d = new D(); static_cast<B *>(d)->f() 中得到编译器错误吗? - Bryan Chen
1
@BryanChen 如果将其转换为指向基类的指针,那么它应该像指向基类的指针一样行动 - 在这种情况下,D::f 仍然会被调用,所以是的。 - Matt Phillips
那种情况下,您会期望什么行为呢? - Theolodis
@Theolodis 更改了我对Bryan Chen问题的回答,一时忘记了在将指针转换为基类后,派生类函数仍然会被调用。 - Matt Phillips
我认为你误解了 delete 关键字的使用。delete 关键字不是用来从子类中删除方法的,这会违反继承规则。它应该被用来禁用特定的调用(比如复制调用或特定类型参数的调用)。 - Holt
显示剩余3条评论
6个回答

55

虽然不符合标准,但您可以使用以下两种解决方法来实现类似的行为。

第一种方法是使用using将方法的可见性更改为私有,从而防止其他人使用它。但该解决方案的问题在于,在超类指针上调用该方法不会导致编译错误。

class B
{
public:
    virtual void f();
};

class D : public B
{
private:
    using B::f;
};

到目前为止,我发现在调用 D 方法时获得编译时错误的最佳解决方案是使用继承自 false_type 的通用结构的 static_assert。只要没有人调用该方法,该结构保持未定义,static_assert 就不会失败。
然而,如果调用了该方法,则该结构被定义且其值为 false,因此 static_assert 失败。
如果没有调用该方法,但您尝试在超类指针上调用它,则 D 方法未定义,您会收到一个 undefined reference 编译错误。
template <typename T>
struct fail : std::false_type 
{
};

class B
{
public:
    virtual void f() 
    {
    }
};

class D : public B
{
public:
    template<typename T = bool>
    void
    f()
    {
        static_assert (fail<T>::value, "Do not use!");
    }
};

另一个变通方法是在使用该方法时抛出异常,但这只会在运行时出现问题。

1
不错,但是 B* d = new D(); d->f(); 将会无错误地运行(并且 B::f 会被调用),但这种情况下我希望出现一个错误。 - Matt Phillips
不,我绝不希望调用基类版本。我的回答已在您回答之后更新给Bryan Chen,我希望在这种情况下出现编译器错误。 - Matt Phillips
@MattPhillips 实际上 fail 已经在我的示例中声明和定义了 ;) 请看空的主体 ({})。 - Theolodis
2
@Zingam 如果你加入 static_assert(false);,它永远不会编译。模板结构只有在使用该方法时才会初始化。 - Theolodis
如果您使用false的static_assert,它将永远无法编译。只有在将代码放入没有人使用的单独头文件中时才能正常工作。然而,我的解决方案允许您无限制地使用B的f()方法,并且仅在您在代码中使用D的f()方法时无法编译。当然,您可以通过http://cpp.sh或类似的最小示例来证明我是错误的。 - Theolodis
显示剩余9条评论

15
标准不允许你在派生类中删除基类的任何成员,有很好的理由:
这样做会破坏继承,特别是“是一个”关系。

由于相关原因,它也不允许派生类定义在基类中已删除的函数:
钩子不再是基类契约的一部分,因此它阻止了您依赖先前不再存在的保证。 如果你想要一些技巧,你可以强制产生错误,但这将只会在链接时而不是编译时发生:
声明成员函数但不定义它(虚拟函数不能保证100%工作)
最好还要查看GCC废弃属性以获得更早的警告。__attribute__ ((deprecated))
有关详细信息和类似MS的魔术,请参见:C++标记为已弃用

好的,对于链接时错误建议加1。我不在意具体使用delete,我只想要这个功能。 - Matt Phillips
实际上,即使从未调用f,执行该操作也会产生“未定义的vtable”错误,如此处所示(http://ideone.com/JW0uKU)。 - Matt Phillips
@MattPhillips:有些编译器/链接器如果某个虚拟方法的实现不存在,它们不会报错。有时这取决于编译器选项。但是,最好的选择始终是弃用。 - Deduplicator

4
我有一个虚基类函数,特定的派生类不应该使用它。
在某些方面,这是一种矛盾。虚函数的整个目的就是提供基类提供的合同的不同实现。你试图做的是打破这个合同。C++语言旨在防止您这样做。这就是为什么它强制您在实例化对象时实现纯虚拟函数的原因。这也是为什么它不允许您删除合同的一部分的原因。
正在发生的事情是一件好事。它可能会阻止您实施不适当的设计选择。
然而:
有时候创建一个空白实现什么都不做是可以的:
void MyClass::my_virtual_function()
{
    // nothing here
}

或者是一个返回“失败”状态的空白实现:

bool MyClass::my_virtual_function()
{
    return false;
}

这完全取决于您想要做什么。也许如果您能提供更多关于您的目标的信息,其他人可以指导您朝正确的方向前进。

编辑

如果您考虑一下,为了避免为特定的派生类型调用函数,调用者需要知道它正在调用的类型。调用基类引用/指针的整个重点在于您不知道哪个派生类型将接收调用。


2
调用基类的整个目的是为了通用情况,但并非总是如此;如果总是这样,C++语言就不会包含“static_cast”和“dynamic_cast”。有时候,派生类的身份对于调用代码很重要,而这就是其中之一。 - Matt Phillips
有时候直接处理派生类确实很重要。这正是 dynamic_cast 的用途。但是,这时您不再调用基类引用/指针,因此我的陈述并不完全适用。但我希望它能回答您的问题,为什么不允许这样做。 - Galik

2
你可以在派生实现中简单地抛出异常。例如,Java集合框架就经常这样做:当对不可变集合执行更新操作时,相应的方法只会抛出UnsupportedOperationException异常。在C++中也可以这样做。
当然,这种方式只能在运行时显示函数的恶意使用,而不能在编译时检测到。但是,在使用虚方法时,由于多态性,你无法在编译时捕获此类错误。例如:
B* b = new D();
b.f();

在这里,你将一个 D 存储在一个 B* 变量中。即使有一种方法可以告诉编译器不允许在 D 上调用 f,编译器也无法在这里报告错误,因为它只看到了 B

1
不要玩花招,为什么不直接使用正确的继承呢?
class B { 
  // B is the generalization that provides common members
};

// Then you define appropriate specializations of B to derive from
class C1: public B {};
class C2: public B { 
  virtual void f() = 0;
};

class D: public C1 {
  // Inherits from B, and doesn't inherit void f()
};

0
我有一个虚基类函数,在特定的派生类中应该永远不会使用
C++11提供了一个关键字final,可以防止虚函数被覆盖。
请看:http://en.cppreference.com/w/cpp/language/final
class B
{
  virtual void f() final;
};

class D : public B
{
  // virtual void f();  // a compile-time error
  // void f() override; // a compile-time error
  void f(); // non-virtual function, it's ok
};

2
这只是确保它不被覆盖,但函数仍然被继承。 - Sridhar Thiagarajan

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