使用operator打印继承体系中的类

请先观察下面这段使用了虚函数的程序:

#include <iostream>

class Base
{
public:
    virtual void print() const { std::cout << "Base"; }
};

class Derived : public Base
{
public:
    void print() const override { std::cout << "Derived"; }
};

int main()
{
    Derived d{};
    Base& b{ d };
    b.print(); // 将调用 Derived::print()

    return 0;
}

至此,你已应熟悉:由于 b 引用的是 Derived 对象,Base::print() 为虚函数且 Derived::print() 为覆盖,因此 b.print() 会调用 Derived::print()

虽然通过成员函数输出尚可接受,但这种风格与 std::cout 并不协调:

#include <iostream>

int main()
{
    Derived d{};
    Base& b{ d };

    std::cout << "b is a ";
    b.print(); // 写法凌乱,必须中断输出语句
    std::cout << '\n';

    return 0;
}

本课将展示如何在继承体系中正确重载 operator<<,使我们能够像下面这样自然地使用:

std::cout << "b is a " << b << '\n'; // 简洁得多

operator« 面临的挑战

先按“常规方式”重载 operator<<

#include <iostream>

class Base
{
public:
    virtual void print() const { std::cout << "Base"; }

    friend std::ostream& operator<<(std::ostream& out, const Base& b)
    {
        out << "Base";
        return out;
    }
};

class Derived : public Base
{
public:
    void print() const override { std::cout << "Derived"; }

    friend std::ostream& operator<<(std::ostream& out, const Derived& d)
    {
        out << "Derived";
        return out;
    }
};

int main()
{
    Base b{};
    std::cout << b << '\n';

    Derived d{};
    std::cout << d << '\n';

    return 0;
}

由于此处无需虚决议,程序按预期输出:

Base
Derived

现在把 main() 改为:

int main()
{
    Derived d{};
    Base& bref{ d };
    std::cout << bref << '\n';

    return 0;
}

输出却为:

Base

这通常并非我们想要的结果。原因在于:我们为 Base 编写的 operator<< 并非虚函数,因此 std::cout << bref 总是调用处理 Base 的版本。


能否把 operator« 设为虚函数?

若问题出在 operator<< 不是虚函数,是否可直接将其设为虚?答案是否定的,原因有二:

  1. 只有成员函数才能声明为虚——这很合理,因为只有类才能继承,而类外函数无法被覆盖(只能重载)。我们通常把 operator<< 实现为友元,而友元并非成员函数,因此无法声明为虚。(如需回顾为何如此实现,请回看在《使用成员函数重载运算符》。)

  2. 即使可以声明为虚,Base::operator<<Derived::operator<< 的参数类型不同(前者接受 Base,后者接受 Derived),因此 Derived 版本不被视为对 Base 版本的覆盖,同样无法利用虚决议。

那么,程序员该如何做?


一种解决方案

答案出奇地简单:

  1. 照例在基类中将 operator<< 设为友元;
  2. 不让 operator<< 决定输出内容,而是让它调用一个可声明为虚的普通成员函数,由该虚函数负责输出。

在第一种实现里,我们把虚成员函数命名为 identify(),它返回 std::string,再由 Base::operator<< 打印:

#include <iostream>

class Base
{
public:
    // 重载 operator<<
    friend std::ostream& operator<<(std::ostream& out, const Base& b)
    {
        out << b.identify(); // 调用虚函数获取待输出字符串
        return out;
    }

    // 普通成员函数,可设为虚
    virtual std::string identify() const
    {
        return "Base";
    }
};

class Derived : public Base
{
public:
    // 覆盖 identify()
    std::string identify() const override
    {
        return "Derived";
    }
};

int main()
{
    Base b{};
    std::cout << b << '\n';

    Derived d{};
    std::cout << d << '\n'; // 无需显式为 Derived 重载 operator<<

    Base& bref{ d };
    std::cout << bref << '\n';

    return 0;
}

输出:

Base
Derived
Derived

深入考察其工作过程:

  • Base b:调用 operator<<(ostream&, const Base&),参数 b 引用 Base 对象,虚调用 b.identify() 解析到 Base::identify(),返回 "Base"
  • Derived d:编译器首先寻找接受 Derivedoperator<<,未找到;随后发现接受 Base& 的版本,于是对 d 执行隐式向上转型并调用之。因参数 b 实际引用 Derived,虚调用解析到 Derived::identify(),返回 "Derived"
  • Base& bref{ d }:同上,最终输出 "Derived"

注意:我们无需为每个派生类再写 operator<<!处理 Base 的版本对 Base 及其所有派生类均适用。


更灵活的解决方案

上述方案已足够,但仍有两点潜在不足:

  1. 假设输出能用一个 std::string 表示;
  2. identify() 无法拿到流对象,因而无法利用流的特性(例如打印拥有自定义 operator<< 的成员变量)。

稍作修改即可解决这两处限制:不再让虚函数返回字符串,而是把打印职责直接交给虚函数 print(),并把流对象传给它。

示例:

#include <iostream>

class Base
{
public:
    friend std::ostream& operator<<(std::ostream& out, const Base& b)
    {
        return b.print(out); // 把打印任务完全委托给虚函数
    }

    virtual std::ostream& print(std::ostream& out) const
    {
        out << "Base";
        return out;
    }
};

// 具有重载 operator<< 的结构体
struct Employee
{
    std::string name{};
    int id{};

    friend std::ostream& operator<<(std::ostream& out, const Employee& e)
    {
        out << "Employee(" << e.name << ", " << e.id << ")";
        return out;
    }
};

class Derived : public Base
{
private:
    Employee m_e{}; // Derived 拥有一个 Employee 成员

public:
    Derived(const Employee& e)
        : m_e{ e }
    {
    }

    std::ostream& print(std::ostream& out) const override
    {
        out << "Derived: ";

        // 利用流对象输出 Employee 成员
        out << m_e;

        return out;
    }
};

int main()
{
    Base b{};
    std::cout << b << '\n';

    Derived d{ Employee{"Jim", 4} };
    std::cout << d << '\n';

    Base& bref{ d };
    std::cout << bref << '\n';

    return 0;
}

输出:

Base
Derived: Employee(Jim, 4)
Derived: Employee(Jim, 4)

此版本中,Base::operator<< 自身不再打印任何内容,只负责调用虚函数 print() 并将流对象传入。

  • Base::print() 用流输出 "Base"
  • Derived::print() 用同一流先输出 "Derived: ",再调用 Employee::operator<< 打印成员 m_e,若沿用前一方案则较难优雅实现。

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"