c++operator-overloadingiostreammanipulators

Custom stream manipulator that passes a character to operator overload


I'm toying around with shift/io stream operator overloads and I was wondering if there is a way to pass additional arguments to the function, while still defining a default value for simpler syntax?

Considering the simple example:

#include <vector>
#include <iostream>

inline std::ostream& operator<<(std::ostream& ostream, const std::vector< int >& data) {
    ostream << data[0];
    for (int idx = 1; idx < data.size(); idx++) {
        ostream << "," << data[idx];   // pass ',' as argument?
    }
    return ostream;
}

I'm looking to pass the delimiter character , to the function e.g. perhaps through a stream modifier:

std::cout << std::vector<int>(3, 15) << std::endl;   // 15,15,15
std::cout << delimiter(;) << std::vector<int>(3,15) << std::endl;  // 15;15;15

I've written a simple class that does this, but the resulting syntax is not very clean (requires forcing member operator overload to be called first):

#include <string>
#include <vector>
#include <iostream>
#include <sstream>

class formatted{
 public:
    explicit formatted(const std::string& sequence = ",") : delimiter(sequence) { }

    template < typename T >
    std::string operator<<(const T& src) const {
        std::stringstream out;
        if (src.size()) {
            out << src[0];
            for (int i = 1; i < src.size(); i++) {
                out << delimiter << src[i];
            }
        }
        return out.str();
    }

 protected:
    std::string delimiter;
};


template < typename T >
inline std::ostream& operator<<(std::ostream& ostream, const std::vector< T >& data) {
    ostream << (formatted() << data);
    return ostream;
}



int main(int argc, char const *argv[]) {
    std::vector< int > data(10, 5);

    std::cout << data << std::endl;  // 5,5,5...5,5
    std::cout << (formatted("/") << data) << std::endl;  // 5/5/5...5/5

    return 0;
}

Is there a way to simplify this, without the need of a helper class, or through the use of conventional stream manipulators?


Solution

  • Minor improvements of the current design

    What you are doing is possible and can't be simplified much further. If you want to stick to your current implementation, I recommend fixing the following issues:

    Unnecessary copy of entire string

    Use

    return std::move(out).str();
    

    to prevent copying the entire string in the stringstream at the end (since C++20).

    Signed/unsigned comparison

    for (int i = 1; i < data.size(); ...)
    

    results in a comparison between signed and unsigned, which results in compiler warnings at a sufficient warning level. Prefer std::size_t i.

    Unconstrained stream insertion operator

    operator<<(const T&)
    

    is not constrained and would accept any type, even though it only works for vectors. Prefer:

    operator<<(const std::vector<T>&)
    

    Using std::string instead of std::string_view

    const std::string& sequence = ","
    

    results in the creation of an unnecessary string, possibly involving heap allocation, even when we use a string literal such as ",". Normally we would prefer std::string_view, but here, you are storing this sequence in the class. This can create problems because std::string_view has no ownership over the string, so the lifetime of the string could expire before you use the view. This problem is inherent to your design and can not be easily resolved.

    Alternative design

    Calling formatted a stream manipulator would be inaccurate, because stream manipulators are functions which accept and return a basic_ios (or derived type). Your formatted type is just some type with an operator overload for <<, but you could just as well do:

    std::cout << vector_to_string(data, "/");
    

    where "/" would default to "," if no argument is provided. The problem is that we are still not using the stream for outputting the characters, but we first have to create a string, write the vector contents to it, and then write the string into the stream.

    Instead, we can make a function that accepts the stream as its first parameter and writes directly to it. The advantages include:

    This design as a regular function has precedent in the standard library as well. A classic examples is std::getline with the signature:

    template < class CharT, ...>
    std::basic_istream<CharT, ...>& getline( std::basic_istream<CharT, ...>& input,
                                             std::basic_string<CharT, ...>& str,
                                             CharT delim );
    

    With this design, we can create much more concise code, which also resolves the issues of:

    #include <string>
    #include <vector>
    #include <iostream>
    
    template < typename T >
    std::ostream& print(std::ostream& ostream,
                        const std::vector< T >& data,
                        std::string_view delimiter = ",")
    {
        if (not data.empty()) {
            ostream << data.front();
            for (std::size_t i = 1; i < data.size(); i++) {
                ostream << delimiter << data[i];
            }
        }
        return ostream;
    }
    
    
    int main(int argc, char const *argv[])
    {
        std::vector< int > data(10, 5);
    
        print(std::cout, data) << std::endl;
        print(std::cout, data, "/") << std::endl;
    }
    

    Further generalization for any range

    We can generalize this code so that it works for any range that provides an input iterator, not just std::vector. This means that we can use it for types such as std::array, std::string, std::vector, std::list, etc.

    template <typename Range>
    std::ostream& print(std::ostream& ostream,
                        const Range& range,
                        std::string_view delimiter = ",")
    {
        auto begin = range.begin();
        auto end = range.end();
    
        if (begin != end) {
            ostream << *begin;
            while (++begin != end) {
                ostream << delimiter << *begin;
            }
        }
        return ostream;
    }