Detecting the Presence of a Member Function

In this blog entry, I present a code snippet to determine whether a given type contains a specific member function. The caller can indicate what types of parameters he intends on passing into the function.

A short while ago I wanted to implement some concepts. All I wanted were some fairly rudimentary ones which simply allowed me to categorize a type based on it’s interface so that later template algorithms could then be optimized based on how much functionality the given type had.

Think iterator classification but with their category being inferred from a compile-time inspection of the iterator’s member functions rather than the traits it ‘exports’.

 

The Basic Technique

This kind of thing has been around for a while. There are any number of variations which all differ on the details but the general technique remains largely the same:

The member function is mentioned in a meaningful way somewhere in a function signature for a templated function overload. SFINAE kicks in and if the binding cannot be done because the member function does not exist, the function overload is rejected from the pool of considerations.

 template< TYPE >
 struct has_callable_Foo
 {
 private:
 	template< CHECKTYPE >
 	static char check( /* mention CHECKTYPE::Foo in some way */ );
 	static long check(...);
public:
	static const bool value = /* typically use sizeof() to look at the return type of check */;
 };

In case this is new to you, what this code structure does is this:

It allows you to query has_callable_Foo<>::value with any type as the template parameter. The compiler will statically determine whether value is true or false by evaluating an expression contained in the sizeof() clause.

sizeof() is used to determine which overload of the check() function would be called by the evaluated expression. You’ll note the two versions of check return differently sized values (one is a char, the other a long to help determine which one would be called).

The first implementation of check() contains something which tries to refer to the member function we’re testing for on the TYPE/CHECKTYPE which is given. If that does not exist, the ‘catch-all’ version of check is used.

 

The Issue With the Old Way

Typically the meat of this function is in how the compiler is asked to check for the existence of Foo. Typically this has been done using a member function pointer.

One of the main limitations with this technique is that it asks the wrong kind of question.

It asks “does the given type contain a function with these parameter types?”. The problem with this question is that it is too restrictive.

If Foo is declared as:

 class MyObject
 {
 	public:
 		void Foo( const int& );
 };

…and I ask whether MyObject contains a function Foo() which takes an int, clearly the answer is no. It does not take an int. It takes a const int&.

However I think a much more useful question to ask is “does the given type contain a function which can _accept_ these parameter types?”. If we can get an implementation which asks _that_ question, we’ll get a very different answer.

Yes, while Foo formally accepts a const int&, it certainly can accept an int too! Both an lvalue and rvalue int will happily bind to a const int&.

 

A New Way of Implementing

Luckily with C++11 we actually have language support to do this! With decltype(), declval() and variadic templates we can actually answer a better question AND improve the usability of the function across the board.

I’ll cut to the chase and paste the new implementation here:

template< typename ARG >
static typename std::conditional<
std::is_rvalue_reference<ARG>::value,
ARG,
typename std::add_lvalue_reference<ARG>::type >::type
refval();

#define HAS_MEMBER_CHECK(FNAME) \
template< class T, typename... ARGS > \
struct has_callable_##fname \
{ \
private: \
struct no; \
template< typename CHECK_TYPE, typename... CHECK_ARGS > \
static decltype(std::declval<CHECK_TYPE>().FNAME(refval<CHECK_ARGS>()...)) check(int); \
template< typename CHECK_TYPE, typename... CHECK_ARGS > \
static no check(...); \
public: \
static const bool value = !std::is_same< decltype(check<T, ARGS...>(0)), no >::value; \
};

This requires the <type_traits> include.

Here is a quick example of how to use the above snippet:

// expand the macro to test for a function named 'Foo'
HAS_MEMBER_CHECK(Foo);

class MyType
{
public:
void Foo( const int& );
};

int main()
{
std::cout << "Does MyType support calling Foo() with an lvalue int parameter?";
std::cout << has_callable_Foo< MyType, int >::value << std::endl;</pre>
<pre>std::cout << "Does MyType support calling Foo() with an rvalue int parameter?";
std::cout << has_callable_Foo< MyType, int&& >::value << std::endl;</pre>
<pre>}

 

Some Things To Mention

External to the struct definition I give is an implemention of refval() as I have called it. I want to mention two things about it – one is why I have it, and the other is why you may want to place it inside the struct has_callable rather than outside.

To explain why I have it, it’s easier to give an example of the problem.

If you’re going to ask whether a function will accept a parameter of type int, it’s actually an ambiguous question. There are two kinds of value you may have. One is as an lvalue and the other an rvalue.

int i = 1;
Foo( i ); //using an lvalue
Foo( 5 ); // using an rvalue

When we ask the compiler to evaluate an expression (such as inside the decltype in the has_callable snippet) we have to be specific which kind of parameter is going to be passed in. If the caller has an lvalue of type int, then it can bind to an int&. However an rvalue of type int cannot. Since the decltype is only evaluated and not compiled, a compile-time technique is needed for giving the compiler a valid expression for each parameter.

Were declval() to be used it would incorrectly assume that when the parameter is by-value, it will be an rvalue. So the refval() simply assumes any by-value parameter might be an lvalue by giving it a reference.

So:

refval< int >() // returns an int& -- behaves like an lvalue is being passed in
refval< int& >() // returns an int& -- remains the same
refval< int&& >(); // returns an int&& -- behaves like an rvalue is passed in

Now if you’re wanting to see whether the function would take an rvalue, you need to use the new C++11 notation for it: int&&

Another place where this makes a big difference is when the parameter is move-only (ie: it cannot be copied). Admittedly it is a niche case, but using refval() ensures that when the caller indicates the intended use is by-value with an lvalue then the has_callable snippet will correctly evaluate to false. If the caller indicates the intended use is specifically by rvalue, then it will evaluate to true since the rvalue can be moved.

I said there were two things I wanted to mention about the refval(). The second point is that I have declared it outside of the has_callable struct. I like to keep macro’s as short as possible. Thats why. However if you put the entire snippet inside a namespace, but then instantiate the macro (HAS_MEMBER_CHECK) outside of the namespace then clearly the macro will not have access to refval() which will be in a different scope. The easy fix here is to just put refval() as a member of the has_callable struct so it is guaranteed to always be in scope.

Finally one final remark about this code snippet: It cannot know about friend status. Now that I’ve mentioned it, it might seem obvious but it’s worth keeping in mind. Should you have a function or class A which is a friend of object B, then asking this snippet whether B can call a private member function on A will result in ‘false’ when actually the member function is visible within the intended context.

Thats it 🙂 An interesting experiment in C++ none-the-less, and probably a temporary one at that (see: Concepts).

Advertisements
Detecting the Presence of a Member Function

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s