Custom Input/Output with Operator Overloading
Enable your custom types to work with std::cout and std::cin.
What Is I/O Operator Overloading?
An overloaded I/O operator is an ordinary function, named operator<< or operator>>, that teaches a stream how to handle a type nobody told it about. Once that function exists, std::cout << yourValue and std::cin >> yourValue work on your type exactly the way they already work on an int.
Without one, the stream falls back on whatever conversion it can find. An unscoped enumeration converts to its integral value, so a soil moisture chart prints as a number:
#include <iostream>
enum SoilBand
{
parched,
thirsty,
moist,
soaked,
};
int main()
{
SoilBand frontBed{ moist };
std::cout << "Front bed soil is " << frontBed << '\n';
return 0;
}
Output:
Front bed soil is 2
A helper function that returns the name repairs the display, but the repair has to be repeated at every call site, and every call site has to remember what the helper is called. Overloading the operator puts that knowledge inside the type instead, so the call site is free to say only what it means.
The Mechanism: an Operator Is a Function Call
C++ lets you define overloads of most existing operators so they accept program-defined types. That is operator overloading, and it is the function overloading you have already met, wearing punctuation instead of a name.
When the compiler meets an operator in an expression and at least one operand is a program-defined type (a class type or an enumerated type), it treats the expression as a function call and runs ordinary overload resolution on it:
| Expression | What the compiler resolves it to | Who wrote that function |
|---|---|---|
total + 1, both int |
nothing, the language has a built-in + |
no one |
std::cout << 42 |
operator<<(std::ostream&, int) |
the standard library |
std::cout << frontBed |
operator<<(std::ostream&, SoilBand) |
you |
Three rules follow from that middle column:
- The function's name is the operator itself, written as
operatorfollowed by the symbol. - It takes one parameter per operand, in left-to-right order.
- At least one parameter must be a program-defined type. You cannot redefine
+for twointvalues, because there is nothing new for overload resolution to choose between.
Operator overloading gets a chapter of its own later on, covering the arithmetic, comparison, and subscript operators, along with overloads written as member functions.
The Shape of the Two Overloads
Every part of the signature is decided by a job it has to do, so it is worth reading rather than memorising:
| Part | operator<< |
operator>> |
The job it does |
|---|---|---|---|
| Left parameter | std::ostream& |
std::istream& |
std::cout is a std::ostream and std::cin is a std::istream. It is a reference because streams are never copied, and non-const because doing I/O changes the stream. |
| Right parameter | the object being printed | a non-const reference to the object being filled | Printing only reads its operand. Reading has to modify its operand, so it takes an out parameter. |
| Return type | std::ostream& |
std::istream& |
The stream is handed straight back, so the next operator in the chain has something to act on. |
The right parameter of operator<< is the one place you get a choice. An enumeration is a couple of bytes, so take it by value. A class type (the structs coming later in this chapter, and classes after them) should be taken by const reference instead, as in std::ostream& operator<<(std::ostream& stream, const Reading& reading): the reference avoids copying the object, and the const is a promise that printing it will not change it.
Pass the right operand of
operator<< by value when it is small and cheap to copy, such as an enumeration, and by const reference for class types. Never take it by non-const reference: output has no business modifying what it outputs.
Teaching operator<< a New Type
With the shape settled, the overload almost writes itself. The body converts the value into something the stream already understands, and then returns the stream:
#include <iostream>
#include <string_view>
enum SoilBand
{
parched,
thirsty,
moist,
soaked,
};
constexpr std::string_view soilBandLabel(SoilBand band)
{
switch (band)
{
case parched: return "parched";
case thirsty: return "thirsty";
case moist: return "moist";
case soaked: return "soaked";
default: return "unrecorded";
}
}
std::ostream& operator<<(std::ostream& stream, SoilBand band)
{
stream << soilBandLabel(band); // the stream already knows how to print a std::string_view
return stream; // hand the stream back so the next << has something to work on
}
int main()
{
SoilBand frontBed{ moist };
SoilBand backBed{ parched };
std::cout << "Front bed " << frontBed << ", back bed " << backBed << '\n';
return 0;
}
Output:
Front bed moist, back bed parched
The single statement in main is four calls, evaluated left to right. std::cout << "Front bed " runs a standard library overload and yields std::cout. That result becomes the left operand of << frontBed, which is the overload written above: it receives std::cout as stream and a copy of the enumerator as band, prints "moist", and returns stream. The remaining two calls repeat the pattern. Since the two lines of the body are just "print, then return the stream", they condense to return stream << soilBandLabel(band);.
The next program is broken on purpose. It changes one thing, the return type, to show what the return value is actually for:
#include <iostream>
#include <string_view>
enum SoilBand
{
parched,
thirsty,
moist,
soaked,
};
constexpr std::string_view soilBandLabel(SoilBand band)
{
switch (band)
{
case parched: return "parched";
case thirsty: return "thirsty";
case moist: return "moist";
case soaked: return "soaked";
default: return "unrecorded";
}
}
void operator<<(std::ostream& stream, SoilBand band) // returns nothing
{
stream << soilBandLabel(band);
}
int main()
{
SoilBand frontBed{ moist };
std::cout << "Front bed " << frontBed << '\n';
return 0;
}
It does not compile:
/tmp/s.cpp: In function 'int main()':
/tmp/s.cpp:33:43: error: invalid operands of types 'void' and 'char' to binary 'operator<<'
33 | std::cout << "Front bed " << frontBed << '\n';
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ^~ ~~~~
| | |
| void char
The diagnostic states the chain exactly: printing the string literal produced a stream, printing frontBed produced nothing, and nothing cannot be the left operand of the '\n' that follows. Returning the left operand is not a formality, it is the entire reason a chain of << holds together.
Why These Overloads Are Never Member Functions
An operator can also be overloaded as a member function, but only of the type on its left. In std::cout << frontBed the left operand is std::cout, whose type is std::ostream, a standard library class you cannot add members to. Even if you could, a member overload would put your object on the left and force you to write the call backwards, as frontBed << std::cout.
So the I/O operators are always overloaded as non-member functions. They are free functions that happen to be named after an operator, which is why one can be added next to your own type without touching the stream classes at all.
Write to the Stream You Were Handed
Nothing in the language forces the body to use the stream parameter. You could name std::cout directly inside it, and the program would still compile. Using the parameter is what lets a single overload serve every output stream, this one included:
#include <iostream>
#include <string_view>
enum SoilBand
{
parched,
thirsty,
moist,
soaked,
};
constexpr std::string_view soilBandLabel(SoilBand band)
{
switch (band)
{
case parched: return "parched";
case thirsty: return "thirsty";
case moist: return "moist";
case soaked: return "soaked";
default: return "unrecorded";
}
}
std::ostream& operator<<(std::ostream& stream, SoilBand band)
{
return stream << soilBandLabel(band);
}
int main()
{
SoilBand frontBed{ parched };
std::cerr << "Irrigation halted with the front bed " << frontBed << '\n';
return 0;
}
This goes to the standard error stream:
Irrigation halted with the front bed parched
An
operator<< that hardcodes std::cout compiles, and in a terminal it even looks correct, because both streams arrive in the same window. The defect only surfaces once the streams are separated, such as when normal output is redirected to a file and diagnostics are meant to stay on screen. Write to the parameter.
Teaching operator>> to Read a Value
Input is the same idea run backwards, with two differences that matter. The stream is a std::istream, and the right operand is a non-const reference, because the whole point is to modify the caller's object.
Extraction can also fail, and failure has an established protocol: the stream carries the bad news, not the return value and not an exception. Setting failbit is what the standard library's own extractions do, so callers can keep using the if (std::cin) check they already know.
| Outcome | What your operator>> does |
What the caller sees |
|---|---|---|
| The text names a value | assign through the reference, return the stream | if (stream) is true, the object holds the new value |
| The text names nothing | stream.setstate(std::ios_base::failbit), return the stream |
if (stream) is false, and the caller clears the state before reading again |
#include <iostream>
#include <limits>
#include <optional>
#include <string>
#include <string_view>
enum SoilBand
{
parched,
thirsty,
moist,
soaked,
};
constexpr std::string_view soilBandLabel(SoilBand band)
{
switch (band)
{
case parched: return "parched";
case thirsty: return "thirsty";
case moist: return "moist";
case soaked: return "soaked";
default: return "unrecorded";
}
}
constexpr std::optional<SoilBand> soilBandFromLabel(std::string_view text)
{
if (text == "parched") return parched;
if (text == "thirsty") return thirsty;
if (text == "moist") return moist;
if (text == "soaked") return soaked;
return {};
}
std::ostream& operator<<(std::ostream& stream, SoilBand band)
{
return stream << soilBandLabel(band);
}
std::istream& operator>>(std::istream& stream, SoilBand& band)
{
std::string word{};
stream >> word; // the stream already knows how to extract a std::string
const std::optional<SoilBand> parsed{ soilBandFromLabel(word) };
if (!parsed.has_value())
{
stream.setstate(std::ios_base::failbit); // report failure the way the standard library does
return stream;
}
band = *parsed;
return stream;
}
int main()
{
std::cout << "Soil bands for the front and back beds: ";
SoilBand frontBed{};
SoilBand backBed{};
std::cin >> frontBed >> backBed;
if (!std::cin)
{
std::cin.clear(); // put the stream back into a usable state
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
std::cout << "\nThat word is not on the moisture chart.\n";
return 0;
}
std::cout << "\nLogged " << frontBed << " and " << backBed << '\n';
return 0;
}
Given the input moist soaked:
Soil bands for the front and back beds:
Logged moist and soaked
Given the input moist swampy:
Soil bands for the front and back beds:
That word is not on the moisture chart.
Both readings come from one chained statement, std::cin >> frontBed >> backBed, for the same reason the output chain worked: each call returns the stream. In the failing run the first word matches and the second does not, so frontBed really does hold moist while the statement as a whole reports failure. Had a third extraction followed, it would have done nothing at all, since a stream already in a failed state ignores further extractions until its state is cleared.
The right operand of
operator>> is an out parameter. If it were passed by value, the function would assign the extracted value to its own local copy and the caller's object would never change. The non-const reference is what makes the result visible to the caller.
One last detail worth matching: when extraction into a fundamental type fails, the standard library value-initializes the object rather than leaving whatever was there. If you want the same behaviour, add band = {}; on the failure path before returning.
Do not signal a bad extraction by throwing, by returning a sentinel enumerator, or by printing an error from inside the operator. Any of those forces callers of your type to handle failure differently from every other type they extract. Set
failbit and let the caller decide what to do.
Summary
Operator overloading defines how an existing operator behaves for a program-defined type. The overload is a function named after the operator, with one parameter per operand, and at least one of those parameters must be a class type or an enumerated type.
operator<< takes std::ostream& on the left and the object being printed on the right, by value if it is small like an enumeration, by const reference if it is a class type. It writes through the stream parameter and returns std::ostream&.
operator>> takes std::istream& on the left and a non-const reference to the object being filled on the right, so it can be modified. It returns std::istream&.
Returning the stream by reference is what makes std::cout << a << b work. Each call passes the stream to the next one. A void return breaks the chain at compile time.
Failure is reported through the stream. When the input does not name a valid value, call stream.setstate(std::ios_base::failbit) and return. The caller checks the stream, then calls clear() and ignore() to recover.
These overloads are never member functions, because the left operand is a stream you do not own, and a member overload would require your object on the left instead.
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.
Custom Input/Output with Operator Overloading - Quiz
Test your understanding of the lesson.
Practice Exercises
Overload operator<< for a Status Enum
Create a Status enumeration with values offline, online, and away. Overload operator<< to output the name of the status instead of its numeric value.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!