Effective Modern C++ 条款12:把重写函数声明为 `override`

在C++面向对象编程的世界中,类的继承和虚函数重写是核心概念之一。然而,重写虚函数时容易出现细微的错误,这些错误可能不会在编译时被发现,但却会导致程序运行时的行为不符合预期。为了避免这些问题,C++11引入了 override 关键字,帮助我们在重写函数时明确意图,并让编译器帮我们检查函数签名是否匹配。

本文将详细讲解 override 的作用、使用场景以及如何在代码中正确应用它,同时还会介绍成员函数的引用资格(reference qualifiers)这一C++11的新特性。


一、重写函数的约束条件

在C++中,派生类重写基类的虚函数需要满足以下条件:

  1. 基类函数必须是虚函数:只有被 virtual 关键字修饰的函数才能被重写。
  2. 函数名相同:除了析构函数外,基类和派生类的函数名必须完全一致。
  3. 参数列表相同:函数的参数类型、个数以及顺序必须完全一致。
  4. const 属性相同:基类和派生类的函数必须具有相同的 const 属性。
  5. 返回类型和异常规范兼容:返回类型和异常规范(exception specifications)必须是兼容的。

这些约束条件是C++98的一部分,而C++11新增了一条规则:

  1. 引用资格相同:如果基类的函数具有引用资格(如 &&&),派生类的函数也必须具有相同的引用资格。

二、override 的作用

override 是C++11引入的一个上下文关键字(contextual keyword),用于明确表明某个函数是重写基类的虚函数。它的作用主要有以下两点:

  1. 强制编译器检查函数签名:当我们在派生类的函数声明中添加 override 时,编译器会自动检查该函数的签名是否与基类的虚函数匹配。如果不匹配,编译器会直接报错。
  2. 减少潜在错误:如果没有使用 override,即使函数签名不匹配,代码仍然可能编译通过,但这些错误会在运行时表现出来,导致难以调试。

三、使用 override 的示例

假设我们有一个基类 Base,其中定义了几个虚函数:

class Base {
public:
    virtual void mf1() const;
    virtual void mf2(int x);
    virtual void mf3() &;
    virtual void mf4() const;
};

在派生类 Derived 中,我们希望重写这些函数。如果不使用 override,代码可能会出现以下错误:

class Derived : public Base {
public:
    virtual void mf1();       // 错误:缺少 const
    virtual void mf2(unsigned int x);  // 错误:参数类型不匹配
    virtual void mf3() &&;    // 错误:引用资格不匹配
    void mf4() const;         // 错误:没有 virtual 关键字
};

上述代码中,派生类的函数签名与基类的虚函数不匹配,但编译器不会报错。如果我们使用 override,编译器会直接指出这些错误:

class Derived : public Base {
public:
    virtual void mf1() override;          // 错误:缺少 const
    virtual void mf2(unsigned int x) override; // 错误:参数类型不匹配
    virtual void mf3() && override;      // 错误:引用资格不匹配
    void mf4() const override;            // 正确
};

通过 override,我们能够快速发现并修复这些错误。


四、引用资格与成员函数

C++11引入了成员函数的引用资格(reference qualifiers),允许我们限制成员函数只能由左值或右值调用。引用资格可以与 override 结合使用,确保派生类的函数与基类的函数在引用资格上一致。

引用资格的语法

成员函数的引用资格可以是 &&&,分别表示函数只能由左值或右值调用:

class Widget {
public:
    void doWork() &;    // 只能由左值调用
    void doWork() &&;   // 只能由右值调用
};

引用资格的应用场景

引用资格常用于优化资源管理。例如,我们可以为右值对象提供更高效的实现:

class Widget {
public:
    std::vector<double>& data() & {
        return values;
    }
    std::vector<double> data() && {
        return std::move(values);
    }
private:
    std::vector<double> values;
};

在上述代码中,左值调用 data() 会返回左值引用,而右值调用 data() 会返回右值,从而实现移动语义。


五、总结

在C++中,重写基类的虚函数是一项容易出错的操作。通过使用 override 关键字,我们可以让编译器帮我们检查函数签名是否匹配,从而减少潜在的错误。同时,引用资格的引入为成员函数的调用提供了更灵活的控制,使得代码更加高效和安全。

在实际开发中,我们建议遵循以下几点:

  1. 将所有重写函数声明为 override :这不仅能够帮助编译器发现潜在错误,还能让代码更具可读性。
  2. 确保函数签名完全匹配:包括参数类型、返回类型、const 属性和引用资格。
  3. 合理使用引用资格:根据实际需求,为成员函数添加 &&& 限定符,优化代码性能。

你可能感兴趣的:(Effective Modern C++ 条款12:把重写函数声明为 `override`)