【问题标题】:Ways to detect whether a C++ virtual function has been redefined in a derived class检测派生类中是否已重新定义 C++ 虚函数的方法
【发布时间】:2011-06-12 02:14:50
【问题描述】:

简而言之: 从指向派生类实例的 C++ 基类指针,如何在运行时确定非纯虚函数(在基类)是否在派生类中重新实现?

上下文: 我正在编写一个 C++ 库来解决某些类别的数学方程。该库提供了一个带有多个虚函数的Equation 类,库用户将其用作他们希望求解的特定方程的基类。该库还提供了一个Solver 类,它将Equation * 作为构造函数参数。然后用户编写代码如下:

class MyEquation : public Equation
{ ... } // equation definition here

int main()
{
  MyEquation myEqn;
  Solver solver(&myEqn);
  solver.Solve();
}

如果Equation 中的某些虚函数组合未在派生方程类中重新定义,则可以省略Solver 对象运行的算法中某些计算量大的部分。因此,我想知道,在Solver 的构造函数中,哪些函数已被重新定义,哪些函数将运行Equation 中的默认实现。

  • 我想让这对库的用户透明,所以我不是在寻找解决方案,例如,用户在派生方程的构造函数中设置一些标志,指定哪些函数已被重新定义。

  • 一种可能的解决方案是Equation 中的虚函数的默认实现在Equation 类中设置私有标志;然后Solver 类的构造函数可以清除这个标志,运行虚函数,并检查标志值以查看Equation 中的实现是否已被调用。不过我想避免这种情况,因为每次执行虚函数时简单地设置标志会大大降低算法的速度(这些虚函数的执行对程序的运行时间有很大贡献,默认实现只是返回一个常数)。

【问题讨论】:

  • 你确定设置一个标志有什么不同吗?我想它与调用虚函数调用的开销相比完全可以忽略不计。
  • 这似乎高度依赖于架构。对于这个程序,在作为目标机器的 64 位 Xeon 服务器上,虚拟函数调用的开销似乎几乎为零。然而,在我的家用计算机(32 位 Athlon)上,正如您所怀疑的那样,函数调用开销远大于设置标志的时间。
  • 首先是否需要确定虚拟是否具有覆盖让您觉得完全正交于多态性?它确实对我有用。换句话说,如果你需要这样做,你的设计就会被破坏。
  • @John:优化经常与好的设计直接对立。对于大多数程序,平衡强烈支持良好的设计(可能以边际减速为代价)。对于像这样一个
  • @Chris:我明白了。但是没有程序需要性能如此糟糕以至于编写不正确代码是可以的。你根本无法做你想做的事。至少不是直接的。

标签: c++ virtual-functions


【解决方案1】:

您不能以可移植的方式检查虚函数的覆盖。

您需要将知识带到Solver,最好通过类型而不是运行时标志。

一种(基于类型的)方法是通过dynamic_cast 检查接口是否存在。

一种可能更好的方法是提供求解函数的重载,在您的情况下是 Solver 构造函数。

如果您对问题提供更具体的描述,可能会给出更好的建议。它确实提醒了以下典型情况:某人 (1) 需要解决某个问题 P,(2) 设想技术方法 X 作为 P 的解决方案,(3) 发现 X 没有解决问题,以及 (4) 询问如何使 X 适用于对 P 的模糊描述,甚至适用于一些不相关的问题 Q。原始问题 P 的细节通常会提出比 X 更好的解决方案,而使 X 起作用的问题与解决 P 无关。

【讨论】:

  • 谢谢。我大体上同意你的最后一段——在我看来,在某些情况下,对 X 的回答(说“不,因为......”)对 SO 更有用(并且可能对这个问题更有用) -asker, long-term) 而不是 P 的答案。
  • 您能否在此问题上提供超出您自己权限的信息(即支持您的声明您不能使用一些标准或其他来源检查虚拟功能是否可移植)?
  • 现在是 2019 年,但我也有同样的问题。我可以告诉你为什么我需要它:我有一个断言来检查某个指针是否不是 nullptr,因为如果是,那么用户在调用 start_input_device 之前忘记调用 set_sink 函数,并得到“警告“用一个很好的断言而不是在某个地方崩溃。但是,当某些虚函数 (read_from_fd) 被覆盖时,可以不调用 set_sink(有时)。所以,我的断言必须看起来像:assert(m_ibuffer || &read_from_fd != &InputDevice::read_from_fd),其中第一个 read_from_fd 是实际的函数
  • 被调用,第二个是要求m_ibuffer 为非零的那个。现在的问题是我得到错误:ISO C++ forbids taking the address of an unqualified or parenthesized non-static member function to form a pointer to member function. Say ‘&evio::InputDevice::read_from_fd’ 但是我显然不能添加第一个read_from_fd 的限定,因为它是未知的!
  • 您可能需要检查基于策略的设计模式,而不是当前基于继承的Equation 类。在Solver 中,您应该能够根据某些函数的可用性 选择哪种方法,而不是它们是否被覆盖。
【解决方案2】:

在只找到链接到 gcc 的 PMF 扩展的结果后,我认为必须有一个正确的方法。

我找到了一个没有任何技巧的解决方案,并且至少经过测试可以在 gcc 和 llvm 上工作:

#include <iostream>
#include <typeinfo>

struct A { virtual void Foo() {} };
struct B : public A { void Foo() {} };
struct C : public A { };

int main() {
    std::cout << int(typeid(&A::Foo) == typeid(&A::Foo)) << std::endl;
    std::cout << int(typeid(&A::Foo) == typeid(&B::Foo)) << std::endl;
    std::cout << int(typeid(&A::Foo) == typeid(&C::Foo)) << std::endl;
    return 0;
}

http://ideone.com/xcQOT6

PS:我实际上在 CEventClient 系统中使用它。因此,您从 CEventClient 派生您的类,如果它覆盖事件方法,它将自动“链接”该事件。

【讨论】:

  • 除了比较typeids,还可以使用std::is_same&lt;decltype(&amp;A::Foo),decltype(&amp;B::Foo)&gt;::value。不幸的是,它仍然是编译时检查,而不是 OP 要求的运行时检查。
  • 也适用于 MSVC。
  • 投了反对票,因为这根本不能回答问题。问题是如何检测Foo() 是否已被覆盖,仅给出一个指向基址A 的指针(并且不知道BC 类)。
【解决方案3】:

为了将来参考,事实证明 GCC 提供了这个扩展:http://gcc.gnu.org/onlinedocs/gcc/Bound-member-functions.html,它允许检查方法是否被覆盖

(void*)(obj.*(&Interface::method)) != (void*)(&Interface::method)

ICC 官方支持这个扩展,clang 的文档没有提到它,但是代码可以工作,甚至可以在没有警告的情况下编译。

不过,MSVC 不支持这个,所以就是这样。

此外,如果您链​​接到实现已更改的不同版本的库,它似乎不适用于单独库中标头(即内联)中定义的方法。如果我正确解释标准,这是未定义的行为(更改实现)但如果实现保持不变,那么地址也应该是唯一的。所以不要使用内联方法这样做。

【讨论】:

  • 谢谢,太好了!我遇到了类似的问题,我提供了一个空的默认方法实现,如果我知道该方法没有被覆盖,可以跳过某些代码。如果这种优化需要 GCC 扩展,那对我来说很好。顺便说一句,转换为 void* 对我不起作用,GCC 只接受转换为正确的函数类型(使用 Interface* 作为第一个参数,如您链接到的文档中所述)。
  • 有人能解释一下这比仅仅比较函数指针更正确吗?即&amp;obj.method != &amp;Interface::method,不需要任何扩展?
  • 对我来说,此代码无法使用 CLANG 版本 6.0.0 进行编译,并出现以下错误消息:reference to non-static member function must be called。 @galinette:将演员表留给void* 会让事情变得更糟,因为没有演员表,GCC 也拒绝编译这段代码。
【解决方案4】:

如何在第一次使用时获取指向基类函数的指针并将其与实际函数进行比较

   class Base { virtual int foo(); }
   class Derived : public Base { virtual int foo(); }

   bool Xxx::checkOverride()
   {
       int (Base::*fpA)();
       int (Base::*fpb)();

       fpA = &Base::foo;
       fpB = &Derived::foo;

       return (fpA != fpB);
   }

【讨论】:

  • 您的函数 checkOverride() 是静态的:它不检查给定对象的 foo() 方法是否等于 Base::foo()。相反,它只检查硬编码的派生类。
【解决方案5】:

也许这会有所帮助。它本身并不能回答您最初的问题,但您可以扩充您的基类(此处为 Foo)以使用某个接口(如果已提供)或使用默认方法。

#include <iostream>

struct X {
    virtual void x () = 0;
};

struct Y {
    virtual void y () = 0;
};


struct Foo {
    virtual ~ Foo () {}

    bool does_x () {
        return NULL != dynamic_cast <X*> (this);
    }

    bool does_y () {
        return NULL != dynamic_cast <Y*> (this);
    }

    void do_x () {
        dynamic_cast <X&> (*this) .x ();
    }

    void do_y () {
        dynamic_cast <Y&> (*this) .y ();
    }
};

struct foo_x : public Foo, public X {
    void x () {
        std :: cout << __PRETTY_FUNCTION__ << std :: endl;
    }
};


struct foo_y : public Foo, public Y {
    void y () {
        std :: cout << __PRETTY_FUNCTION__ << std :: endl;
    }
};

struct foo_xy : public Foo, public X, public Y {
    void x () {
        std :: cout << __PRETTY_FUNCTION__ << std :: endl;
    }

    void y () {
        std :: cout << __PRETTY_FUNCTION__ << std :: endl;
    }
};

void test (Foo & f)
{
    std :: cout << &f << " "
        << "{"
        << "X:" << f .does_x () << ", "
        << "Y:" << f .does_y ()
        << "}\n";

    if (f .does_x ())
        f .do_x ();

    if (f .does_y ())
        f .do_y ();
}

int main ()
{
    foo_x x;
    foo_y y;
    foo_xy xy;
    test (x);
    test (y);
    test (xy);
}

【讨论】:

    【解决方案6】:

    我不知道如何进行这种检测,但是您是否考虑过使用静态多态性来代替?如果 Equation 的每个虚拟方法都替换为具有默认值的模板“策略”,则可以在编译时进行优化。

    //default implementation of a virtual method turned into a functor
    //which optionally takes a reference to equation
    class DefaultFunctor1
    {
    public:
        //...
        double operator()(double x) { return log(x); }
    };
    template<class F1 = DefaultFunctor1>
    class Equation
    {
    public:
        typedef F1 Functor1;
        //...
    private:
        F1 f1;
    };
    class Solver
    {
    public:
        //....
        template<class Equation>
        void solve(Equation& eq)
        {
            Loki::Int2Type<
                            Loki::IsSameType<Equation::Functor1, DefaultFunctor1>::value
                           > selector;
            //choose at compile time appropriate implementation among overloads of doSolve
            doSolve(selector);
        }
    private:
        //.... overloads of doSolve here
    };
    int main()
    {
        Equation<> eq;
        Solver solver;
        solver.solve(eq); //calls optimized version
        Equation<MyFunctor> my_eq;
        solver.solve(my_eq); //calls generic version
        return 0;
    }
    

    【讨论】:

      【解决方案7】:

      如果你不使用虚拟方法呢?

      此时我想到了一个基于模板的解决方案。使用模板来检测给定类是否具有具有特定签名的方法是可能的(尽管不是那么容易)。一旦确定,您可以在编译时在轻量级方案和重型方案之间切换。

      请注意,我不建议将所有代码都模板化,模板化代码只会覆盖解决方案的Template(设计模式)部分,而繁重的工作可以通过常规函数来完成以减少依赖关系.

      此外,这对客户端来说是完全透明的,前提是您不更改方法的签名。

      【讨论】:

        【解决方案8】:

        尽管这在某种程度上是可能的,但我建议不要这样做。你是:

        • 违反 OOP 原则。 如果给您一个通用类/接口,则不应检查实现细节。这增加了类之间的依赖关系,与 OOP 的设计目的完全相反。
        • 重复代码。考虑到您使用的常量会由 Equation 类返回。
        • 混淆代码。许多带有类型检查的条件会使你的程序看起来很丑。
        • 可能进行了过早的优化。 执行虚函数调用和执行条件之间几乎没有速度差异。你有没有运行你的分析器来检查它是否是虚拟功能的瓶颈?在设计良好的应用程序/库中几乎不会出现这种情况。

        【讨论】:

        • -[你有没有运行你的分析器来检查是否是虚函数是瓶颈]- 是的。
        • 我想解决一个潜在的误解。 OP并不是说虚函数调用(即指针间接)是性能损失。他说,如果他知道派生类正在使用基类的默认实现,他可以省略运行昂贵的算法。
        【解决方案9】:

        无法按照您尝试的方式完成。如果没有明确告知,基类的函数无法知道它们实现的虚函数是否已被覆盖。

        看来您需要回到绘图板上。不完全确定我将如何解决您的设计问题,但我知道您现在尝试的方法根本行不通。

        【讨论】:

          猜你喜欢
          • 2015-08-04
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-09-08
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多