Вопрос

I am trying to write a template class which may or may not define a particular member function depending on its template parameter type. Further the return type of this member function depends on the return type of of a member of the template paramter (if defined).

Below is a minimal example of my code

#include <iostream>
#include <type_traits>

template <typename T>
struct has_foo_int {
private:
    template <typename U>
    static decltype(std::declval<U>().foo(0), void(), std::true_type()) test(int);
    template <typename>
    static std::false_type test(...);
public:
    typedef decltype(test<T>(0)) test_type;
    enum { value = test_type::value };
};

template <typename T, bool HasFooInt>
struct foo_int_return_type;

template<typename T>
struct foo_int_return_type<T,false> {};

template<typename T>
struct foo_int_return_type<T,true> {
    using type = decltype(std::declval<T>().foo(0));
};

template<typename T>
struct mystruct
{
    T val;

    //auto someMethod(int i) -> decltype(std::declval<T>().foo(0)) // error: request for member ‘foo’ in ‘std::declval<double>()’, which is of non-class type ‘double’
    //auto someMethod(int i) -> typename foo_int_return_type<T,has_foo_int<T>::value>::type // error: no type named ‘type’ in ‘struct foo_int_return_type<double, false>’
    template<typename R=typename foo_int_return_type<T,has_foo_int<T>::value>::type> R someMethod(int i) // error: no type named ‘type’ in ‘struct foo_int_return_type<double, false>’
    {
        return val.foo(i);
    }
};

struct with_foo_int {
    int foo(int i){
        return i+1;
    }
};

using namespace std;

int main(void)
{
    mystruct<with_foo_int> ms1;
    cout << ms1.someMethod(41) << endl;

    mystruct<double> ms2;

    return 0;
}

What I would like to happen is that the code compiles fine and outputs 42 for ms1.someFunc(41). I would also expect that if one accidentally tried to call someFunc on ms2 that it would fail to compile.

Unfortunately each of the alternatives I have tried has failed. The first and second, I think I understand why they wouldn't work.

I read here that SFINAE only works for template functions so I tried giving a dummy template parameter to work out the return type but this too fails in the same way.

I'm clearly not understanding something here, what am I missing? Is it possible to achieve what I'm trying to do?

Thanks.

P.s. I'm using g++ 4.7.3

P.p.s I have also tried std::enable_if but get much the same results as with my foo_int_return_type struct.

Это было полезно?

Решение

Here is a short, tidy and documented way of doing what you are attempting, with some possible bugs addressed thereafter.

#include <type_traits>

/*
    Template `has_mf_foo_accepts_int_returns_int<T>`
    has a static boolean public member `value` that == true
    if and only if `T` is a class type that has a public
    member function or member function overload 
    `int T::foo(ArgType) [const]`  where `ArgType`
    is a type to which `int` is implicitly convertible.
*/
template <typename T>
struct has_mf_foo_accepts_int_returns_int {

    /* SFINAE success:
        We know now here `int *` is convertible to
        "pointer to return-type of T::foo(0)" 
    */
    template<typename A>
    static constexpr bool test(
        decltype(std::declval<A>().foo(0)) *prt) {
        /* Yes, but is the return-type of `T::foo(0)`
            actually *the same* as `int`?...
        */
        return std::is_same<int *,decltype(prt)>::value;
    }

    // SFINAE failure :(
    template <typename A>
    static constexpr bool test(...) {
        return false; 
    }

    /* SFINAE probe.
        Can we convert `(int *)nullptr to 
        "pointer to the return type of T::foo(0)"?
    */
    static const bool value = test<T>(static_cast<int *>(nullptr)); 
};

template<typename T>
struct mystruct
{
    using has_good_foo = has_mf_foo_accepts_int_returns_int<T>;

    T val;

    /*  SFINAE:
        `template<typename R> R someMethod(R)` will be this if and only
        if `R` == `int` and `has_good_foo` == true.         
    */
    template<typename R = int>
    typename std::enable_if<
        (has_good_foo::value && std::is_same<R,int>::value),R
    >::type 
    someMethod(R i) {
        return val.foo(i);
    }

    /*  SFINAE:
        `template<typename R> R someMethod(R)` will be this if and only
        if `R` != `int` or `has_good_foo` != true.      
    */
    template<typename R = int>
    typename std::enable_if<
        !(has_good_foo::value && std::is_same<R,int>::value),R
    >::type
    someMethod(R i) {
        static_assert(has_good_foo::value && std::is_same<R,int>::value,
            "mystruct<T> does not implement someMethod(R)");
        return i;
    }
};

// Testing...

#include <iostream>

struct with_foo_int
{
    int foo(int i) {
        return i + 1;
    }
};

using namespace std;

int main(void)
{
    mystruct<with_foo_int> ms1;
    cout << ms1.someMethod(41) << endl;

    mystruct<double> ms2;
    cout << ms2.someMethod(41) << endl; // static_assert failure

    return 0;
}

This solution faithfully reproduces a couple of possible loopholes in your own attempt as posted:-

1) It looks as if you may believe that evaluating std::declval<U>().foo(0) is a SFINAE way of determining whether U::foo exists and takes a single argument of type int. It doesn't. It is merely a SFINAE way of determining whether U::foo(ArgType) exists where ArgType is anything to which 0 is implicitly convertible. Thus ArgType could be any pointer-or-arithmetic type, not just int.

2) You may not have considered that std::declval<U>().foo(0) will be satisfied if either or both of U::foo(ArgType) U::foo(ArgType) const exists. You may well care whether you call a const or a non-const member function on U, and you would certainly care which of two member function you call. If with_foo_int were defined as:

struct with_foo_int
{
    int foo(int i) const {
        return i + 1;
    }
    int foo(int i) {
        return i + 2;
    }   
};

then the solution given would call the non-const overload and ms1.someMethod(41) would == 43.

2) Is easily dealt with. If you wish to ensure that you can only call T::foo(ArgType) const then add a const qualifier to mystruct::someMethod. If you don't care or wish only to call T::foo(ArgType) then leave things as they are.

1) is a little harder to solve, because you must craft a SNIFAE probe for T::foo that is satisfied only if it has the right signature, and that signature will either be const qualified or not. Let's assume you want int T::foo(int) const. In that case, replace template has_mf_foo_accepts_int_returns_int with:

/*  Template `has_mf_foo_arg_int_returns_int<T>
    has a static boolean public member `value` that == true
    if and only if `T` is a class type that has an un-overloaded
    a public member `int T::foo(int) const`.
*/ 
template< typename T>
struct has_mf_foo_arg_int_returns_int
{
    /* SFINAE foo-has-correct-sig :) */
    template<typename A>
    static std::true_type test(int (A::*)(int) const) {
        return std::true_type();
    }

    /* SFINAE foo-exists :) */
    template <typename A> 
    static decltype(test(&A::foo)) 
    test(decltype(&A::foo),void *) {
        /* foo exists. What about sig? */
        typedef decltype(test(&A::foo)) return_type; 
        return return_type();
    }

    /* SFINAE game over :( */
    template<typename A>
    static std::false_type test(...) {
        return std::false_type(); 
    }

    /* This will be either `std::true_type` or `std::false_type` */
    typedef decltype(test<T>(0,0)) type;

    static const bool value = type::value; /* Which is it? */
};

and in template mystruct replace:

using has_good_foo = has_mf_foo_accepts_int_returns_int<T>;

with:

using has_good_foo = has_mf_foo_arg_int_returns_int<T>;

(Template has_mf_foo_arg_int_returns_int is adapted from my other answer and you can read how it works there.)

What you gain in SFINAE-precision from the latter approach comes at a price. The approach requires you to attempt to take the address of T::foo, to see if it exists. But C++ will not give you the address of an overloaded member function, so this approach will fail if T::foo is overloaded.

The code here will compile (or appropriately static_assert) with GCC >= 4.7.2 clang >= 3.2.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top