Search code examples
c++c++11traitssfinaetype-traits

Matching member function existence and signature: parameters


Reading related questions "How to call member function only if object happens to have it?" and "Is it possible to write a C++ template to check for a function's existence?", I am implementing my own traits class. The objective is very simple, although I cannot achieve what I'd like: provide a traits class that statically redirects a call to a matched class.

So, if the class I provide to my traits has, for instance an void open_file() method, it calls it, otherwise use the traits function (a NOP one, but now an output). Obviously, this is a SFINAE-task, but being not too familiar to the process, I've followed the ideas, as you will see.

Everything works fine for void open_file(), but when trying void open_file(int), it doesn't match and calls the NOP function. This is my attempt (almost verbatim from those two questions!):

template <class Type>
class my_traits
{
    //! Implements a type for "true"
    typedef struct { char value;    } true_class;
    //! Implements a type for "false"
    typedef struct { char value[2]; } false_class;

    //! This handy macro generates actual SFINAE class members for checking event callbacks
    #define MAKE_MEMBER(X) \
        public: \
            template <class T> \
            static true_class has_##X(decltype(&T::X)); \
        \
            template <class T> \
            static false_class has_##X(...); \
        public: \
            static constexpr bool call_##X = sizeof(has_##X<Type>(0)) == sizeof(true_class);

    MAKE_MEMBER(open_file)

public:

    /* SFINAE foo-has-correct-sig :) */
    template<class A, class Buffer>
    static std::true_type test(void (A::*)(int) const)
    {
        return std::true_type();
    }

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

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

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

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

    /*  `eval(T const &,std::true_type)`
     delegates to `T::foo()` when `type` == `std::true_type`
     */
    static void eval(Type const & t, std::true_type)
    {
        t.open_file();
    }
    /* `eval(...)` is a no-op for otherwise unmatched arguments */
    static void eval(...)
    {
        // This output for demo purposes. Delete
        std::cout << "open_file() not called" << std::endl;
    }

    /* `eval(T const & t)` delegates to :-
     - `eval(t,type()` when `type` == `std::true_type`
     - `eval(...)` otherwise
     */
    static void eval(Type const &t)
    {
        eval(t, type());
    }

};


class does_match
{
public:
    void open_file(int i) const { std::cout << "MATCHES!" << std::endl; };
};

class doesnt_match
{
public:
    void open_file() const { std::cout << "DOESN'T!" << std::endl; };
};

As you see I've implemented both, the first one with the macro MAKE_MEMBER just checks the member existence, and it works. The next, I tried to use it for a static SFINAE if/else, i.e., if a member function exists then call it, otherwise use a predefined function, without success (as I said, I am not too deep into SFINAE).

The second attempt is almost verbatim from the check signature and existence question, but I've modified it to handle a parameter. However, it won't work:

    does_match   it_does;
    doesnt_match it_doesnt;

    my_traits<decltype(it_does)>::eval(it_does);
    my_traits<decltype(it_doesnt)>::eval(it_doesnt);

    // OUTPUT:
    // open_file() not called
    // open_file() not called

Obviously, there are problems here: I didn't provide parameters, but I don't know how I can do it.

I am trying to understand and learn, also, can I use an open_file() that depends on a template parameter, for instance having a SFINAE matching template <class T> open_file(T t)?

Thanks & Cheers!


Solution

  • The problem is that your call to test(&A::open_file):

    typedef decltype(test(&A::open_file)) return_type;
    

    is always matched to:

    static std::false_type test(...)
    

    because your true-test has an undeduced type template parameter Buffer:

    template<class A, class Buffer>
    //                      ~~~~~^
    static std::true_type test(void (A::*)(int) const)
    

    as such it's never considered as a viable function, unless you give that type argument explicitly, or remove it (what should be done here).

    Fixing this still won't solve all the problems of your code, as you don't have a fallback function that can be picked if open_file member function doesn't exist at all, so you'd need to add something like below (adjusted to your implementation):

    /* SFINAE foo-not-exists */
    template <class A>
    static std::false_type test(void*, ...);
    

    as a fallback for:

    static decltype(test(&A::open_file)) test(decltype(&A::open_file), void *)
    

    Tip: You don't have to provide the body of a function that appears only in an unevaluated context like within a decltype() operator.

    Finally, when you eventually match the call with a void (A::*)(int) const signature, you seem to forget about the argument:

    t.open_file(1);
    //          ^
    

    Test:

    my_traits<decltype(it_does)>::eval(it_does);
    my_traits<decltype(it_doesnt)>::eval(it_doesnt);
    

    Output:

    MATCHES!
    open_file() not called
    

    DEMO


    The entire trait can be much simplified using an expression SFINAE:

    template <class Type>
    struct my_traits
    {
        template <typename T>
        static auto eval(const T& t, int) -> decltype(void(t.open_file(1)))
        {
            t.open_file(1);
        }
    
        template <typename T>
        static void eval(const T& t, ...)
        {
            std::cout << "open_file() not called" << std::endl;
        }
    
        static void eval(const Type& t)
        {
            eval<Type>(t, 0);
        }
    };
    
    my_traits<decltype(it_does)>::eval(it_does);     // MATCHES!
    my_traits<decltype(it_doesnt)>::eval(it_doesnt); // open_file() not called
    

    DEMO 2


    Bonus Question

    Is it possible to generalize the approach? For instance given any function f use SFINAE to match it, using the code you posted in DEMO 2, and passing parameters from the user code (e.g., my_traits::eval(it_does, parameter, parameter))?

    template <typename T, typename... Args>
    static auto call(T&& t, int, Args&&... args)
        -> decltype(void(std::forward<T>(t).open_file(std::forward<Args>(args)...)))
    {
        std::forward<T>(t).open_file(std::forward<Args>(args)...);
    }
    
    template <typename T, typename... Args>
    static void call(T&& t, void*, Args&&... args)
    {
        std::cout << "open_file() not called" << std::endl;
    }
    
    template <typename T, typename... Args>
    static void eval(T&& t, Args&&... args)
    {
        call(std::forward<T>(t), 0, std::forward<Args>(args)...);
    }
    
    eval(it_does, 1);    // MATCHES!
    eval(it_doesnt, 2);  // open_file() not called
    eval(it_does);       // open_file() not called
    eval(it_doesnt);     // DOESN'T!
    

    DEMO 3