Overloading the I/O Operators

For classes with multiple member variables, printing each variable individually becomes tedious. Consider this class:

class Vector2D
{
private:
    double m_x{};
    double m_y{};

public:
    Vector2D(double x = 0.0, double y = 0.0)
        : m_x{x}, m_y{y}
    {}

    double getX() const { return m_x; }
    double getY() const { return m_y; }
};

To print an instance, you'd need:

Vector2D velocity{3.5, 2.0};

std::cout << "(" << velocity.getX() << ", " << velocity.getY() << ")";

This is cumbersome. A reusable print() member function helps:

class Vector2D
{
private:
    double m_x{};
    double m_y{};

public:
    Vector2D(double x = 0.0, double y = 0.0)
        : m_x{x}, m_y{y}
    {}

    double getX() const { return m_x; }
    double getY() const { return m_y; }

    void print() const
    {
        std::cout << "(" << m_x << ", " << m_y << ")";
    }
};

This is better, but has downsides. Since print() returns void, it can't be used mid-expression:

int main()
{
    const Vector2D velocity{3.5, 2.0};

    std::cout << "Velocity: ";
    velocity.print();
    std::cout << '\n';
}

It would be cleaner to write:

Vector2D velocity{3.5, 2.0};
std::cout << "Velocity: " << velocity << '\n';

Overloading operator<< makes this possible!

Overloading operator<<

Overloading operator<< is similar to overloading other binary operators, except the parameter types differ.

Consider std::cout << velocity. The operator is <<, the left operand is std::cout (type std::ostream), and the right operand is velocity (type Vector2D). Our overloaded function signature:

// std::ostream is the type of std::cout
friend std::ostream& operator<<(std::ostream& out, const Vector2D& vec);

Implementation is straightforward - use operator<< to output the Vector2D's data members:

#include <iostream>

class Vector2D
{
private:
    double m_x{};
    double m_y{};

public:
    Vector2D(double x = 0.0, double y = 0.0)
        : m_x{x}, m_y{y}
    {}

    friend std::ostream& operator<<(std::ostream& out, const Vector2D& vec);
};

std::ostream& operator<<(std::ostream& out, const Vector2D& vec)
{
    // Access members directly (friend function)
    out << "(" << vec.m_x << ", " << vec.m_y << ")";

    return out; // Return std::ostream for chaining
}

int main()
{
    const Vector2D velocity{3.5, 2.0};

    std::cout << velocity << '\n';

    return 0;
}

This is similar to our print() function, except std::cout becomes parameter out (which references std::cout when called).

The tricky part is the return type. Returning std::ostream by value fails because std::ostream disallows copying.

We return the left parameter by reference. This not only prevents copying, it also enables chaining: std::cout << velocity << '\n'.

How? Due to precedence/associativity, this evaluates as (std::cout << velocity) << '\n'. The first part (std::cout << velocity) calls our overloaded operator<<, which returns std::cout. The expression becomes std::cout << '\n', which then evaluates normally.

Returning the left-hand parameter by reference is safe here - since it was passed by the caller, it still exists when our function returns.

Here's a complete example:

#include <iostream>

class Vector2D
{
private:
    double m_x{};
    double m_y{};

public:
    Vector2D(double x = 0.0, double y = 0.0)
        : m_x{x}, m_y{y}
    {}

    friend std::ostream& operator<<(std::ostream& out, const Vector2D& vec);
};

std::ostream& operator<<(std::ostream& out, const Vector2D& vec)
{
    out << "(" << vec.m_x << ", " << vec.m_y << ")";

    return out;
}

int main()
{
    Vector2D position{5.0, 3.0};
    Vector2D velocity{1.5, -0.5};

    std::cout << position << " moving at " << velocity << '\n';

    return 0;
}

Output:

(5, 3) moving at (1.5, -0.5)

If Vector2D provided public getters, operator<< could be a non-friend.

Overloading operator>>

Overloading the input operator is analogous. std::cin has type std::istream. Here's Vector2D with overloaded operator>>:

#include <iostream>

class Vector2D
{
private:
    double m_x{};
    double m_y{};

public:
    Vector2D(double x = 0.0, double y = 0.0)
        : m_x{x}, m_y{y}
    {}

    friend std::ostream& operator<<(std::ostream& out, const Vector2D& vec);
    friend std::istream& operator>>(std::istream& in, Vector2D& vec);
};

std::ostream& operator<<(std::ostream& out, const Vector2D& vec)
{
    out << "(" << vec.m_x << ", " << vec.m_y << ")";

    return out;
}

// Vector2D must be non-const to be modified
std::istream& operator>>(std::istream& in, Vector2D& vec)
{
    // This version subject to partial extraction (see below)
    in >> vec.m_x >> vec.m_y;

    return in;
}

int main()
{
    std::cout << "Enter x and y coordinates: ";

    Vector2D point{1.0, 2.0}; // Non-zero test data
    std::cin >> point;

    std::cout << "You entered: " << point << '\n';

    return 0;
}

With input 4.5 7.2:

You entered: (4.5, 7.2)

With invalid input 4.5x 7.2:

You entered: (4.5, 2)

This produces a weird hybrid: one user value (4.5), one unchanged value (2). This is a partial extraction problem.

Guarding Against Partial Extraction

When extracting a single value, extraction either fails or succeeds. With multiple values, partial extraction can occur.

In the invalid input case above: extraction to m_x succeeds (4.5), leaving x 7.2 in the stream. Extraction to m_y fails on x, so m_y gets 0 and the stream enters failure mode.

Partial extraction is never desirable. Sometimes it's dangerous (imagine a denominator getting zero, causing division by zero later).

We can make this transactional - all-or-nothing. Extract to temporary variables first, then update the object only if all extractions succeed:

std::istream& operator>>(std::istream& in, Vector2D& vec)
{
    double x{};
    double y{};

    if (in >> x >> y)        // If all extractions succeeded
        vec = Vector2D{x, y}; // Update object

    return in;
}

Now extraction either fully succeeds or leaves the object unchanged.

Tip
`if (in >> x >> y)` is equivalent to:
```cpp in >> x >> y; if (in) ```

For consistency with fundamental types (which are set to 0 on failed extraction), you might want to reset the object to default on failure:

std::istream& operator>>(std::istream& in, Vector2D& vec)
{
    double x{};
    double y{};

    in >> x >> y;
    vec = in ? Vector2D{x, y} : Vector2D{};

    return in;
}

Handling Semantically Invalid Input

Extraction can fail in different ways. When operator>> fails to extract anything, std::cin automatically enters failure mode, which the caller can check.

But what about extractable but semantically invalid values (e.g., a magnitude that must be positive)? Since extraction succeeded, std::cin won't enter failure mode automatically.

We can manually set failure mode by calling std::cin.setstate(std::ios_base::failbit):

std::istream& operator>>(std::istream& in, Vector2D& vec)
{
    double x{};
    double y{};

    in >> x >> y;

    // Check for NaN or infinity
    if (std::isinf(x) || std::isinf(y) || std::isnan(x) || std::isnan(y))
        in.setstate(std::ios_base::failbit); // Manually set failure mode

    vec = in ? Vector2D{x, y} : Vector2D{};

    return in;
}

Conclusion

Overloading operator<< and operator>> makes printing and inputting custom classes intuitive and consistent with built-in types.

Quiz

Add overloaded operator<< and operator>> to this Coordinate class. operator>> should prevent partial extraction and fail if either latitude is outside [-90, 90] or longitude is outside [-180, 180].

class Coordinate
{
private:
    double m_latitude{};
    double m_longitude{};

public:
    Coordinate(double lat = 0.0, double lon = 0.0)
        : m_latitude{lat}, m_longitude{lon}
    {}

    // Add operator<< and operator>>
};

int main()
{
    Coordinate loc1{};
    std::cout << "Enter latitude and longitude: ";
    std::cin >> loc1;

    Coordinate loc2{};
    std::cout << "Enter another location: ";
    std::cin >> loc2;

    std::cout << "Location 1: " << loc1 << '\n';
    std::cout << "Location 2: " << loc2 << '\n';

    return 0;
}

Sample run:

Enter latitude and longitude: 40.7128 -74.0060
Enter another location: 51.5074 -0.1278
Location 1: (40.7128, -74.006)
Location 2: (51.5074, -0.1278)
Show Solution
#include <iostream>

class Coordinate
{
private:
    double m_latitude{};
    double m_longitude{};

public:
    Coordinate(double lat = 0.0, double lon = 0.0)
        : m_latitude{lat}, m_longitude{lon}
    {}

    friend std::ostream& operator<<(std::ostream& out, const Coordinate& coord);
    friend std::istream& operator>>(std::istream& in, Coordinate& coord);
};

std::ostream& operator<<(std::ostream& out, const Coordinate& coord)
{
    out << "(" << coord.m_latitude << ", " << coord.m_longitude << ")";
    return out;
}

std::istream& operator>>(std::istream& in, Coordinate& coord)
{
    double lat{};
    double lon{};

    in >> lat >> lon;

    // Check for valid ranges
    if (lat < -90.0 || lat > 90.0 || lon < -180.0 || lon > 180.0)
        in.setstate(std::ios_base::failbit);

    if (in)
        coord = Coordinate{lat, lon};

    return in;
}

int main()
{
    Coordinate loc1{};
    std::cout << "Enter latitude and longitude: ";
    std::cin >> loc1;

    Coordinate loc2{};
    std::cout << "Enter another location: ";
    std::cin >> loc2;

    std::cout << "Location 1: " << loc1 << '\n';
    std::cout << "Location 2: " << loc2 << '\n';

    return 0;
}

Summary

Overloading operator<<: Allows natural output syntax like std::cout << myObject. Takes std::ostream& as first parameter and the object as second, returns std::ostream& by reference to enable chaining.

Overloading operator>>: Enables natural input syntax like std::cin >> myObject. Takes std::istream& as first parameter and non-const object reference as second, returns std::istream& by reference for chaining.

Return by reference: Both I/O operators return their left-hand parameter by reference (not by value) to prevent copying and enable chaining like std::cout << obj1 << obj2.

Partial extraction problem: When extracting multiple values, extraction can fail partway through, leaving some members updated and others not. This produces inconsistent object state.

Transactional extraction: Extract to temporary variables first, then update the object only if all extractions succeed. This makes extraction all-or-nothing.

Semantic validation: For extractable but semantically invalid values (like coordinates outside valid ranges), manually set failure mode using in.setstate(std::ios_base::failbit) so callers can detect the error.

Overloading I/O operators makes custom classes work naturally with streams, providing the same intuitive syntax as built-in types. The key considerations are returning by reference for chaining, preventing partial extraction for safety, and validating semantic constraints beyond just type compatibility.