Output Streaming for Hierarchies
Implement polymorphic output by combining virtual functions with operator<<.
What Is Polymorphic Printing?
A hierarchy earns its keep when code that holds a base reference still gets derived behaviour. Virtual member functions deliver exactly that, and by now the pattern is routine. Stream output is the one place where the machinery seems to stop working: std::cout << device picks its function the same way any other overloaded call does, which is from the type written in the source, not from the object that happens to be there at run time.
Polymorphic printing is the arrangement that restores the missing dispatch. It is two cooperating pieces, and the whole lesson follows from the fact that they are chosen at different times:
| Piece | What it is | When it is chosen |
|---|---|---|
operator<< |
one non-member function taking a const reference to the base class |
build time, from the static type of the expression |
| the hook | a virtual member function that the operator calls | run time, from the dynamic type of the object |
The operator never varies. The hook does all the varying. Neither piece is unusual on its own, and the combination costs about five lines.
Overload resolution reads the static type
Start with the arrangement most people reach for first: give every class in the hierarchy its own operator<<. It looks like an override, and for objects named directly it even behaves like one, but no part of it is virtual. The program below prints the same object twice and gets two different answers.
#include <iostream>
class Sensor
{
private:
int m_channel{};
public:
explicit Sensor(int channel)
: m_channel{ channel }
{
}
virtual ~Sensor() = default;
int channel() const { return m_channel; }
friend std::ostream& operator<<(std::ostream& stream, const Sensor& device)
{
stream << "Sensor on channel " << device.channel();
return stream;
}
};
class ThermalSensor : public Sensor
{
public:
explicit ThermalSensor(int channel)
: Sensor{ channel }
{
}
friend std::ostream& operator<<(std::ostream& stream, const ThermalSensor& device)
{
stream << "ThermalSensor on channel " << device.channel();
return stream;
}
};
int main()
{
ThermalSensor probe{ 7 };
Sensor& wired{ probe };
std::cout << probe << '\n';
std::cout << wired << '\n';
return 0;
}
Output:
ThermalSensor on channel 7
Sensor on channel 7
There is only one object in this program. probe and wired name the same ThermalSensor, so the second line is wrong about what it is describing.
The reason is overload resolution. probe has static type ThermalSensor, so the compiler picks the operator whose parameter is const ThermalSensor&. wired has static type Sensor&, so the compiler picks the operator whose parameter is const Sensor& and never looks any further. Both decisions were made while the program was being compiled, when the dynamic type of the object was not available to consult. Anything you write inside a second operator<< is therefore unreachable through a base reference, which is the form every polymorphic container, parameter, and return value takes.
Why the operator cannot be virtual itself
The obvious repair is to mark operator<< virtual and let the usual dispatch take over. C++ blocks that repair twice over, for two independent reasons.
| Obstacle | Why it applies here |
|---|---|
| Only member functions can be virtual | operator<< is written as a non-member (usually a friend) because its left operand must be the stream, and a member operator would put the class on the left instead. A non-member has no class to be virtual in. |
| An override must match the signature | Even inside a class, Sensor::operator<< would take a Sensor parameter and ThermalSensor::operator<< a ThermalSensor parameter. Different parameter types make the second one an unrelated overload, not an override, so it would never be reached through the base. |
Notice that the second obstacle survives the first: fixing the non-member problem would not help, because the two functions still differ in the parameter that is supposed to vary. Any solution has to leave operator<< alone and put the variation somewhere a real override can live.
One operator, one virtual hook
That somewhere is an ordinary virtual member function. Write a single operator<< for the base class, and have it ask the object for its own contribution to the output. The simplest hook returns a std::string.
#include <iostream>
#include <string>
class Sensor
{
private:
int m_channel{};
public:
explicit Sensor(int channel)
: m_channel{ channel }
{
}
virtual ~Sensor() = default;
int channel() const { return m_channel; }
virtual std::string label() const { return "Sensor"; }
friend std::ostream& operator<<(std::ostream& stream, const Sensor& device)
{
stream << device.label() << " on channel " << device.channel();
return stream;
}
};
class ThermalSensor : public Sensor
{
public:
explicit ThermalSensor(int channel)
: Sensor{ channel }
{
}
std::string label() const override { return "ThermalSensor"; }
};
int main()
{
Sensor plain{ 12 };
ThermalSensor probe{ 7 };
Sensor& wired{ probe };
std::cout << plain << '\n';
std::cout << probe << '\n';
std::cout << wired << '\n';
return 0;
}
Output:
Sensor on channel 12
ThermalSensor on channel 7
ThermalSensor on channel 7
ThermalSensor has no operator<< of its own now, and it does not need one. Each of the three statements takes a slightly different route to the same function:
std::cout << plainmatches the only candidate exactly. Inside it,devicerefers to aSensor, sodevice.label()dispatches toSensor::label().std::cout << probefinds no operator taking aThermalSensor, so the compiler applies the implicit conversion fromThermalSensor&toSensor&and calls the base operator anyway. The reference now denotes aThermalSensor, sodevice.label()dispatches toThermalSensor::label().std::cout << wiredmatches the base operator directly, and dispatches toThermalSensor::label()for the same reason.
The last two lines are the point. The static type of the expression decides which operator runs, and every path funnels into the same one; the dynamic type of the object decides which label() runs, and that is where the derived class gets its say. Add ten more classes to the hierarchy and the operator does not change: one override each is the entire cost.
The hook is an ordinary member function, so its name is yours to pick.
label, describe, and print are all common. Nothing in the language ties operator<< to a particular helper name.
Two shapes for the hook
Returning a string is not the only option, and it is the more limited one. The alternative takes the stream as a parameter and writes to it directly.
Hook returns std::string |
Hook takes std::ostream& |
|
|---|---|---|
| Signature | virtual std::string label() const |
virtual std::ostream& writeTo(std::ostream& stream) const |
| Who writes to the stream | operator<< |
the hook |
| Building the text | assemble a whole string first | write pieces as they are produced |
Member with its own operator<< |
needs a conversion to string first | streams straight through |
Stream state (std::setw, flags) |
not reachable | reachable |
| Cost | one string allocated per call | none |
The first shape reads well when the output is a short fixed name. The second is what you want as soon as a class has to print something it does not know how to spell itself.
Handing the stream to the hook
Here the derived class holds a Calibration member, and Calibration has its own operator<<. With the stream in hand, the hook simply forwards to it.
#include <iostream>
struct Calibration
{
double offset{};
int revision{};
friend std::ostream& operator<<(std::ostream& stream, const Calibration& setup)
{
stream << "rev " << setup.revision << ", offset " << setup.offset;
return stream;
}
};
class Sensor
{
private:
int m_channel{};
public:
explicit Sensor(int channel)
: m_channel{ channel }
{
}
virtual ~Sensor() = default;
int channel() const { return m_channel; }
virtual std::ostream& writeTo(std::ostream& stream) const
{
stream << "Sensor on channel " << m_channel;
return stream;
}
friend std::ostream& operator<<(std::ostream& stream, const Sensor& device)
{
return device.writeTo(stream);
}
};
class ThermalSensor : public Sensor
{
private:
Calibration m_setup{};
public:
ThermalSensor(int channel, const Calibration& setup)
: Sensor{ channel }
, m_setup{ setup }
{
}
std::ostream& writeTo(std::ostream& stream) const override
{
stream << "ThermalSensor on channel " << channel() << " (" << m_setup << ')';
return stream;
}
};
int main()
{
Sensor plain{ 12 };
ThermalSensor probe{ 7, Calibration{ 0.25, 3 } };
Sensor& wired{ probe };
std::cout << plain << '\n';
std::cout << probe << '\n';
std::cout << wired << '\n';
return 0;
}
Output:
Sensor on channel 12
ThermalSensor on channel 7 (rev 3, offset 0.25)
ThermalSensor on channel 7 (rev 3, offset 0.25)
operator<< has shrunk to a single forwarding line and now prints nothing itself. ThermalSensor::writeTo uses the stream twice: once for its own text, and once to invoke Calibration's operator on m_setup. Reaching that second use through a string-returning hook would have meant building the nested text by hand.
Returning the stream from the hook is what lets operator<< end with return device.writeTo(stream);. A void hook works just as well if the operator returns stream itself.
Keeping the hook private to the hierarchy
The hook exists to serve operator<<, so callers have no business invoking it directly. Since operator<< is a friend of the base, it can still reach a hook declared protected, and access to an override is checked against the static type, so the derived class may keep its version protected or private too.
Marking the base hook pure virtual goes one step further: it makes Sensor abstract and turns a forgotten override into a compile error rather than a line of base-class text appearing in the log.
#include <iostream>
#include <memory>
#include <vector>
class Sensor
{
private:
int m_channel{};
protected:
virtual std::ostream& writeTo(std::ostream& stream) const = 0;
public:
explicit Sensor(int channel)
: m_channel{ channel }
{
}
virtual ~Sensor() = default;
int channel() const { return m_channel; }
friend std::ostream& operator<<(std::ostream& stream, const Sensor& device)
{
return device.writeTo(stream);
}
};
class ThermalSensor : public Sensor
{
protected:
std::ostream& writeTo(std::ostream& stream) const override
{
stream << "ThermalSensor on channel " << channel();
return stream;
}
public:
explicit ThermalSensor(int channel)
: Sensor{ channel }
{
}
};
class PressureSensor : public Sensor
{
private:
double m_fullScale{};
protected:
std::ostream& writeTo(std::ostream& stream) const override
{
stream << "PressureSensor on channel " << channel() << " up to " << m_fullScale << " bar";
return stream;
}
public:
PressureSensor(int channel, double fullScale)
: Sensor{ channel }
, m_fullScale{ fullScale }
{
}
};
int main()
{
std::vector<std::unique_ptr<Sensor>> rack{};
rack.push_back(std::make_unique<ThermalSensor>(7));
rack.push_back(std::make_unique<PressureSensor>(12, 6.5));
for (const auto& device : rack)
std::cout << *device << '\n';
return 0;
}
Output:
ThermalSensor on channel 7
PressureSensor on channel 12 up to 6.5 bar
The loop body is the payoff. It knows nothing about either concrete class, *device has static type Sensor, and each iteration still prints the right text.
Give a hierarchy exactly one
operator<<, declared for the base and taking a const reference, and put every type-specific decision in a virtual hook. Prefer the hook that takes std::ostream&, since it costs nothing and keeps composition open.
Adding a second
operator<< for a derived class after the hook is in place is worse than useless. Direct uses of that class quietly switch to the new operator while every base reference keeps using the hook, so the same object prints two different strings depending on how the calling code happens to spell its type.
Summary
operator<< is not a member function, so it cannot be virtual, and even as a member it could not be an override because base and derived versions differ in the parameter that matters. Overload resolution therefore picks it from the static type of the expression, which is why one operator<< per class prints base text whenever the object is reached through a base reference.
The fix keeps operator<< non-virtual and single. Declare one for the base, and have it call a virtual member function that each derived class overrides. The operator is selected at compile time; the hook is dispatched at run time; between them the output follows the real type of the object.
Two hook shapes are in common use. A hook returning std::string is the simplest, and the operator prints whatever string comes back. A hook taking std::ostream& writes to the stream directly, which is what you need when the class must print a member that has its own operator<<, or when it wants control over stream state.
Either way, derived classes never need an operator<< of their own. A derived object passed by value or by reference converts implicitly to the base parameter, the base operator runs, and the virtual call inside it lands on the derived override. That is what makes a std::vector of base pointers print correctly with one unremarkable loop.
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Output Streaming for Hierarchies - Quiz
Test your understanding of the lesson.
Practice Exercises
Printing Inherited Classes
Implement operator<< for printing polymorphic classes. Learn the virtual function helper pattern to enable proper printing of derived classes through base class references.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!