std::ostream_iterator in C++: Printing Ranges

In C++, std::ostream_iterator serves as a bridge between the Standard Template Library (STL) algorithms and output streams such as std::cout. This article explores the mechanics of std::ostream_iterator, explaining how it functions as an output iterator adapter, how it simplifies printing range elements without manual loops, its integration with both classic and modern C++ ranges, and its practical trade-offs.

What is std::ostream_iterator?

Defined in the <iterator> header, std::ostream_iterator is an output iterator adapter that writes successive elements to a designated std::basic_ostream object using operator<<. It abstracts the output stream into an iterator interface, allowing any range or sequence of elements to be treated as a source that flows directly into a stream destination.

Core Mechanics

An std::ostream_iterator requires a value type and an optional delimiter string upon instantiation.

std::ostream_iterator<T>(stream, delimiter);
  • Target Stream: The stream object to write to (e.g., std::cout or a std::ofstream).
  • Optional Delimiter: A C-style string appended to the stream immediately after each element is written.

Whenever an algorithm writes to an std::ostream_iterator, the following operations occur under the hood:

  1. Dereference and Assignment (*it = value): Writes the value to the underlying stream via stream << value. If a delimiter was specified, it immediately follows with stream << delimiter.
  2. Increment (++it / it++): Performs no operation and simply returns a reference to the iterator itself, fulfilling the interface requirements of an output iterator.

Using std::ostream_iterator with Algorithms

The primary role of std::ostream_iterator is eliminating manual for loops when transferring elements from containers to an output destination.

Classic STL with std::copy

Before C++20, the canonical approach to printing a container involved pairing std::copy with std::ostream_iterator:

#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator>

int main() {
    std::vector<int> numbers = {10, 20, 30, 40, 50};

    // Print all elements separated by a space
    std::copy(numbers.begin(), numbers.end(),
              std::ostream_iterator<int>(std::cout, " "));

    return 0;
}

Modern C++ with std::ranges::copy

In C++20 and later, std::ostream_iterator integrates seamlessly with constrained algorithms in the std::ranges namespace, allowing direct passing of containers or range views:

#include <iostream>
#include <vector>
#include <ranges>
#include <algorithm>
#include <iterator>

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5, 6};

    // Filter even numbers and stream them with a newline delimiter
    auto evens = numbers | std::views::filter([](int n) { return n % 2 == 0; });
    
    std::ranges::copy(evens, std::ostream_iterator<int>(std::cout, "\n"));

    return 0;
}

Key Advantages

  • Algorithm Interoperability: It enables standard algorithms (std::copy, std::transform, std::unique_copy) to direct their output to streams without creating intermediate containers.
  • Separation of Concerns: The source container does not need internal print methods; formatting and output destinations are handled externally.
  • Declarative Code: Replacing explicit loops with standard algorithms and stream iterators clarifies the intent of streaming data directly to output sinks.

Considerations and Limitations

  • Trailing Delimiters: Because std::ostream_iterator prints the delimiter after every element, the final element will also be followed by the delimiter. When formatting requirements demand strict separation without a trailing separator (such as standard comma-separated lists), alternatives like std::views::join_with (C++23) or custom formatting loops may be preferable.
  • Type Requirement: The element type must support stream insertion via operator<< for the target stream type.