How to detect if a function exists?
Asked Answered
P

3

7

I'm trying to detect if a specific overload for my function is callable. I assumed I could do something similar to this answer, but I believe the issue is that the function signature template<typename From, typename To> convert(const From&) is well defined, but the instantiation is not.

#include <iostream>
#include <string>

template<typename From, typename To>
To convert(const From& from)
{
    // I have a lot of additional template specializations for this function
    return from; 
}

template<typename From, typename To>
struct IsConvertible
{
    template<typename = decltype(convert<From, To>(From()))>
    static std::true_type test(int);
    template<typename T>
    static std::false_type test(...);

    static bool const value = decltype(test(0))::value;
};

int main()
{
    std::cout << "IsConvertible=" << IsConvertible<int, float>::value << std::endl;
    // Returns 1 as expected

    std::cout << "IsConvertible=" << IsConvertible<int, std::string>::value << std::endl;
    // Returns 1, expected 0. The issue seems to be that decltype(convert<From, To>(From()))
    // is somehow ok, although convert<int, std::string>(1) definitly isn't
}

I want to use IsConvertible for some additional metaprogramming. Is it possible to detect if the template<typename From, typename To> To convert(const From&) function is actually callable?`

Preoccupation answered 10/5, 2019 at 6:57 Comment(8)
No. This is called SFINAE unfriendly.Quid
My 2 cents: I tried on coliru because I struggled to believe... Live Demo on coliru.Phi
This already exists: en.cppreference.com/w/cpp/types/is_convertibleBlackcap
It's not an overload, it's a specialisation. You can detect if an overload exists, but you cannot detect if a specialisation exists. Use overloads.Creek
Googling for "SFINAE unfriendly" I found this: SFINAE-friendly std::bindPhi
@Blackcap Note that I have many specializations for the convert<From, To> function for types which does not have a trivial conversion operation, i.e. checking for std::is_convertible is not sufficient.Preoccupation
@n.m. Sorry I'm not sure I understand. Could you elaborate?Preoccupation
I'm not sure what's to elaborate. Do not use template specialisations for conversion, use overloads. The slight problem is that you cannot overload on return type, but this is easily fixable with a tag type.Creek
W
2

With declaration of

template<typename From, typename To> To convert(const From& from);

Your traits

template<typename From, typename To>
struct IsConvertible

would always detect presence of convert function.

One way to fix it is overloads and/or SFINAE:

template <typename> struct Tag{};

int convertImpl(tag<int>, const std::string& from);
float convertImpl(tag<float>, const std::string& from);
// overloads ...

template<typename From, typename To>
auto convert(const From& from)
-> decltype(convertImpl(tag<To>{}, from))
{
    return convertImpl(tag<To>{}, from);
}
Waksman answered 10/5, 2019 at 7:54 Comment(1)
Ah of course -- this makes a lot of sense. Now I understand n.m.'s as well!Preoccupation
R
1

I might have misunderstood your question but is not the using std::is_invocable enough in this case as showcased in the following?

#include<type_traits>

template<typename From, typename To>
To convert(const From& from)
{
    // I have a lot of additional template specializations for this function
    return from; 
}

template<>
std::string convert(const int& from)
{
    //silly specialization
    return "2"+from; 
}


struct Foo{
    int bar;
};

int main()
{
   //ok specialization is called 
   std::cout<<std::is_invocable<decltype(convert<int,std::string>),std::string>::value<<std::endl; 
   //no way I can convert int to Foo, specialization required
   std::cout<<std::is_invocable<decltype(convert<int,Foo>),Foo>::value<<std::endl; 
return 0;
}
Reconcilable answered 10/5, 2019 at 7:44 Comment(3)
This question is tagged C++11.Loner
I didn't know about std::invocable, so +1 for that. But as the previous commenter indeed said, I'm unfortunately stuck with C++11.Preoccupation
@Preoccupation what you might want to do is to look into how invocable is implemented and see if you can do something similar. good luck.Reconcilable
C
1

I see some problems in your code.

Without a particular order...

(1) SFINAE, using decltype(), check only the presence of a declared function; doesn't check if that function is defined or if it's definition works (compile) or not.

I propose you to rewrite convert() using directly SFINAE to declare it only when is compilable

template <typename To, typename From,
          decltype( To(std::declval<From>()), bool{} ) = true>
To convert (From const & f)
 { return f; }

This way convert() is declared only if you can construct a To object starting from a From object.

(2) Observe that I've also switched the order of To and From: this way you can call the convert() function explicating only the To type

convert<float>(0); // From is deduced as int from the 0 value

If you declare To (that isn't deducible) after From (that is deducible), you necessarily have to explicit both types, calling the function, also when the From type is deducible.

(3) Your IsConvertible struct doesn't works.

It's a common error using SFINAE.

When you write

template<typename = decltype(convert<From, To>(From()))>
static std::true_type test(int);

you're trying enable/disable this test() method using SFINAE over From and To that are the template parameters of the struct

Wrong.

SFINAE works over template parameters of the method itself.

If you want to use SFINAE, you have to transform From and To in template parameters of the method; by example

template <typename F = From, typename T = To,
          typename = decltype(convert<F, T>(std::declval<F>()))>
static std::true_type test(int);

Now SFINAE uses F and T that are template parameters of the test() method and this is correct.

(4) Observe that I've written std::declval<F>() instead of F(). It's because you're not sure that F (From) is default constructible. With std::declval() you go around this problem.

I propose a different IsConvertible custom type traits that take in count the From/To inversion and demand to the value call of test() the From+To->F+T type conversion

template <typename To, typename From>
struct IsConvertible
 {
   template <typename T, typename F,
             typename = decltype(convert<T>(std::declval<F>()))>
   static std::true_type test(int);

   template <typename...>
   static std::false_type test(...);

   static bool const value = decltype(test<To, From>(0))::value;
 };

(5) you're expecting that

IsConvertible<int, std::string>::value

is zero; but you forgetting that std::string is constructible from int; so this value (or IsConvertible<std::string, int>, switching To and From) should be one.

The following is a corrected full working example

#include <iostream>
#include <string>
#include <vector>

template <typename To, typename From,
          decltype( To(std::declval<From>()), bool{} ) = true>
To convert (From const & f)
 { return f; }

template <typename To, typename From>
struct IsConvertible
 {
   template <typename T, typename F,
             typename = decltype(convert<T>(std::declval<F>()))>
   static std::true_type test(int);

   template <typename...>
   static std::false_type test(...);

   static bool const value = decltype(test<To, From>(0))::value;
 };

int main ()
 {
   std::cout << "IsConvertible=" << IsConvertible<float, int>::value
      << std::endl;

   std::cout << "IsConvertible=" << IsConvertible<int, std::string>::value
      << std::endl;
 }
Cabin answered 10/5, 2019 at 8:14 Comment(1)
The missing thing in this example is that my convert function has many specializations/overloads for non-trivially convertible types, and so adding in decltype( To(std::declval<From>()), bool{} ) = true> as a condition will not work. However, your #3 and #4 are correct and I've used it together with the answer from Jarod. Much appreciated!Preoccupation

© 2022 - 2024 — McMap. All rights reserved.