【问题标题】:How can a C++ base class determine at runtime if a method has been overridden?C++ 基类如何在运行时确定方法是否已被覆盖?
【发布时间】:2009-11-26 06:50:10
【问题描述】:

下面的示例方法旨在检测它是否已在派生类中被覆盖。我从 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”;
    }
};

【问题讨论】:

    标签: c++ virtual-functions


    【解决方案1】:

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

    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 virtual 是私有的,因为派生类不能调用它。 (NonLegacy 可能会酌情将其设为受保护或公开,但默认使用与其基类相同的可访问性是个好主意。)

    【讨论】:

    • 不幸的是,在基类和可以实现新方法或旧方法的类之间有许多中间类。我希望以此作为一种尽可能少更改代码的方法,但现在看起来不太可能了。
    【解决方案2】:

    您实际上可以找到这一点。我们遇到了同样的问题,我们找到了一个黑客来做到这一点。

    #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;
        }
    };
    

    我们有两个类ABB 使用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 是方法的编号,因为它们存储在 vtable 中。通常,这是您定义方法的顺序。

    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 被覆盖

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

    【讨论】:

      【解决方案3】:

      没有可移植的方式来做到这一点。如果您的意图是拥有一个不是纯虚拟的方法,但需要为每个将被调用的类重写,您只需将 assert( false ) 语句插入基类方法实现即可。

      【讨论】:

      • 为什么会有派生类必须重写的非虚拟方法?如果它是虚拟的而不是纯粹的,这比让它变得纯粹有什么优势?
      • 在某些情况下,您可能需要几种派生类。对于属于某种类型的类,只会调用方法的子集。这当然听起来像是不完美的 OO 设计,但您有时在现实生活中需要它。
      • 我的问题来自需要支持两种不同接口方法之间的映射,一种是新的,一种是旧的。如果该方法没有被覆盖,则意味着尝试将调用映射到另一个方法是安全的。一些对象实现了新方法,而另一些则没有,类似地,一些调用代码会调用新方法和一些遗留方法(我知道这很混乱,是逐步重构的一部分)。没有这种能力,它只能被映射到一个方向。
      • 所以在包装接口中使用非纯虚方法调用旧的。如果您覆盖它并且仍然需要 legacy-call,请在覆盖中显式调用基本方法。
      • 如果我们从一开始就使用非虚拟包装方法,这将是小菜一碟,但不幸的是,这些方法在相当大的类层次结构中的多个位置被覆盖。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-06-01
      • 2012-08-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多