4

I'm attempting to use SFINAE to detect if a type passed as a template argument T has an T::operator()(P const&) where P is also a template argument. I modeled my solution after this example of the Member Detector Idiom Unfortunately, I could not get this working for an operator(), even though I could get it to function for a normal method.

Here's some sample code that demonstrates the problem I'm facing:

#include <iostream>
#include <iomanip>
#include <utility>
#include <type_traits>

using namespace std;

struct has
{
    void operator()(int const&);    
};

struct hasNot1
{
    void operator()(int);   
};

struct hasNot2
{
    void operator()();  
};

struct hasNot3
{
    void operator()(float); 
};

struct hasNot4
{
};

template<typename T, typename EDT>
struct is_callable_oper
{
      private:
                    typedef char(&yes)[1];
                    typedef char(&no)[2];

                    template <typename U, void (U::*)(EDT const &)> struct
                                                                        Check;
                    template<typename>
                    static yes test(...);

                    template <typename U>
                    static no
                            test(Check<U, &U::operator()>*);

                public:
                    static constexpr bool value = sizeof(test<T>(0))
                                                                == sizeof(yes);
};

int main() {
    cout << boolalpha << is_callable_oper<has, int&>::value << " " 
         << is_callable_oper<has, int>::value << " "
         << is_callable_oper<hasNot1, int&>::value << " "
         << is_callable_oper<hasNot2, int&>::value << " "
         << is_callable_oper<hasNot3, int&>::value << " "
         << is_callable_oper<hasNot4, int&>::value << endl;
    return 0;
}

Running it on ideone (https://ideone.com/tE49xR) produces: true false true true true true

I expected: true false false false false false

Work Done:

  1. Read this Stackoverflow question. I've also followed related links.

  2. Looked up std::declval, decltype. I've also studied a bit on how the non-type template parameter results in an ambiguity. I've been using http://en.cppreference.com/ primarily.

  3. Read some other related questions and links.

Note: I'm working on gcc 4.6.3 with C++ 0x.

End Goal: Detect all callables function pointers included with this signature.

Related Clarifications: I'm still confused about some concepts, and I would be grateful if you could answer these. Of course, if they belong in a separate question, please do let me know.

  1. Can we trigger the ambiguity for SFINAE by using declval rather than a Check template for this case?

  2. What is the function type for an overloaded operator? For example, would the overloaded operator in this case have the following type: void (operator())(EDT const&)?

  3. As CV Qualifiers are discarded during this check, what is the best way to check for the const-ness of the argument I'm passing.\

  4. I haven't been able to figure out a way to use Boost to do this. I'm also stuck using an older Boost version 1.43 (Will check and update the exact), I believe. If there is no reason to roll my own check, that may be for the best.

I'm still very much a beginner in this area, and I sincerely apologize if this is too basic. I would appreciate it if you can point me to additional resources that you think I should be looking at as well. In the meantime, I'll keep searching online and trying solutions.


EDIT 1


After discussing the problem with @Nir Friedman, I have come to appreciate that breaking implicit conversion rules and achieving an exact match is not what I want. So long as the passed type can be converted, it should be fine. I would appreciate a pointer on how to achieve this.


EDIT 2


I'm marking this question as closed because @Sam Varshavchik answered the exact question I asked. If anyone is interested in the question posed in EDIT 1, I'll either ask it as a separate question, or post my solution here for reference.

Community
  • 1
  • 1
batbrat
  • 5,155
  • 3
  • 32
  • 38
  • Is it really not possible for you to get a newer compiler? TMP is way, way easier in newer versions of the language. I can solve this problem for you in like 5 lines of code with newer compilers. – Nir Friedman Mar 03 '17 at 15:46
  • I'm stuck with this one for now. I'd love to see your solution with a more modern compiler; please do share it with me. – batbrat Mar 03 '17 at 15:48
  • 1
    There's a very subtle issue here. If a parameter to a function is, say a `float`, you can pass an `int &`, since this will be convertible to a `float`. You need to decide whether this is "callable" or not, for you. Using `decltype` and `declval` will result in your operator deemed to be callable in this case. If you want to check the operator signature's strictly, this will only be possible if you also specify the operator's return type. – Sam Varshavchik Mar 03 '17 at 15:56
  • ... and if you use `decltype` and `declval`, the correct results of your test program should be `true true true false true false`. – Sam Varshavchik Mar 03 '17 at 15:58
  • @Sam Varshavchik so conversion rules are biting me; good to know. How exactly would I use decltype and declval to improve this? My return type is fixed -> void. How can I exploit that? Thanks for your help! – batbrat Mar 03 '17 at 16:29

2 Answers2

2

On more modern compilers, the typical approach for this kind of problem is the void_t idiom: How does `void_t` work. Here's how it goes. Step 1:

template <class T>
using void_t = void;  // avoid variadics for simplicity

Obviously this is general and not specific to your problem but it just doesn't happen to be in the standard library until 17.

Next, we define our trait and give the default detection as false:

template <class T, class P, class = void>
struct is_callable_oper : std::false_type {};

Now, we defined a specialized form that will work out if our function has the operator we want.

template <class T, class P>
struct is_callable_oper<T, P, 
    void_t<decltype(std::declval<T>()(std::declval<P>()))>>: std::true_type {};

The basic idea is that void_t as we know will always translate to void. But, the type being input will not make any sense unless the expression is valid; otherwise it will cause substitution failure. So basically, the code above will check whether

std::declval<T>()(declval<P>())

Is sensible for your type. Which is basically the trait detection that you want. Note that there are some subtleties involving lvalues, rvalues, constness, but that would be a bit involved to get into here.

Now, as Sam notes in the comments, whether this expression is sensible or not includes implicit conversions. So the output indeed is true true true false true false. Working example: http://coliru.stacked-crooked.com/a/81ba8b208b90a2ba.

For instance, you expected is_callable_oper<has, int>::value to be false. However, clearly one can call a function accepting const int& with an int. So instead you get true.

Community
  • 1
  • 1
Nir Friedman
  • 17,108
  • 2
  • 44
  • 72
  • Thanks for the detailed answer. I'll take a while to chew on it before asking any questions. That said, I've been wondering if there's any way to restrict type conversion here. – batbrat Mar 03 '17 at 16:36
  • 1
    @batbrat Sam's answer may be better for what you want. Although, I will say, it is one of those situations that smacks of XY problem. In C++ you generally care about whether you can do something, e.g. whether something is callable with a certain type. Not whether the signature is an exact match. In some sense, by doing this, you may well be breaking the contract of implicit conversions. – Nir Friedman Mar 03 '17 at 16:54
  • you're absolutely right - I don't need to restrict for an exact match; in fact, I shouldn't. – batbrat Mar 04 '17 at 04:08
2

You clarified that your operator() returns a void, and you want to strictly match the signature, ignoring type conversions.

If that's so, then your expected results should be false true false false false false, and not true false false false false false:

 is_callable_oper<has, int&>::value

Since has::operator() does not take a int & const & parameter, which collapses to an int &, the result of this test must be false.

 is_callable_oper<has, int>

Since has does, indeed, have an operator() that takes a const int & parameter, this test should pass.

My solution simply uses std::is_same to compare two types, and std::enable_if to SFINAE-fail a template resolution candidate.

#include <type_traits>
#include <iostream>

struct has
{
    void operator()(int const&);
};

struct hasNot1
{
    void operator()(int);
};

struct hasNot2
{
    void operator()();
};

struct hasNot3
{
    void operator()(float);
};

struct hasNot4
{
};

template<typename T, typename P, typename foo=void>
class is_callable_oper : public std::false_type {};

template<typename T, typename P>
class is_callable_oper<T, P, typename std::enable_if<
         std::is_same<decltype(&T::operator()),
                      void (T::*)(const P &)>::value>::type>
    : public std::true_type {};

int main() {
    std::cout << std::boolalpha << is_callable_oper<has, int&>::value << " "
         << is_callable_oper<has, int>::value << " "
         << is_callable_oper<hasNot1, int&>::value << " "
         << is_callable_oper<hasNot2, int&>::value << " "
         << is_callable_oper<hasNot3, int&>::value << " "
          << is_callable_oper<hasNot4, int&>::value << std::endl;
    return 0;
}

EDIT: By using std::void_t, or a reasonable facsimile, in the specialization it should also be possible to make this work with overloaded operators:

template<typename T, typename P>
class is_callable_oper<T, P,
        std::void_t<decltype( std::declval< void (T::*&)(const P &)>()=&T::operator())>>
    : public std::true_type {};
Sam Varshavchik
  • 114,536
  • 5
  • 94
  • 148
  • Better than my solution, taking into account what OP asked for. – Nir Friedman Mar 03 '17 at 16:52
  • Not `const int & &`, `int& const&`... which is to say `int&`. Also this won't work with overloadable call operators. – Barry Mar 03 '17 at 16:53
  • @SamVarshavchik, why exactly does int& const& loose the const? This is the cause of the mismatch, isn't it? I'm also curious about how you would achieve this test without going for an exact match - as Nir Friedman rightly pointed out, I shouldn't do an exact match. I've edited the question to reflect this. – batbrat Mar 04 '17 at 04:13
  • 1
    You need to post a revised question, that minimally, but explicitly, spells out what you're trying to achieve, and, most importantly, shows the work you already did and explain what you are missing (so that your question doesn't just come out to be a synonym "please write my code for me"). As is now, this question is became too tangled, and is difficult to follow. You should start with a clean slate. – Sam Varshavchik Mar 04 '17 at 13:36
  • @SamVarshavchik Good point. I'll remove the edit and accept your answer; after that, I'll start with a clean slate and ask a question properly. I definitely don't want anyone to write the code for me: the whole point is to do understand enough to do this by myself. – batbrat Mar 06 '17 at 09:19