【问题标题】:Pure virtual function call interesting cases纯虚函数调用有趣案例
【发布时间】:2014-11-01 14:25:17
【问题描述】:

考虑以下代码:

#include <iostream>
using namespace std;

class A
{
  public:
   virtual void f() = 0;
   A(){f();}
};

void A::f() {
    cout<<"A"<<endl;
}

class B:public A{
 public:
    void f(){cout<<"B"<<endl;}
};
int main()
{
 B b;
}

在这种情况下,我直接从构造函数调用虚函数并得到编译器警告:
警告:从构造函数调用的抽象虚拟 'virtual void A::f()'。
但它执行时没有终止并打印 A。

如果我像这样包装函数的调用:

class A
{
  public:
   virtual void f() = 0;
   A(){g();}
   void g(){f();}
};

void A::f(){cout<<"A"<<endl;}

class B:public A{
 public:
    void f(){cout<<"B"<<endl;}
};
int main()
{
 B b;
}

编译器在编译期间不输出任何警告,但在运行时崩溃并显示以下消息:

pure virtual method called   
terminate called without active exception   
Abort

谁能解释这两种情况的行为?

【问题讨论】:

标签: c++ inheritance compilation virtual-functions pure-virtual


【解决方案1】:

§ 10.4 抽象类 [class.abstract] / p6

可以从抽象类的构造函数(或析构函数)调用成员函数;对纯虚拟函数直接或间接为正在创建(或销毁)的对象进行虚拟调用(10.3)的效​​果构造函数(或析构函数)未定义

简而言之:为从构造函数创建的对象直接或间接调用纯虚函数的效果是不确定的。

无论是直接调用还是间接调用,都不能从构造函数或析构函数调用纯虚成员函数,因为这样会导致未定义的行为

提供纯虚函数实现的唯一有用示例是从派生类调用它时:

struct A
{
    virtual void f() = 0;
};

void A::f()
{
    cout<<"A"<<endl;
}

struct B : A
{
    void f()
    {
        A::f();
        cout<<"B"<<endl;
    }
};

【讨论】:

    【解决方案2】:

    在第一种情况下,编译器恰好通过静态分派到A::f() 来节省您的时间,因为它知道A 的静态类型。但这是非常未定义的行为,这是完全正确的,您不应该这样做。

    在第二种情况下,编译器不会静态分派到A::f(),因为调用不在构造函数中,所以它必须动态分派它。不同的 ABI 处理纯虚拟调用的方式不同,但 MSVC 和 Itanium 都有一个专用的纯虚拟调用处理程序,该处理程序放置在 vtable 中以捕获这些事件。这就是产生您看到的错误消息的原因。

    【讨论】:

      【解决方案3】:

      从编译器的角度来看,如果你看一下函数 f() 是如何被调用的:

      • 案例 1:A 的 ctor 直接调用 A-ctor => f()。编译器确切地知道是这种情况并决定发出警告。
      • 案例 2:A 的 ctor 调用 A-ctor => g() => f()。从类方法之一调用 f() 是完全合法的。编译器不能说这是非法的。调用图可能来自 * => bar() => g() -> f(),这意味着对象的类型未知。这样的路径是可能的,因此需要动态调度 - 导致运行时错误。

      正如其他人指出的那样,这是未定义的用法,编译器只能在检测和警告方面做到这一点。

      【讨论】:

        【解决方案4】:

        Undefined 行为意味着编译器不必以任何特别定义的方式处理这种情况。

        在这里,您的编译器知道 A 在其构造函数中的实际类型能够内联到纯虚方法中,而不是通过 v-table 调用它。如果方法是普通虚拟而不是纯虚拟会发生这种情况,这将是定义的行为。

        虽然即使通过g() 也会出现这种行为,但编译器并未对纯虚拟f() 函数执行此操作。没必要。

        简单的道理是不要调用未定义的行为,如果你想从构造函数中调用f(),不要让它成为纯虚拟的。

        如果你想强制你的子类实现f(),不要从A的构造函数中调用它,而是给你想调用的函数一个不同的名字。最好根本不是虚拟的。

        【讨论】:

          猜你喜欢
          • 2011-08-06
          • 1970-01-01
          • 1970-01-01
          • 2012-03-20
          • 1970-01-01
          • 1970-01-01
          • 2012-01-28
          • 2014-03-03
          相关资源
          最近更新 更多