4

I would want to make a template of a << operator in C++, that would show a Object that is a "range" (by that i mean any object like : std::vector, std::set, std::map, std::deque). How can i achieve this? I've been googling and looking in docs for a few days now, but without any effect. I've been doing few templates and been overriding few operators before, but these were inside of a certain class that was representing a custom vector class. I cant seem to find a good way of implementing this, because it collides with a standard cout. How do i do it then, inside of a class that can pass a vector,set,map,deque as an argument, and operator inside? I would also want this operator to return the begin() and end() iterator of an object. By now i have this code:

template <typename T>
ostream& operator<<(ostream& os, T something)
{
    os << something.begin() << something.end();
    return os;
}

it doesnt really work, and i think that experienced C++ programmer can explain me why.

Thanks in advance for any answer for that problem.

Insekure
  • 89
  • 1
  • 5
  • My answer also has some deficiencies with checking against `std::string` instead of a general check for an existing `operator<<` implementation. I can change/remove it if @TedLyngmo had something better. – GILGAMESH Jan 02 '21 at 04:55

2 Answers2

4

Your overload will match on pretty much everything causing ambiguity for the types for which operator<< already has an overload.

I suspect that you want to print all elements in the container here: os << something.begin() << something.end();. This will not work because begin() and end() return iterators. You could dereference them

if(something.begin() != something.end())
    os << *something.begin() << *std::prev(something.end());

but you'd only get the first and last element printed. This would print all of them:

for(const auto& v : something) os << v;

To solve the ambiguity problem, you could use template template parameters and enable the operator<< overload for the containers you'd like to support.

Example:

#include <deque>
#include <iostream>
#include <iterator>
#include <list>
#include <map>
#include <type_traits>
#include <vector>

// helper trait - add containers you'd like to support to the list
template <typename T> struct is_container : std::false_type {};
template <typename... Ts> struct is_container<std::vector<Ts...>> : std::true_type{};
template <typename... Ts> struct is_container<std::list<Ts...>> : std::true_type{};
template <typename... Ts> struct is_container<std::deque<Ts...>> : std::true_type{};
template <typename... Ts> struct is_container<std::map<Ts...>> : std::true_type{};

// C is the container template, like std::vector
// Ts... are the template parameters used to create the container.
template <template <typename...> class C, typename... Ts>
// only enable this for the containers you want to support
typename std::enable_if<is_container<C<Ts...>>::value, std::ostream&>::type
operator<<(std::ostream& os, const C<Ts...>& something) {
    auto it = something.begin();
    auto end = something.end();
    if(it != end) {
        os << *it;
        for(++it; it != end; ++it) {
            os << ',' << *it;
        }
    }
    return os;
}

An alternative could be to make it generic but to disable the overload for types that already supports streaming.

#include <iostream>
#include <iterator>
#include <type_traits>

// A helper trait to check if the type already supports streaming to avoid adding
// an overload for std::string, std::filesystem::path etc.
template<typename T>
class is_streamable {
    template<typename TT>
    static auto test(int) ->
    decltype( std::declval<std::ostream&>() << std::declval<TT>(), std::true_type() );

    template<typename>
    static auto test(...) -> std::false_type;

public:
    static constexpr bool value = decltype(test<T>(0))::value;
};

template <typename T, 
    typename U = decltype(*std::begin(std::declval<T>())), // must have begin
    typename V = decltype(*std::end(std::declval<T>()))    // must have end
>
// Only enable the overload for types not already streamable
typename std::enable_if<not is_streamable<T>::value, std::ostream&>::type
operator<<(std::ostream& os, const T& something) {
    auto it = std::begin(something);
    auto end = std::end(something);
    if(it != end) {
        os << *it;
        for(++it; it != end; ++it) {
            os << ',' << *it;
        }
    }
    return os;
}

Note: The last example works in clang++ and MSVC but it fails to compile in g++ (recursion depth exceeded).

For containers with a value_type that is in itself not streamable, like the std::pair<const Key, T> in a std::map, you need to add a separate overload. This needs to be declared before any of the templates above:

template <typename Key, typename T>
std::ostream &operator<<(std::ostream &os, const std::pair<const Key, T>& p) {
    return os << p.first << ',' << p.second;
}
Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
  • What are the "..." in the <> case? I cant really understand the syntax – Insekure Jan 01 '21 at 23:25
  • It's a variadic template and the `...` is used for the [parameter pack](https://en.cppreference.com/w/cpp/language/parameter_pack) – Ted Lyngmo Jan 01 '21 at 23:27
  • Ok, so variadic template means, that it can accept different amount of arguments whenever we use it, yes? So once we can pass 2, 3 or any other amount. That i understand, but the syntax looks kinda weird, i still cant really get used to the cpp syntax. template < template class C - what does it mean exactly, what function it has? Is it the one that is some kind of container, like vector, deque, set, map or such? And Ts... Are the values, that we fill our container with, right? – Insekure Jan 01 '21 at 23:33
  • Im also having an error, i dont know if this is because of the version of C++ im using - https://learn.microsoft.com/en-us/cpp/error-messages/compiler-errors-1/compiler-error-c2429?view=msvc-160 – Insekure Jan 01 '21 at 23:35
  • 1
    @Insekure It means that `C` is a template (like `std::vector`), not an instantiation of that template (like `std::vector`). `Ts...` are the parameters used to instantiate it. Regarding the error: Do you have to use an older C++ version? If you have VS2019, you can change the language standard to C++17 (or `latest` to get some C++20 support too). Anyway, I changed the answer to support C++11 and 14 too. – Ted Lyngmo Jan 01 '21 at 23:41
  • Out of curiosity, why `advance` instead of `++`? Does `÷÷` not always exist? Don't think I've ever seen it nor exist on an iterator. – ChrisMM Jan 01 '21 at 23:45
  • @ChrisMM I just have gotten used to using it, but I've changed to `++it`.now. – Ted Lyngmo Jan 02 '21 at 08:06
  • How can i make it work for both map and deque too? Thank you for this answer! – Insekure Jan 02 '21 at 12:34
  • is_same, vector>::value || is_same, list>::value || is_same, map>::value || is_same, deque>::value, – Insekure Jan 02 '21 at 12:48
  • @Insekure You're welcome! I updated the answer. Yeah, just add the `deque` to the list of containers in the first version and it should work. – Ted Lyngmo Jan 02 '21 at 13:46
2

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.

GILGAMESH
  • 1,816
  • 3
  • 23
  • 33
  • 1
    I like the idea but just as my answer caused ambiguous overloads for standard types for which an `operator<<` overload already exists, this will too. `std::cout << std::filesystem::path{"."};` would be getting an ambiguous overload etc. I'm thinking it'd perhaps be best to restrict it to a fixed set of containers - or perhaps, if possible, use sfinae to exclude types for which `operator<<` already has an overload. – Ted Lyngmo Jan 02 '21 at 07:44
  • 1
    A minor detail: Your example implementations of `operator<<` will dereference `end()` if used with empty containers. – Ted Lyngmo Jan 02 '21 at 09:46
  • 1
    @TedLyngmo Nice catches. – GILGAMESH Jan 02 '21 at 14:38
  • 1
    @GILGAMESH Thanks. I think I've never deleted and undeleted and edited an answer as much as I did for this. Such a seemingly simple thing - and I'm still not 100% sure that my generic version is ok. I think I'll revisit this to think about it again in a few days.:-) – Ted Lyngmo Jan 02 '21 at 14:46