Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

Interface Detection

4.95/5 (33 votes)
24 Jul 200714 min read 1   341  
Detecting the presence of a member in a class

Contents

Introduction

In any C++ program, calling a function that hasn't been declared obviously leads to a compilation error. Detecting the interface of a class lets the programmer check the presence of a given public member function or data without generating such errors, allowing him to specify a different behavior when the member doesn't exist. Interface detection doesn't use any inheritance properties and, as such, offers new cleaner and safer solutions. This article describes a way to implement such detection facilities, by exploring and explaining some advanced C++ topics.

Motivating example

Suppose we want to write a container class that provides all standard container operations such as push, pop, insert, etc. and adds new functionalities unseen in any other container. We will call this new container class MetaContainer. Its implementation relies on a given classic container -- typically STL ones such as std::vector and std::list -- but user defined containers are also accepted. Consequently, the MetaContainer class defines a template parameter that accepts a basic container class.

Usage

C++
MetaContainer< int, std::vector > myContainer1;
MetaContainer< double, std::list > myContainer2;
MetaContainer< int, UserContainer > myContainer3;

The programmer can then choose the underlying container to tune performance. For example, if a lot of insertions in the middle of the collection are performed, std::list is a better choice than std::vector. However, the Container template argument isn't forced to follow a strict interface. For instance, std::list provides a remove function whereas std::vector does not. The MetaContainer class does provide a remove function though, whose implementation calls the remove function of the underlying container, along with other internal operations.

C++
void MetaContainer::Remove(...) 
{
    ... 
    m_UnderlyingContainer.remove(...); 
    ... 
}

If the underlying container class, such as std::vector or some other user container, doesn't declare a remove function, then a generic, low performance remove algorithm is used. To apply this strategy, we need to answer the following question: How can we know whether the container class has a remove function, so that we can silently switch to the generic remove implementation when the container doesn't provide one?

Required technical background

Before explaining the solution, some key C++ concepts used in the interface detection implementation must be reviewed. In this chapter, the following topics are tackled in a very concise way:

  • Pointers to member syntax and behavior
  • Dependent names
  • Pointers to members as non-type template arguments
  • SFINAE

If you already know all of those concepts, you may jump directly to the next chapter.

Pointers to member

Introduction

Pointers to members can be divided into 4 categories:

  • Pointers to non-static member functions
  • Pointers to non-static data members
  • Pointers to static member functions
  • Pointers to static data members

Example:

C++
struct MyClass 
{
    void MF (int); // non static Member Function
    int  MD;       // non static Data Member

    static void Static_MF (int); // static Member Function
    static int  Static_MD;       // static Data Member
}

We'll refer to those 4 member declarations in the rest of the chapter.

Syntax of pointers to member

Pointers to each of the aforementioned members adopt a homogeneous syntax:

MemberPointer
MF&MyClass::MF
MD&MyClass::MD
Static_MF&MyClass::Static_MF
Static_MD&MyClass::Static_MD

Because the syntax of pointers to members includes the name of a class, such pointers can be dependent names. In contrast, pointers to ordinary functions or data can't. A dependent name is a name that depends on a template parameter. For example:

C++
template< class T >
void f () 
{
    ...
    pf = &T::MF; // pointer dependent on the T parameter
}

The name dependence property plays an important role in our solution.

Type of pointers to members

Type of pointers to member functions

The pointers to member functions have the following types:

PointerType
&MyClass::MFvoid (MyClass::*)(int)
&MyClass::Static_MFvoid (*)(int)

Notice how the type of a pointer to a static member function can be misleading. A non-static member function follows a specific convention: it adds an implicit parameter that accepts a pointer to an object. A static member function doesn't apply to an object and doesn't have an implicit object parameter. A pointer to such a function has the same type as an ordinary function pointer. 1 The important point here is that pointers to non-static and static member functions have different types that are incompatible with each other.

Type of pointers to data members

Types of pointers to static and non-static data members follow the same syntax differences as their function counterparts:

PointerType
&MyClass::MDint MyClass::*
&MyClass::Static_MDint*

The types of pointers to static and non-static members are again different and incompatible. This is because the latter is associated with an object and thus contains an offset rather than an address.

Pointers to member as non-type template arguments

A pointer to a member can be a non-type template argument. The syntax is straightforward. As an example, we'll use each of the types of pointers we saw above as template parameters:

C++
template < void (MyClass::*)(int) >  
void f1 () {}

template < void (*)(int) >  
void f2 () {}

template < int MyClass::* >  
void f3 () {}

template < int* >  
void f4 () {}

f1 < &MyClass::MF > (); // ok
f1 < &MyClass::Static_MF > (); // Error: type mismatch
f2 < &MyClass::Static_MF > (); // ok

A pointer to a member as a template argument must be of the exact type specified in the template declaration. There is no possible conversion.

SFINAE

SFINAE, an acronym for "Substitution Failure Is Not An Error," is a principle that works during function overload resolution as follows: if the instantiation of a template function produces an invalid parameter or return type, the compiler silently removes the ill-formed function instantiation from the overload resolution set. For example:

C++
struct Test 
{
    typedef int Type;
};

template < typename T > 
void f( typename T::Type ) {} // definition #1


template < typename T > 
void f( T ) {}                // definition #2


f< Test > ( 10 ); //call #1 
f< int > ( 10 );  //call #2 without error thanks to SFINAE

Without SFINAE, the second call would have generated an error during the substitution of the template parameter in #1. The result of the instantiation of #1 from the second call is: void f( typename int::Type ) {}. Thanks to SFINAE, the resulting function instantiation doesn't generate an error because there is another function that matches the call.

Interface detection implementation

Member function detection

As pointed out in the introduction, using the identifier of an undeclared function generates a compilation error. Checking if a function exists logically necessitates the use of its identifier, as we don't use any inheritance property or any extra information. We must find a mechanism that doesn't generate an error when the identifier refers to an undeclared function. SFINAE fits well in this task.

Since member function pointers can be dependent names, we could use the SFINAE principle on them. However, SFINAE is applied to the parameter type or return type of a function. A member function pointer isn't a type and can't be directly used in a function signature. To overcome this, the function pointer is used as a template argument:

C++
template < void (MyClass::*)() >
struct TestNonStatic { };

With the TestNonStatic structure, we can use a pointer to a non-static member function in a parameter or return type of a function:

C++
template < class T >
TestNonStatic<&T::foo> Test( );

SFINAE doesn't generate an error as long as there is another function in the overload resolution set. We need to declare a function that will be used as a "fallback" when the member function -- foo, in the example -- doesn't exist. A function with the ellipsis parameter is the right candidate for this job since such a function always has the lowest priority in the overload resolution process and matches any argument.

C++
template < class T >
void Test( ... );

A function with the ellipsis parameter is always the lowest priority function in an overload resolution set, isn't it? The C++ standard says yes, Visual C++ 8 says yes, GCC says no for our case. 2 The alternative solution is to simply use the TestNonStatic structure as a parameter to the Test function instead of a return type.

The next step is to find a way of knowing at compile-time which Test function will be selected. The trick commonly used in template metaprogramming is to specify a different return type for each function declaration and employ sizeof on a call of the function. The return types must then have different sizes to obtain different results from sizeof. The sizeof operand isn't evaluated. That's why the function called inside sizeof doesn't need to be defined. The following example determines whether the class MyClass contains a member function void foo():

C++
// Return types for sizeof
typedef char NotFound;  
struct NonStaticFound { char x[2]; }; 
struct StaticFound { char x[3]; }; 

// Test Structures for SFINAE
template < void (MyClass::*)() >
struct TestNonStatic ;

template < void (*)() >
struct TestStatic ;

// Overload functions
template < class T >
StaticFound 
Test( TestStatic< &T::foo >* );

template < class T >
NonStaticFound 
Test( TestNonStatic< &T::foo >* );

template < class T >
NotFound 
Test( ... );

check_presence = sizeof( Test< MyClass >( 0 ) ); 

The final step is to wrap the whole mechanism into a reusable class from which you can specify the class and function signature to test. However, the identifier of a function -- in the above example, foo -- can't be specified as a parameter of the reusable class. Preprocessor macros are provided in the current interface detection implementation in order to deliver this need. Usage of those macros is detailed in the last chapter.

Last consideration: what about constant member functions? The above code doesn't detect them. If the user specifies a constant member signature to the reusable detector class, it won't compile because as we use the given function signature to construct the template parameter of the TestNonStatic structure, we use it also for the TestStatic structure. Static member functions can't be constant; there is no implicit object to apply the constant qualifier. Using two interfaces, one for detecting member functions and one for constant member functions, would be too cumbersome for the user.

Adding a test structure with a constant member signature to the detector class seems to deal with the problem:

C++
template < void (MyClass::*)() const >
struct TestNonStaticConst ;

Simply putting together the functions Test( TestNonStatic< &T::foo >* ) and Test( TestNonStaticConst< &T::foo >* ) in the same overload set would lead to an ambiguous resolution error when the given class has both a constant member function and non-constant member function with exactly the same signature. We need to lower the priority of one of the two functions by using the ellipsis as a second parameter:

C++
// Test Structures for SFINAE
template < void (MyClass::*)() >
struct TestNonStatic ;

template < void (*)() >
struct TestStatic ;

template < void (T::*)() const >
struct TestNonStaticConst ;

// Overloaded functions
template < class U >
NonStaticFound 
Test( TestNonStatic< &U::aff >*, ... );

template < class U >
NonStaticFound 
Test( TestNonStaticConst< &U::aff >*, int );

template < class U>
StaticFound 
Test( TestStatic< &U::aff >*, int );

template < class U >
NotFound 
Test( ... );

check_presence = sizeof( Test( 0,0 ) );

Data member detection

After seeing how to implement member function detection, doing the same for data members is straightforward. It is, in fact, easier since we don't have to care about the constant members problem. The only changes concern the template parameter of the TestStatic and TestNonStatic structures:

C++
// Return types for sizeof
typedef char NotFound;
struct NonStaticFound { char x[2]; }; 
struct StaticFound { char x[3]; }; 

// Test Structures for SFINAE 
template < int MyClass::* > // change 1 of 2
struct TestNonStatic ;

template < int * > // change 2 of 2
struct TestStatic ;

// Overload functions
template < class T >
StaticFound 
Test( TestStatic< &T::foo >* );

template < class T >
NonStaticFound 
Test( TestNonStatic< &T::foo >* );

template < class T >
NotFound 
Test( ... );

Known limitations and problems

Limitations of the current interface detector fall into 2 categories:

  • Compilation problems: everything that leads to a compilation error
  • Design limitations: what is not possible to do with the current interface detection implementation and design warnings

Compilation problems

There are two kinds of compilations errors:

  • Errors directly coming from the C++ standard
  • Compiler-specific errors

Standard compilation errors

Access checking error

Only public members are concerned by the interface detector. However, if a function given to the detector happens to exist in a private or protected section of the class, the compiler will issue an "access denied" error. Because class member access checking comes after name look-up and overload resolution, SFINAE won't silence the error.

Compiler-specific errors

This section lists non-standard errors generated by the latest C++ compilers. Of course, earlier compilers that don't fully support templates -- such as Visual C++ 6 -- are likely to give some errors, but they aren't listed here. The following table represents the detection capabilities and bugs from the currently tested compilers:

Simple member functionOverloaded member functionMember function template specializationData member
Visual C++ 8
Visual C++ 7.1
GCC 4.1
Comeau
Visual C++ data member detection bug

In Visual C++, a dependent name consisting of a pointer to a static or non-static data member gives an error during the substitution if the data member isn't of a built-in type.
Example:

C++
struct Y {};

struct X 
{
    Y a;
};

template < Y X::* >
struct Test ;

template < class T >
void f (Test< &T::a >*) {}

f< X >(0); // error on Visual c++ 8

The above code is well-formed according to the C++ standard. GCC and Comeau compile it, but not Visual C++. It leads to a pernicious effect in our interface detector since the data member detection is included in a SFINAE mechanism and therefore won't generate errors. This leads to the fallacious behavior of returning the NOT_FOUND value, although the data member is indeed present. For this reason, the data member detection macros are disabled for Visual C++.

Design limitations

Exact signature

The interface detector checks the exact signature with no conversion. For example, if you want to check the presence of void foo(int), you won't be able to detect a compatible function such as void foo(double). An important side-effect of this limitation is the impossibility of detecting an inherited function. This is because the type of the implicit parameter of such functions is a pointer to the parent class from which the function is declared.

Semantic discrepancy

The second limitation is the possible semantic difference between the detected function and the actual use of the function. For instance, in biology, some cells can be cloned. I can check this "clonable" capability by detecting whether the object contains a clone function. However, some classes that aren't even cells may declare a clone function that has other purposes, i.e. virtual constructor.

Usage

The utilization of the provided interface detector relies on 4 macros:

  • CREATE_FUNCTION_DETECTOR
  • CREATE_DATA_DETECTOR
  • DETECT_FUNCTION
  • DETECT_DATA

The first two macros are needed to construct the detector from the identifier of the function or the data to be detected. This is the first step before proceeding to the actual detection. For example, if I want to detect the function int foo (double), I need first to construct the detector:

C++
CREATE_FUNCTION_DETECTOR(foo);

Note that once the detector for foo is constructed, any signature associated with the identifier foo can be detected: int foo(double), void foo(), etc. The DETECT_FUNCTION and DETECT_DATA macros perform the detection. The first argument of those two macros is the name of the class subject to the detection. The rest of the arguments follow the declaration syntax of the function or data to be detected, except that commas are needed around the identifiers to separate them from the rest of the type:

C++
// detection: int foo (double, int)
DETECT_FUNCTION ( MyClass, int, foo, (double, int) ) 

// detection: const int bar
DETECT_DATA ( MyClass, const int, bar ) 

// detection: void foo()
DETECT FUNCTION ( MyClass, void, foo, () ) 

The DETECT macros return the following self-explanatory constants:

  • NOT_FOUND (== 0)
  • STATIC_FUNCTION
  • NON_STATIC_FUNCTION
  • STATIC_DATA
  • NON_STATIC_DATA

All of those constants belong to the Detector namespace. As a simple example, suppose that we want to check whether a class contains the member function void Print(). We use the function if it's available. Otherwise, we print a "No Print function available" message. We'll test the 2 following structures:

C++
struct X 
{
    void Print() 
    { 
        std::cout << "X Print"  << std::endl ;
    }
};

struct Y 
{};

First, we need to construct the detector:

C++
#include "Detector.h"

CREATE_FUNCTION_DETECTOR(Print);

Second, we have to define a structure that will be used to select a different behavior according to the presence of the function. The first template parameter of the structure will be used to hold the result of the DETECT_FUNCTION macro. The second parameter is the class we want to test:

C++
template < int, class T >
struct Select 
{
    static void Print ( T obj )
    {
        obj.Print();
    }
};

template < class T >
struct Select < Detector::NOT_FOUND , T > 
{
    static void Print ( ... )
    {
        std::cout << "No Print function" << std::endl;
    }
};

// Helper function

template < class T >
void PrintHelper( T a )  
{
    Select< DETECT_FUNCTION ( T, void, Print, () ) , T >::Print( a );
}

Now we can safely call PrintHelper on any object of type X or Y:

C++
X a;
Y b;

PrintHelper(a); // "X Print"
PrintHelper(b); // "No Print function"

The process of selecting the correct behavior is done at compile-time. A whole class interface can be checked at once. For example, let's say that any object that can fly and quack is a duck. To know whether a class represents a duck according to this definition, we can check if both the Fly and Quack functions are present within a single expression:

C++
DETECT_FUNCTION( Class, void, Fly, () ) &
DETECT_FUNCTION( Class, void, Quack, () )

A better way to do a multiple function or data check is to define a macro like this:

C++
#define DUCK_INTERFACE( Class ) \
    DETECT_FUNCTION( Class, void, Fly, () ) & \
    DETECT_FUNCTION( Class, void, Quack, () )

This way, a simple and understandable expression can be used and reused to detect whether a class is a duck. DUCK_INTERFACE( MyDuckClass ) returns Detector::NOT_FOUND if MyDuckClass doesn't strictly follow a duck interface.

Now here's the solution of the "motivating example," i.e. "How can we know whether the container class has a remove function?"

C++
CREATE_FUNCTION_DETECTOR(remove);

template < class T, template < class , class > class Container >
int HasRemove ()
{
    return DETECT_FUNCTION( Container< T >, void, remove , (const T& ) );
}

HasRemove< int, std::vector >(); // NOT_FOUND 
HasRemove< int, std::list >();   // NON_STATIC_FUNCTION

Conclusion

Interface detection brings unique solutions to specific problems. It can also be used to support any duck typing 3 design -- such as policy-based design -- resulting in a safer, cleaner and extended design. One might also check the BCCL 4 that makes this kind of design more robust. The interface detection implementation heavily relies on many advanced C++ techniques, especially template ones. As such, it has some inevitable downsides: code complexity, support only by the latest compilers, difficulty in tracking bugs exhaustively and homogeneously amongst compilers, etc. Fortunately, the next C++ standard 5 should ease the programming of such template solutions.

Notes

[1] The compiler, for the purpose of overload resolution only, assumes that a static member function accepts an implicit object parameter.

[2] GCC considers that 2 functions share the same priority if one of them has the same declaration as the other with the addition of an ellipsis at the end of its parameter list

[3] In a duck typing system, the value of an object determines the object's behavior. C++, through the use of templates, implements a static form of duck typing. Duck typing on Wikipedia

[4] BCCL: Boost Concept Check Library

[5] Bjarne Stroustrup, A Brief Look at C++0x

References

  • David Vandevoorde, Nicolai M. Josuttis. C++ Templates: The Complete Guide. Addison Wesley, 2002
  • Andrei Alexandrescu. Modern C++ Design: Generic Programming and Design Patterns Applied. Addison Wesley, 2001

History

  • 08-01-2007:
    • Edited and moved to the main CodeProject.com article base
  • 07-24-2007:
    • [Article] Table enumerating compiler-specific bugs added; Visual C++ 7.1 bugs added in "Compiler-specific errors"
    • [Article] static qualifier removed from the Test functions declarations, as we're not in the context of a class definition
  • 07-07-2007:
    • [Article] Table of contents fixed
    • [Article] Expression "hidden object parameter" replaced by "implicit object parameter," which is the exact wording used in the C++ standard
    • [Article] Short explanation added about the incapability of detecting inherited functions in "Design Limitations, Exact signature"
  • 07-03-2007:
    • Initial version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here