Is it possible to write a template to check for a function's existence?


Question

Is it possible to write a template that changes behavior depending on if a certain member function is defined on a class?

Here's a simple example of what I would want to write:

template<class T>
std::string optionalToString(T* obj)
{
    if (FUNCTION_EXISTS(T->toString))
        return obj->toString();
    else
        return "toString not defined";
}

So, if class T has toString() defined, then it uses it; otherwise, it doesn't. The magical part that I don't know how to do is the "FUNCTION_EXISTS" part.

1
445
5/3/2017 5:35:37 PM

Accepted Answer

Yes, with SFINAE you can check if a given class does provide a certain method. Here's the working code:

#include <iostream>

struct Hello
{
    int helloworld() { return 0; }
};

struct Generic {};    

// SFINAE test
template <typename T>
class has_helloworld
{
    typedef char one;
    struct two { char x[2]; };

    template <typename C> static one test( typeof(&C::helloworld) ) ;
    template <typename C> static two test(...);    

public:
    enum { value = sizeof(test<T>(0)) == sizeof(char) };
};

int main(int argc, char *argv[])
{
    std::cout << has_helloworld<Hello>::value << std::endl;
    std::cout << has_helloworld<Generic>::value << std::endl;
    return 0;
}

I've just tested it with Linux and gcc 4.1/4.3. I don't know if it's portable to other platforms running different compilers.

302
5/27/2019 10:56:30 AM

This question is old, but with C++11 we got a new way to check for a functions existence (or existence of any non-type member, really), relying on SFINAE again:

template<class T>
auto serialize_imp(std::ostream& os, T const& obj, int)
    -> decltype(os << obj, void())
{
  os << obj;
}

template<class T>
auto serialize_imp(std::ostream& os, T const& obj, long)
    -> decltype(obj.stream(os), void())
{
  obj.stream(os);
}

template<class T>
auto serialize(std::ostream& os, T const& obj)
    -> decltype(serialize_imp(os, obj, 0), void())
{
  serialize_imp(os, obj, 0);
}

Now onto some explanations. First thing, I use expression SFINAE to exclude the serialize(_imp) functions from overload resolution, if the first expression inside decltype isn't valid (aka, the function doesn't exist).

The void() is used to make the return type of all those functions void.

The 0 argument is used to prefer the os << obj overload if both are available (literal 0 is of type int and as such the first overload is a better match).


Now, you probably want a trait to check if a function exists. Luckily, it's easy to write that. Note, though, that you need to write a trait yourself for every different function name you might want.

#include <type_traits>

template<class>
struct sfinae_true : std::true_type{};

namespace detail{
  template<class T, class A0>
  static auto test_stream(int)
      -> sfinae_true<decltype(std::declval<T>().stream(std::declval<A0>()))>;
  template<class, class A0>
  static auto test_stream(long) -> std::false_type;
} // detail::

template<class T, class Arg>
struct has_stream : decltype(detail::test_stream<T, Arg>(0)){};

Live example.

And on to explanations. First, sfinae_true is a helper type, and it basically amounts to the same as writing decltype(void(std::declval<T>().stream(a0)), std::true_type{}). The advantage is simply that it's shorter.
Next, the struct has_stream : decltype(...) inherits from either std::true_type or std::false_type in the end, depending on whether the decltype check in test_stream fails or not.
Last, std::declval gives you a "value" of whatever type you pass, without you needing to know how you can construct it. Note that this is only possible inside an unevaluated context, such as decltype, sizeof and others.


Note that decltype is not necessarily needed, as sizeof (and all unevaluated contexts) got that enhancement. It's just that decltype already delivers a type and as such is just cleaner. Here's a sizeof version of one of the overloads:

template<class T>
void serialize_imp(std::ostream& os, T const& obj, int,
    int(*)[sizeof((os << obj),0)] = 0)
{
  os << obj;
}

The int and long parameters are still there for the same reason. The array pointer is used to provide a context where sizeof can be used.


Licensed under: CC-BY-SA with attribution
Not affiliated with: Stack Overflow
Icon