tukra
tukra

Reputation: 921

SFINAE with multi-layer type determination

The following code specializes two versions of f(). The first detects a vector and returns an iterator. The second takes all other types and returns a copy.

This fails to compile on VC 2010 with an error in GetIter2 that GetIter::type doesn't exist. This is only when resolving a call to f() with a non-vector. If I remove one layer of type indirection, using GetIter instead of GetIter2 (see commented line) for the return type, then things work fine.

I guess I want to know if this a compiler bug or correct behavior. If this looks weird it's because it's a reduction of a problem I'm having using boost::range_iterator where I can't just remove what GetIter2 represents.

#include <vector>
using namespace std;

template<typename T>
struct GetIter {
};

template<typename T>
struct GetIter<vector<T>> {
    typedef typename vector<T>::iterator type;
};

template<typename T>
struct GetIter2
{
    typedef typename GetIter<T>::type type;
};

template<typename T>
typename enable_if<is_same<T, vector<int>>::value, typename GetIter2<T>::type>::type
//typename enable_if<is_same<T, vector<int>>::value, typename GetIter<T>::type>::type
f(T & t) {
    return t.begin();
}

template<typename T>
typename enable_if<!is_same<T, vector<int>>::value, T>::type
f(T & t) {
    return t;
}

int main(int argc, char* argv[])
{
    vector<int> v(2);
    int i = 6;

    f(v);
    f(i);  // error C2039: 'type' : is not a member of 'GetIter<T>'

    return 0;
}

EDIT: Here is the actual problem I'm trying to solve. The second call to copy() with an iterator as the second argument causes a similar error as above on the boost::mpl::eval_if_c object.

#include <vector>
using namespace std;

#include <boost/range.hpp>
#include <boost/tti/has_type.hpp>

BOOST_TTI_TRAIT_HAS_TYPE(has_iterator, iterator)

template<typename InCont, typename Out>
typename enable_if<has_iterator<Out>::value, typename boost::range_iterator<Out>::type>::type
copy(InCont const & in_cont, Out & out_cont)
{
    return std::copy(boost::begin(in_cont), boost::end(in_cont), boost::begin(out_cont));
}

template<typename InCont, typename Out>
typename enable_if<!has_iterator<Out>::value, Out>::type
copy(InCont const & in_cont, Out & out_iter)
{
    return std::copy(boost::begin(in_cont), boost::end(in_cont), out_iter);
}

int main(int argc, char* argv[])
{
    vector<int> v1;
    vector<int> v2;

    copy(v1, v2);
    copy(v1, v2.begin());  // error C2039: 'type' : is not a member of 'boost::mpl::eval_if_c<C,F1,F2>'

    return 0;
}

EDIT 2: The original problem was fixed in a recent version of boost::range_iterator. Once I patched that things got easier. Here's what I've landed on, checking for containers with boost::has_range_iterator:

#include <vector>
using namespace std;

#include <boost/range.hpp>

template<typename InCont, typename Out>
typename boost::range_iterator<Out>::type
copy(InCont const & in_cont, Out & out_cont)
{
    return std::copy(boost::begin(in_cont), boost::end(in_cont), boost::begin(out_cont));
}

template<typename InCont, typename Out>
typename enable_if<!boost::has_range_iterator<Out>::value, Out>::type
copy(InCont const & in_cont, Out out_iter)
{
    return std::copy(boost::begin(in_cont), boost::end(in_cont), out_iter);
}

int main(int argc, char* argv[])
{
    vector<int> v1;
    vector<int> v2;

    copy(v1, v2);
    copy(v1, v2.begin());

    return 0;
}

Upvotes: 1

Views: 107

Answers (3)

Kurt Stutsman
Kurt Stutsman

Reputation: 4034

I'm still trying to find the relevant part of the standard to explain why this doesn't work. I think it has something to do with the substitution of GetIter2<T> not failing but subsequently accessing GetIter2<T>::type where it then determines that GetIter<T>::type doesn't exist. By making the GetIter2<T> template fail to substitute right away you don't get the error. You can do that by changing its definition slightly:

template<typename T, typename ST = typename GetIter<T>::type>
struct GetIter2
{
    typedef ST type;
};

I was able to get your real example working without modifying boost types with the following code:

template<typename InCont, typename Out, typename = typename enable_if<has_iterator<Out>::value>::type>
typename boost::range_iterator<Out>::type
copy(InCont const & in_cont, Out & out_cont)
{
    return std::copy(boost::begin(in_cont), boost::end(in_cont), boost::begin(out_cont));
}

Upvotes: 1

Barry
Barry

Reputation: 303457

Why it doesn't work

The rule is, from [temp.deduct], emphasis mine:

Only invalid types and expressions in the immediate context of the function type and its template parameter types can result in a deduction failure.

The immediate context is what you see in the declaration itself. The failure that happens when you try to substitute T=int isn't in the immediate context of typename GetIter2<T>::type - the failure happens when trying to determine what that type actually is and seeing that GetIter<int> doesn't have a type member. Since that isn't in the immediate context, this isn't a deduction failure - it's a hard error.

Note: even if this were a deduction failure, it would still only work for std::vector<int, std::allocator<int>>, not any vector.

How to make it work

Actually, you don't need SFINAE at all here. Just have one overload for vector and another overload for everything:

template <class T, class A>
auto f(std::vector<T,A>& t) {
    return t.begin();
}

template <class T>
T f(T& t) {
    return t;
}

Upvotes: 1

Zhu Yi
Zhu Yi

Reputation: 1

I think the non-vector GetIter missed a type function. template struct GetIter { typedef T type; };

Upvotes: -1

Related Questions