Categories
c++ sfinae template-meta-programming templates

Templated check for the existence of a class member function?

594

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.

3

370

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( decltype(&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.

21

  • 20

    Although, I used the following for ‘one’ and ‘two’: typedef char Small; class Big{char dummy[2];} to ensure no ambiguity about platform dependent variable size.

    – user23167

    Nov 2, 2008 at 21:40

  • 7

    I doubt it exists on earth a platform with the sizeof(char) == sizeof(long)

    Nov 2, 2008 at 21:46

  • 18

    I’m not entirely sure, but I don’t think this is portable. typeof is a GCC extension, this will not work on other compilers.

    Nov 2, 2008 at 21:52

  • 58

    typeof isn’t needed – char[sizeof(&C::helloworld)] works as well. And to avoid sizeof(long)==sizeof(char), use a struct { char[2] };. It must have a size >=2

    – MSalters

    Feb 2, 2009 at 9:03

  • 58

    Trivial, but took me a while to figure out: replace typeof by decltype when using C++0x, e.g., via -std=c++0x.

    – hrr

    Jun 10, 2011 at 16:46

290

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.

12

  • 4

    The advantage of decltype over sizeof is also that a temporary is not introduced by specially crafted rules for function calls (so you don’t have to have access rights to the destructor of the return type and won’t cause an implicit instantiation if the return type is a class template instantiation).

    Apr 11, 2014 at 17:29


  • 6

    Microsoft has not implemented Expression SFINAE in it’s C++ compiler yet. Just figure I might help save some people time, as I was confused why this wasn’t working for me. Nice solution though, can’t wait to use it in Visual Studio!

    – Jonathan

    Jan 26, 2015 at 6:22

  • 1

    It has to be said, that static_assert(has_stream<X, char>() == true, "fail X"); will compile and not assert because char is convertable to int, so if that behavior is not wanted and want that all argument types match i dont know how that can be achieved?

    – Gabriel

    Oct 4, 2015 at 14:10


  • 4

    If you are as puzzled as I was on the two arguments to decltype: decltype really only takes one; the comma is an operator here. See stackoverflow.com/questions/16044514/…

    – André

    Feb 17, 2017 at 6:06


  • 1

    This works perfectly in situations that require complete types, but in situations that don’t this will give false negatives for incomplete (forward declared) types. I added a sfinae_false counterpart and used a return type on the long override that detected for the presence of a destructor. This excluded types that were still incomplete or didn’t have public destructors. Excluding non-public destructors was acceptable for me.

    – John

    May 22, 2017 at 18:23

167

C++ allows SFINAE to be used for this (notice that with C++11 features this is simplier because it supports extended SFINAE on nearly arbitrary expressions – the below was crafted to work with common C++03 compilers):

#define HAS_MEM_FUNC(func, name)                                        \
    template<typename T, typename Sign>                                 \
    struct name {                                                       \
        typedef char yes[1];                                            \
        typedef char no [2];                                            \
        template <typename U, U> struct type_check;                     \
        template <typename _1> static yes &chk(type_check<Sign, &_1::func > *); \
        template <typename   > static no  &chk(...);                    \
        static bool const value = sizeof(chk<T>(0)) == sizeof(yes);     \
    }

The above template and macro tries to instantiate a template, giving it a member function pointer type, and the actual member function pointer. If the types do not fit, SFINAE causes the template to be ignored. Usage like this:

HAS_MEM_FUNC(toString, has_to_string);

template<typename T> void
doSomething() {
   if(has_to_string<T, std::string(T::*)()>::value) {
      ...
   } else {
      ...
   }
}

But note that you cannot just call that toString function in that if branch. Since the compiler will check for validity in both branches, that would fail for cases the function doesn’t exist. One way is to use SFINAE once again (enable_if can be obtained from boost, too):

template<bool C, typename T = void>
struct enable_if {
  typedef T type;
};

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

HAS_MEM_FUNC(toString, has_to_string);

template<typename T> 
typename enable_if<has_to_string<T, 
                   std::string(T::*)()>::value, std::string>::type
doSomething(T * t) {
   /* something when T has toString ... */
   return t->toString();
}

template<typename T> 
typename enable_if<!has_to_string<T, 
                   std::string(T::*)()>::value, std::string>::type
doSomething(T * t) {
   /* something when T doesnt have toString ... */
   return "T::toString() does not exist.";
}

Have fun using it. The advantage of it is that it also works for overloaded member functions, and also for const member functions (remember using std::string(T::*)() const as the member function pointer type then!).

19

  • 7

    I like how type_check is used to ensure that the signatures agree exactly. Is there a way to make it so that it will match any method which could be called in the manner that a method with signature Sign could be called? (E.g. if Sign = std::string(T::*)(), allow std::string T::toString(int default = 42, ...) to match.)

    Nov 17, 2010 at 1:37

  • 5

    I just figures something out about this that wasn’t immediate obvious to me, so in case it helps others: chk isn’t and needn’t be defined! The sizeof operator determines the size of the output of chk without chk ever needing to be called.

    – SCFrench

    Jan 23, 2011 at 17:24

  • 3

    @deek0146: Yes, T must not be a primitive type, because the pointer-to-method-of-T declaration is not subject to SFINAE and will error out for any non-class T. IMO the easiest solution is to combine with is_class check from boost.

    – Jan Hudec

    May 22, 2012 at 12:04

  • 2

    How can I make this work if my toString is a templated function?

    – Frank

    Aug 20, 2012 at 2:00


  • 4

    Is this (or anything equivalent) in Boost?

    Mar 29, 2013 at 6:24