Function Types

C/C++ Users Journal July, 2004

Function types are a little-known but widely supported and useful feature in C++. Using typedefs of function types offers an alternative to macros with a different set of advantages and drawbacks

By Herb Sutter

Herb Sutter (http://www.gotw.ca/) is a leading authority and trainer on C++ software development. He chairs the ISO C++ Standards committee and is a Visual C++ architect for Microsoft, where he is responsible for leading the design of C++ language extensions for .NET programming. His two most recent books are Exceptional C++ Style and C++ Coding Standards (available in August and October 2004).

What if you want to declare the same function signature many times? For example, let's say that a SysEvent class lets you register event handlers of a certain type, but the type is a pain to write out:

// Example 1
//
class SysEvent {
public:
  void Register(    
    SomeConvoluted* 
    (*PtrToFunction)
      ( WithA, Very::Verbose&, 
        Signature** ) 
);
  ... etc. ...
};

The clients that want to declare such callbacks have to write the whole shebang in each declaration:

// Example 1, continued
//
SomeConvoluted*
AGlobalHandlerFunction( WithA, 
  Very::Verbose&, Signature** );

class Client {
public:
  static SomeConvoluted*
  OutOfMemory( WithA, Very::Verbose&, 
              Signature** );

  static SomeConvoluted*
  OutOfDiskSpace( WithA, Very::Verbose&, 
                Signature** );
  ...
};

Wouldn't it be nice if we could make this a bit easier? Fortunately, we can.

Macros: The Dirt-Encrusted-Blunt-Instrument Solution

A common approach to make this less tedious is to use a macro:

// Example 2: A convenience macro
//
#define SYSEVENT_CALLBACK( x ) \
  SomeConvoluted* (x)( WithA, \
    Very::Verbose&, Signature** )

#define DECLARE_SYSEVENT_CALLBACK( x ) \
  SomeConvoluted* x( WithA, \
    Very::Verbose&, Signature** )

class SysEvent {
public:
  void Register( SYSEVENT_CALLBACK(*) );
  ... etc. ...
};

Yes, macros are evil, and good coding standards deplore most macro uses. But here they do offer the advantage that the clients that declare such callbacks can write simpler declarations using the appropriate helper macro:

// Example 2, continued
//
DECLARE_SYSEVENT_CALLBACK( 
   AGlobalHandlerFunction );
class Client {
public:
  static DECLARE_SYSEVENT_CALLBACK( 
     OutOfMemory );
  static DECLARE_SYSEVENT_CALLBACK( 
     OutOfDiskSpace );
  ...
};

Incidentally, did you notice why we typically need two macros? To paraphrase an old commercial, it's because sometimes we need those parens, sometimes we don't. The parameter to SysEvent::Register has to have parentheses around the pointer, whereas the function declarations must not have parentheses.

But hey, this is C++ and we're not limited to smelly macro solutions anymore, right? Wouldn't it be nice if we could avoid those horrible ugly macros, and instead make better use of the C++ type system?

Indeed we can.

Typedefs of Function Types: A Better Solution (Sometimes)

In C++, the class could instead provide a typedef that declares the function type:

// Example 3: A convenience typedef
//
class SysEvent {
public:
  typedef SomeConvoluted* 
    Callback( WithA, Very::Verbose&, 
             Signature** );
  void Register( Callback* );
  ... etc. ...
};

Aside: If this syntax is unfamiliar to you, think of it this way: Just as writing typedef int (*PFunc)(int); creates a handy name for a pointer to a function type, if we omit the * and just write typedef int Func(int);, we are creating a handy name for a function type.

Now clients that declare such callbacks can write simple declarations:

// Example 3, continued
//
SysEvent::Callback AGlobalHandlerFunction;
class Client {
public:
  static SysEvent::Callback OutOfMemory;
  static SysEvent::Callback 
     OutOfDiskSpace;
  ...
};

Using the typedef avoids the ugly smelly (and did I mention hairy?) macro, and it provides elegant syntax—really, what's not to like? Take another look at Example 3. I ask you: Isn't that nicely self-documenting, concise, and even pretty?

The answer is "Yes," but with some reservations because, alas, it's useful but not quite as charming as this first look might make it appear.

Glitch #1: Declarations Versus Definitions

One glitch with this is that this typedef-of-a-function-type technique only works for function declarations. Function definitions still have to be written out the usual long way:

// Example 4(a): Declaring vs. defining a function
//
SysEvent::Callback SomethingHappened;        // ok for declaration
SysEvent::Callback SomethingHappened { ... } // error for definition

Consider: In this particular example, how would the code inside the function body even be able to use its parameters? The typedef didn't provide any formal parameter names. But even if it had, the Standard rules that this is illegal, and I think that's probably a good thing; if it were allowed, then a programmer reading the code would have to navigate to some far-flung sysevent.h header file to understand the uses of the parameter names in a local function body. Locality of reference is good for programs, but let's not forget that it's good for programmers, too.

Instead, the function definition has to be written out:

// Example 4(b): Defining a function is done "the usual way"
//
SysEvent::Callback SomethingHappened; // ok for declaration
SomeConvoluted*
SomethingHappened( WithA w,
                   Very::Verbose& vv,
                   Signature** s ) { // ok, definition 
                                    // uses the long form
  ...
}

Glitch #2: Members Versus Nonmembers

Another glitch is that a typedef of a function type can be used to declare either a member or a nonmember:

// Example 5(a)
//
typedef int Func( int );
Func GlobalFunction;        // ok
class Client {
  Func MemberFunction;      // ok
};

That could be viewed as cool, but it also blurs the type of the typedef Func by making it context dependent, and if we've learned anything from C++ over the past decade, it's that context-dependent types should set off warning bells in our heads. One immediate consequence is that if you want to use such a typedef to declare a const member function, you can't do it the ways you might think:

// Example 5(a), continued
//
class AnotherClient {
  Func MemberFunction1 const;   // error
  const Func MemberFunction2;   // error
};

In both declarations, the const is illegal according to the current Standard C++, although in the draft of the next C++ Standard (C++0x) the const is merely ignored. Today some compilers already allow (and ignore) the const and will let you off with a warning instead of writing you a ticket. Note that even if it is accepted, this const wouldn't mean the same thing as declaring a const member function, and confusing it with that is similar to confusing const char* (a pointer to a constant char) with char* const (a constant pointer to a char); they're very different things.

The good news is that the const qualification can be done, but you have to instead put the const in the typedef itself, which has the side effect of simultaneously rendering the typedef unusable to declare a nonmember function:

// Example 5(b): Adding const
//
typedef int Func( int ) const;  // ok
Func GlobalFunction;        // error
class Client {
  Func MemberFunction;      // ok, a const member function
};

This leads us to a related problem, which will help to remind us why the callback functions were made static back in Example 3. Returning to that example, consider:

// Example 6 (based on Example 3)
//
class SysEvent {
public:
  typedef SomeConvoluted* Callback( WithA, Very::Verbose&,
                                  Signature** );
  void Register( Callback* );
  ... etc. ...
};

What is the type of Register's parameter? The answer is that the parameter is a pointer to a function with the required signature.

That may seem unsurprising at first, so let's play it back one more time: It's a pointer to a function—not a pointer to a member function. Thus, the earlier example continued:

// Example 6, continued
//
SysEvent::Callback AGlobalHandlerFunction;
class Client {
public:
  static SysEvent::Callback OutOfMemory;
  static SysEvent::Callback OutOfDiskSpace;
  ...
};

The typedef could instead have been used to create a nonstatic member function and that would have worked just fine for the declaration, but the whole purpose of declaring such functions is to pass them to SysEvent, which, after all, takes a pointer to a function, not a pointer to a member function:

// Example 6, continued
//
class AnotherClient {
public:
  SysEvent::Callback OutOfMemory;       // note: not static
  SysEvent::Callback OutOfDiskSpace;    // note: not static
  ...
};
SysEvent e;
e.Register( &AnotherClient::OutOfMemory );  // error, type mismatch

This doesn't work because, whereas SysEvent::Register takes a pointer to a nonmember function, AnotherClient::OutOfMemory is a member function—more specifically, it's a member function of AnotherClient. It turns out that the above semantics are actually perfectly fine and dandy and just what SysEvent intended, and if we hadn't been using function typedefs, the difference would have been obvious. In fact, because we started with the raw declarations in Example 1, you might have noticed this issue straight off before the present discussion.

The point is this: Using a function typedef obscures the difference between declaring member and nonmember functions, because the typedef can be used to declare either and so the meaning of the typedef changes depending on the context in which it's used. That last bit is generally not such a great thing. Different things ought to have visibly different names, and here, using the same name for quite different things obscures the meaning of the program.

Incidentally, the newly-being-standardized facility tr1::function [1] deserves mention here. If SysEvent::Register instead took a parameter of type

tr1::function< SomeConvoluted* ( WithA, Very::Verbose&,    Signature** )>

instead of the bald function pointer type

SomeConvoluted* (*)( WithA, Very::Verbose&, Signature** )

then you could Register any member or nonmember function or function object that has a compatible signature (possibly using binder helpers), including functions or function objects whose parameter and return types are compatible but different. See [1] and [2] for details and further analysis.

Glitch #3: Typedefs Versus Templates

Finally, a third glitch appears when you say, "Hey, that's cool, let me try doing something similar with template parameters," as certain C++ illuminati who suffer from a template fetish and whose names all begin with the letter "A" are wont to do. For example, this is an error:

// Example 7: Mightn't it be nice if...? But it's not.
//
template<typename T>
class Client {
public:
  T t;
};
typedef int Func( int );
Client<Func> test;  // error when instantiating the member t

Granted, typedefs and template parameters aren't the same thing, but usually C++'s features are pretty well integrated and work similarly. In this case, similar-looking constructs don't work the same way.

Summary

Function types are a little-known but useful feature in C++. Despite not being widely known to programmers, the great news is that they are widely supported in both current and older C++ compilers. Nearly all of the code examples in this article were handled correctly by all the compilers I tried, and that includes a dozen different versions of compilers ranging from creaky ancient versions to current releases to bleeding-raw prealpha builds.

Using typedefs of function types offers an alternative to macros with a different set of advantages and drawbacks. Function types also crop up in other places, notably in the newly drafted Standard tr1::function facility I recently described in [1] and [2]. It's worth knowing about function types' capabilities, their limitations, and their place in Standard C++.

Acknowledgments

Thanks to Felix Metzger and Detlef Vollmann for asking a question about a strange typedef, and to Steve Adamczyk, Bill Gibbons, Mike Miller, and Erwin Unruh for their comments about this feature's history and limitations.

References

  1. [1] Sutter, H. "Generalized Function Pointers," C/C++ Users Journal, 21(8), August 2003 (http://www.cuj.com/documents/s=8464/ cujcexp0308sutter/0).
  2. [2] Sutter, H. "Generalizing Observer," C/C++ Users Journal, 21(9), September 2003 (http://www.cuj.com/documents/s=8840/cujexp0309sutter/).