C++基类如何在运行时确定方法是否被覆盖?

6
下面的示例方法旨在检测它是否在派生类中被覆盖。从MSVC得到的错误暗示尝试获取“绑定”成员的函数指针是错误的,但我看不出为什么这会成为问题(毕竟,它将在this->vtable中)。有没有非hacky的方法来修复这个代码?
class MyClass
{
public:
    typedef void (MyClass::*MethodPtr)();  

    virtual void Method()
    {
        MethodPtr a = &MyClass::Method; // legal
        MethodPtr b = &Method;  // <<< error C2276: ‘&’ : illegal operation on bound member function expression

        if (a == b)     // this method has not been overridden?
            throw “Not overridden”;
    }
};
3个回答

3

除了纯虚方法之外,没有办法确定一个方法是否被覆盖:它们必须在派生类中被覆盖并且是非纯虚的。(否则,您无法实例化对象,因为类型仍然是“抽象的”。)

struct A {
  virtual ~A() {} // abstract bases should have a virtual dtor
  virtual void f() = 0; // must be overridden
}

如果派生类可能或必须调用纯虚方法,则仍可以提供其定义:

void A::f() {}

根据你的评论,“如果该方法没有被覆盖,那么尝试映射调用到另一个方法是安全的。”
struct Base {
  void method() {
    do_method();
  }

private:
  virtual void do_method() {
    call_legacy_method_instead();
  }
};

struct Legacy : Base {
};

struct NonLegacy : Base {
private:
  virtual void do_method() {
    my_own_thing();
  }
};

现在,任何派生类都可以提供自己的行为,或者如果他们没有提供,则使用遗留行为作为后备。do_method虚函数是私有的,因为派生类不能调用它。(NonLegacy可以根据需要将其设置为protected或public,但默认与其基类具有相同的可访问性是一个好主意。)


不幸的是,在基类和实现新旧方法的类之间存在许多中间类。我希望能尽可能少地更改代码,但现在看来这种可能性越来越小了。 - intepid

1

你实际上可以找到这个答案。我们遇到了同样的问题,但我们发现了一个技巧来解决它。

#include<iostream>
#include<cstdio>
#include<stdint.h>

using namespace std;

class A {
public:
    virtual void hi(int i) {}
    virtual void an(int i) {}
};

class B : public A {
public:
    void hi(int i) {
        cout << i << " Hello World!" << endl;
    }
};

我们有两个类A和B,B使用A作为基类。
以下函数可用于测试B是否覆盖了A中的某些内容。
int function_address(void *obj, int n) {
    int *vptr = *(int **)&obj;
    uintptr_t vtbl = (uintptr_t)*vptr;

    // It should be 8 for 64-bit, 4 for 32-bit 
    for (int i=0; i<n; i++) vtbl+=8;

    uintptr_t p = (uintptr_t) vtbl;
    return *reinterpret_cast<int*>(p);
}

bool overridden(void *base, void* super, int n) {
    return (function_address(super, n) != function_address(base, n));
}

int n是作为方法存储在虚表中的编号,通常它是你定义方法的顺序。

int main() {
    A *a = new A();
    A *b = new B();

    for (int i=0; i<2; i++) {
        if (overridden(a, b, i)) {
            cout << "Function " << i << " is overridden" << endl;
        }
    }

    return 0;
}

输出结果将会是:

函数0被覆盖

编辑:我们获取每个类实例的虚函数表指针,然后将其与方法的指针进行比较。每当一个函数被覆盖时,超级对象的值将会不同。


1
我非常兴奋地看到了这个解决方案,但是在“return *reinterpret_cast<int*>(p);”期间出现了“访问冲突”的错误。有人遇到过同样的问题吗? - Yann
那个reinterpret_cast违反了TBAA。它也不是很有意义,因为这里想要的是整数转换,而不是指针转换。 - Chris Kitching

0

没有可移植的方法来实现这一点。如果您的意图是拥有一个不是纯虚函数,但需要为每个调用它的类进行重写的方法,您可以将assert(false)语句插入基类方法实现中。


为什么你会有一个非虚方法,派生类必须重写它?如果它是虚的但不是纯的,与使它成为纯的相比,有什么优势? - Roger Pate
在某些情况下,您可能需要几种不同的派生类。对于属于某种类型的类,只会调用一部分方法。这听起来肯定不像是完美的面向对象设计,但在现实生活中有时确实需要这样做。 - sharptooth
我的问题源于需要支持两种不同接口方法之间的映射,一种是新的,另一种是遗留的。如果该方法未被覆盖,则意味着可以尝试将调用映射到其他方法中。有些对象实现了新方法,而其他对象则没有,同样地,一些调用代码会调用新方法,而其他调用遗留方法(很混乱,我知道,这是逐步重构的一部分)。如果没有这种能力,那么只能沿着一个方向进行映射。 - intepid
因此,在包装接口中使用非纯虚方法来调用遗留方法。如果你需要覆盖它并且仍然需要遗留调用,请在重写中显式调用基本方法。 - Georg Fritzsche
如果一开始我们使用非虚拟的包装方法,那就轻而易举了,但不幸的是这些方法被一个相当大的类层次结构中的多个位置覆盖。 - intepid

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