is_container trait fails on std::set SFINAE issue

2019-01-27 01:55发布

问题:

I am trying to write a stream operator for std containers, mainly for the purpose of debugging.

I have the following code:

#include <type_traits>
#include <iostream>
#include <ostream>
#include <iterator>
#include <algorithm>
#include <functional>

#include <vector>
#include <set>
#include <deque>


template<typename Container>
struct is_container
{
    typedef char no;
    typedef long yes;


    template<typename A, A, A>
    struct is_of_type;

    template<typename T>
            static yes& is_cont(
                            is_of_type
                            <
                                    typename T::iterator(T::*)(), 
                                    &T::begin,
                                    &T::end
                            >*);


    template<typename T>
    static no& is_cont(...);        //any other

    template<typename C, bool B>
    struct is_class_is_container
    {
            const static bool value=sizeof( is_cont<C>(nullptr) )==sizeof(yes);
    };

    template<typename C>
    struct is_class_is_container<C, false>
    {
            const static bool value=false;
    };

    const static bool value = is_class_is_container
            <
                    Container, 
                    std::is_class<Container>::value 
            >::value;
};

template<typename T>
    typename std::enable_if
    < is_container<T>::value, std::ostream >::type& 
    operator<<(std::ostream& os, const T& a)
{
    os << '[';
    std::copy(a.begin(), a.end(), std::ostream_iterator<typename T::value_type>(os, ", "));
    os << ']';
    return os;
}

I am aware this is far from perfect (constructive comments appreciated) but the problem I am having is that it works great for vector, deque and list but fails to match on sets, I don't know why because sets still have the iterator interfaces begin and end.

Thanks.

EDIT: tested on g++ (GCC) 4.6.2 2012012 clang version 3.0

EDIT2: I got it sort of working using decltype however that is sub optimal because now I can't assert that it does what I expect (return an iterator) to.

I don't exactly know what the set was return in the first place, maybe if someone has a way of debugging TMP that would be good.

回答1:

Since std::set<T> only has one set of immutable iterators, there is only one version of begin() and end() which is declared to be const. That is, the definition of std::set<T> looks something like this (assuming it was declared in namespace std before):

template <typename T>
class std::set
{
public:
    class            iterator;
    typedef iterator const_iterator;
    ...
    const_iterator begin() const;
    const_iterator end() const;
    ...
};

The other containers have both a const and a non-const version of begin() and end() matching the signature you ask for. std::set doesn't have this. I'm not sure what the easiest work-around for this would be though.

That said, sizeof(char) is allowed to be sizeof(long). The easiest way to guarantee that the yes and no types have different size is to use references to arrays of different sizes for the same type, e.g.:

typedef char (&yes)[1];
typedef char (&no)[2];
...
enum { value = sizeof(some_expression) == sizeof(yes) };


回答2:

It works for vector but not for set because the latter returns const_iterator for begin/end functions. Change:

typename T::iterator(T::*)(), 

to:

typename T::const_iterator(T::*)() const,