65.9K
CodeProject 正在变化。 阅读更多。
Home

如何确保你重写了一个现有函数

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.05/5 (21投票s)

2003年10月28日

4分钟阅读

viewsIcon

138760

downloadIcon

496

一篇关于如何确保覆盖基类中现有函数, 以便在基类函数在您不知情的情况下发生更改时, 编译器会报错的文章。

引言

经验丰富的 C++ 程序员通常有个习惯, 认为只要代码能编译通过, 就一定是类型安全且正确的。尽管不总是如此, 但这种习惯是逐步养成的, 因为它 非常经常 是正确的。几天前, 我遇到了一个编译通过的程序, 我本以为它会正常工作 (因为它能编译且通过了我的测试), 但在特定情况下却并未如我所愿。我调试了一下, 找出问题所在。原来, 我给某个类的成员函数添加了一个参数, 但忘记给派生类中本应覆盖基类成员函数的那个成员函数添加相同的参数。通常情况下, 我会注意到这个问题, 因为派生类中的成员函数实际上 调用 了它本应覆盖的基类成员函数。然而, 这次我并未注意到, 因为我给新参数添加了一个 默认值。我本以为这样一来, 所有使用该成员函数的地方都不需要修改了。但正是这种期望导致了派生类中的覆盖 默默地 不再是覆盖, 这确实非常令人恼火!

我将举一个我遇到的情况的例子。假设您有以下代码

class A
{
public:  
    virtual void foo()   
    {     
        std::cout << "A::foo" << endl;   
    }
};

class B : public A
{
public:  
    virtual void foo()   
    {    std::cout << "B::foo" << endl;  
    }
};

int main()
{  
    A* a = new B;  a->foo(); // prints "B::foo".
}

您会遇到一个情况, 即 foo 需要一个额外的参数, 您将其修改如下

class A
{
public:  
    virtual void foo(int n = 3)  
    {    
        std::cout << "A::foo(" << n << ")" << endl;  
    }
};
class B : public A
{
public:  
    virtual void foo()  
    {    std::cout << "B::foo" << endl;  
    }
};

int main()
{  
    A* a = new B;  a->foo(); // prints "B::foo".
}

您很聪明, 给新参数添加了默认值, 这样所有使用 A 的代码都不需要修改。令人惊讶的是, 程序现在打印的是 "A::foo(3)" 而不是像以前那样打印 "B::foo"。这怎么可能发生呢? 显然, 您也忘了修改 B, 而不幸的是, 编译器并没有对此发出警告。它怎么会知道呢? 它不知道 Bfoo 本应覆盖 Afoo!

经过一番思考, 我决定对此做些什么。我编写了一个宏, 可以在派生类中使用, 这样可以 绝对确保 覆盖的成员函数实际上覆盖了一个具有完全相同签名的基类成员。我将其提供给社区, 以便他们不必陷入我遇到的同样的陷阱。

使用代码

使用这个宏非常简单。假设您有一个类 A, 以及一个从中派生的类 B。假设类 A 有一个成员函数 A::foo, 您想在类 B 中覆盖它。通常, 您会这样写:

class B
{  
    // ...  void foo(int,double);  
    // ...
};

然而, 当您想进行覆盖检查时, 您会这样写:

#include "override.h"
// ...
class B
{  
    // ...  
    OVERRIDE(A,void,foo,(int,double));  
    // ...
};

现在, 如果 A 中的 foo 函数的签名发生更改, 以至于 A::foo(int,double) 不再存在, 您将收到一个错误, 指出这一点。此外, 如果 B 不再是从 A 派生的, 您也会收到一个错误。

让我们在引言中给出的示例中实践一下。如果您使用了我的 OVERRIDE 宏, 您最初的代码会写成这样:

class A
{
public:  
    virtual void foo()  
    {    
        std::cout << "A::foo" << endl;  
    }
};
class B : public A
{
public:  
    OVERRIDE(A,void,foo,())  
    {    
        std::cout << "B::foo" << endl;  
    }
};

int main()
{  
    A* a = new B;  a->foo(); // prints "B::foo".
}

现在, 让我们看看如果您做出了与引言中相同的更改, 即给 Afoo 函数添加了一个参数, 会发生什么。您将得到以下代码:

class A
{
public:  
    virtual void foo(int n = 3)  
    {    
        std::cout << "A::foo(" << n << ")" << endl;  
    }
};
class B : public A
{
public:  OVERRIDE(A,void,foo,())  
         {    
             std::cout << "B::foo" << endl;  
         }
};

int main()
{  
    A* a = new B;  a->foo(); // prints "B::foo".
}

如果您没有使用 OVERRIDE, 这将顺利编译通过, 并且您只有在运行程序后才会注意到您的错误。但现在, 由于 OVERRIDE 宏, 您的编译器会生成一个错误, 提醒您注意您的错误!

为了完整起见, 我将提供宏参数的列表

  1. 我们希望覆盖的成员函数的基类
  2. 成员函数的返回类型
  3. 我们正在覆盖的成员函数的名称
  4. 成员函数的参数列表, 用括号括起来!

需要知道的是, 检查会生成一些代码, 并且即使这些代码永远不会被调用, 您也可能希望在发布版本中禁用这些检查。您可以通过在项目设置中定义 WITHOUT_OVERRIDE_CHECKING 来实现此目的。

注意事项

不幸的是, 这个宏使用的技巧并不适用于所有编译器。原因是某些编译器 (例如 Comeau 编译器) 不允许您获取受保护或私有成员函数的地址。在这些编译器上, 对于覆盖公共成员函数, 检查工作正常, 但对于受保护或私有成员函数则不行。要解决这个问题, 在使用这些编译器时, 您必须定义 WITHOUT_OVERRIDE_CHECKING, 以便不编译这些检查。

历史

  • 版本 0.1, 2003 年 10 月 24 日: 首次发布。
  • 版本 0.2, 2003 年 11 月 16 日: 大幅修改, 将宏使用移至类定义, 添加了对受保护函数的支持。
© . All rights reserved.