Implementation of template of a << operator // C++

我与影子孤独终老i 提交于 2021-01-24 06:57:13

问题


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.


回答1:


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;
}



回答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.



来源:https://stackoverflow.com/questions/65534442/implementation-of-template-of-a-operator-c

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!