Your code has the right idea but is missing a few things.
template <typename T>
ostream& operator<<(ostream& os, T something)
{
os << something.begin() << something.end();
return os;
}
Iterable containers (like std::map
and such) should be outputted by iterating through all their elements, and outputting each one-by-one. Here, you're only outputting the beginning and end iterators, which aren't the same as elements themselves.
We can instead use *it
to get an element from its iterator in the container. So, the code below will output all elements in a standard container of type T
. I also include some additional pretty-printing.
template <typename T>
std::ostream &operator<<(std::ostream &os, const T &o) {
auto it = o.begin();
os << "{" << *it;
for (it++; it != o.end(); it++) {
os << ", " << *it;
}
return os << "}";
}
If we just use
template <typename T>
ahead of this function declaration, then it will conflict with existing <<
operator declarations. That is, when we writestd::cout << std::string("hello world");
, does this call our function implementation, or does this call the function implementation from <string>
? Of course, we want to use the standard operator<<
implementations if available. We do this by limiting the template so that it only works for standard containers with begin()
and end()
members, but not for std::string
, which has begin()
and end()
but also has an existing operator<<
implementation that we want to use.
template <typename T,
typename std::enable_if<is_iterable<T>::value, bool>::type = 0,
typename std::enable_if<!std::is_same<T, std::string>::value, bool>::type = 0>
The second std::enable_if
is straightforward: the template should cover types as long as they aren't std::string
. The first std::enable_if
checks if the type T
is iterable. We need to make this check ourselves.
template <typename T>
class is_iterable {
private:
typedef char True[1];
typedef char False[2];
template <typename Q,
typename std::enable_if<
std::is_same<decltype(std::declval<const Q &>().begin()),
decltype(std::declval<const Q &>().begin())>::value,
char>::type = 0>
static True &test(char);
template <typename...>
static False &test(...);
public:
static bool const value = sizeof(test<T>(0)) == sizeof(True);
};
is_iterable
has two versions of the function test
. The first version is enabled if begin()
and end()
exist on type T
, and their return types are the same (there are more precise ways to do checks, but this suffices for now). The second version is called otherwise. The two versions' return types are different, and by checking the size of the return type, we can set value
, which will be true
if and only if T
is iterable (in our case, if T
defines begin()
and end()
and their return types are the same).
Finally, we note that std::map<T1, T2>
's elements are actually of type std::pair<T1, T2>
, so we need to additionally overload operator<<
for templated pairs.
template <typename T1, typename T2>
std::ostream &operator<<(std::ostream &os, const std::pair<T1, T2> &o) {
return os << "(" << o.first << ", " << o.second << ")";
}
Putting it all together, we can try this. Note that it even works for nested iterator types like listUnorderedSetTest
.
#include <iostream>
#include <list>
#include <map>
#include <set>
#include <type_traits>
#include <unordered_set>
#include <vector>
template <typename T>
class is_iterable {
private:
typedef char True[1];
typedef char False[2];
template <typename Q,
typename std::enable_if<
std::is_same<decltype(std::declval<const Q &>().begin()),
decltype(std::declval<const Q &>().begin())>::value,
char>::type = 0>
static True &test(char);
template <typename...>
static False &test(...);
public:
static bool const value = sizeof(test<T>(0)) == sizeof(True);
};
template <typename T1, typename T2>
std::ostream &operator<<(std::ostream &os, const std::pair<T1, T2> &o) {
return os << "(" << o.first << ", " << o.second << ")";
}
template <typename T,
typename std::enable_if<is_iterable<T>::value, bool>::type = 0,
typename std::enable_if<!std::is_same<T, std::string>::value, bool>::type = 0>
std::ostream &operator<<(std::ostream &os, const T &o) {
auto it = o.begin();
os << "{" << *it;
for (it++; it != o.end(); it++) {
os << ", " << *it;
}
return os << "}";
}
int main() {
std::vector<std::string> vectorTest{"hello", "world", "!"};
std::cout << vectorTest << std::endl;
std::set<const char *> setTest{"does", "this", "set", "work", "?"};
std::cout << setTest << std::endl;
std::map<std::string, std::size_t> mapTest{
{"bob", 100}, {"alice", 16384}, {"xavier", 216}};
std::cout << mapTest << std::endl;
std::list<std::unordered_set<std::string>> listUnorderedSetTest{
{"alice", "abraham", "aria"},
{"carl", "crystal", "ciri"},
{"november", "nathaniel"}};
std::cout << listUnorderedSetTest << std::endl;
return 0;
}
This outputs:
{hello, world, !}
{does, this, set, work, ?}
{(alice, 16384), (bob, 100), (xavier, 216)}
{{alice, abraham, aria}, {carl, crystal, ciri}, {november, nathaniel}}
There's a lot of additional related discussion at Templated check for the existence of a class member function? which you might find helpful. The downside of this answer is a check against std::string
instead of a check for existing operator<<
implementations, which I think can be solved with a bit more work into type checking with decltype
.