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?
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:
Use
return std::move(out).str();
to prevent copying the entire string in the stringstream
at the end (since C++20).
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
.
operator<<(const T&)
is not constrained and would accept any type, even though it only works for vectors. Prefer:
operator<<(const std::vector<T>&)
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.
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:
#include <sstream>
, potentially just including <iosfwd>
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:
std::string_view
std::string
to store the whole printed vector#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;
}
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;
}