Most C++ programmers are familiar with the pointer to function facility provided by the language; the majority of those that do will be betraying their C heritage. However, few are aware of, or have used, the pointer to member facility. This article introduces C++'s pointer to member and then presents a genuine application of the facility.
The problem with most tutorials on a particular language feature is that the examples provided are generally not real world ones - a few demonstration lines of code do not show the programmer how they should apply the facility, just how to use it. The natural outcome of this is that the inexperienced programmer will go out of their way to try to use the facility at the earliest possible opportunity, when it may not be applicable or serve as the best way of implementing a certain functionality.
In this description and application I aim to avoid luring the reader into this pitfall.
What are pointers to members?
In C and C++ we can declare a pointer to a function as follows:
void exampleFn(char *s, int i) { /* ... */ } int (*fnptr)(char *, int); fnptr = &exampleFn; fnptr("string", 1);
On the second line we declare a variable that is of type pointer to function with signature (char*, int) and return type void called fnptr . We assign the address of exampleFn to it, and then call exampleFn through it.
When assigning to a function pointer variable the supplied function must match the signature (return type and parameter types) exactly.
The use of the & preceding exampleFn is optional in the language, we could just have legally written fnptr = exampleFn; Similarly when we call exampleFn through the fnptr variable we are really dereferencing a pointer to the function so we can write (*fptr)("string", 1); to call exampleFn if we choose. However the compiler can deduce that fptr is a pointer to function and will dereference appropriately for us. Because of this the * s are more often than not omitted for clarity.
Conventionally we may try to tidy up the syntax using typedef s:
typedef void (*FPTR_T)(char *, int); FPTR_T fptr = &exampleFn; fptr("string", 1);
This declares a new type name called FPTR_T that points to functions with signature (char *, int) and return type void . fptr is a variable of that type, and the rest follows as before. It is not strictly necessary, but does makes the syntax a lot more readable.
This much is common to C and C++: C++ provides us a similar mechanism to point to elements that are within a class. Let's say that we have something like a function pointer, but that points to a member function in a class. How would we call it? Being a function tied to a class we need to provide one extra piece of information: the object on which the member function is to operate. If we tried to use the same syntax as above then we would not be supplying this information.
So how do we do it? Say we have the following class:
class ExampleClass { public: void exampleMFn(char *s, int i); int data; // Euch! We all know not to have // public data members, don't we? private: void anotherMFn(char *s, int i); int moreData; };
Now, to create and use a pointer to the member function exampleMFn we write the following:
// Create the member pointer void (ExampleClass::*mfptr)(char *, int); mfptr = &ExampleClass::exampleMFn; // Use it on an object ExampleClass obj; (obj.*mfptr)("string", 1); // Use it on an object pointer ExampleClass objptr = new ExampleClass; (objptr->*mfptr)("string", 1); delete objptr;
The .* and ->* operators associate (or bind) the mfptr member pointer to the object obj and the object pointed to by objptr respectively; the member function call can then proceed as normal. Note that the syntax in both cases includes an asterisk exactly as did the full version of the function pointer syntax. However, with pointers to members the asterisk cannot be omitted. Again, it is common to use typedef s to increase readability:
typedef void (ExampleClass::*MFPTR_T)(char *, int); MFPTR_T mfptr = &ExampleClass::exampleMFn;
Pointers to members aren't restricted to member functions, although most tutorials focus entirely on them. We can create a pointer to a data member too. For example:
int ExampleClass::*miptr = &ExampleClass::data; ExampleClass obj; obj.*miptr = 10;
Pointers to member functions honour run-time polymorphism too. If a pointer to virtual member function is bound to an object you are guaranteed that the correct virtual function definition is called for that object.
static member functions are treated slightly differently since they are not directly associated with an object. There isn't a pointer to member syntax for them; you use the normal function pointer syntax. It is an error to try to use pointer to member syntax in this case.
Pointers to members naturally honour C++'s visibility rules; otherwise they would be a simple way to get access to a class' private data! This means that a non-member function of ExampleClass can only assign pointers to the public parts of the class. For example:
MFPTR_T p1 = &ExampleClass::exampleMFn; // OK MFPTR_T p2 = &ExampleClass::anotherMFn; // Error: anotherFn is private
However a member function can assign a pointer to its class' non-public members. This is shown in the following (frivolous) definition of exampleMFn :
void ExampleClass::exampleMFn(char *s, int i) { void (ExampleClass::*fp)(char *, int); fp = &anotherMFn; // use is OK - note we don't need to fully // qualify the member function name since // it is in the class scope (this->*fp)(s, i); }
If a class DerivedClass inherits from ExampleClass then we can assign the address of its members to a MFPT_T variable. However, you can only assign members that exist in the ExampleClass interface to the variable. This again preserves C++'s visibility rules.
When should I (not) use them?
The above do not really show useful examples of how to use pointers to members, only of how to master the syntax. The next question we should be asking is why would I want to use a pointer to member? The following application section is a good example of a valid use for pointers to members.
Be careful that you're not trying to use a pointer to member where a better class hierarchy design would allow you to use virtual functions instead. An indicator of this happening is if the choice of member is based on some form of class type information. Virtual functions are preferable in this kind of situation because the maintenance overhead is smaller (think about what will happen if you add a new class to the program).
An application of pointers to members
I present here a framework for creating easily maintainable command line utilities in C++. The implementation of the framework uses pointers to members and should give a good idea of a valid application of this language mechanism.
Developing command line utilities is a common task (well, for most UNIX programmers anyway). Such utilities typically have to parse arguments provided on the command line and act upon them accordingly. Because this is such an often performed task C and C++ provide us with a mechanism for accessing the command line arguments in main via the argc and argv parameters.
This framework for parsing the contents of argc and argv follows the UNIX style of having an arbitrary number of switches, which can be entered in short or long form. The short form is prefixed by a single dash, the long form by two dashes. Each switch may be followed by a number of arguments. An example command line may be:
cmd -s --long1 arg --long2 6 5 --long3
Where cmd is the name of the command line utility, -s is a shortened version of a switch that takes no arguments, --long1 is a long form switch (note the two dashes) that takes one argument (here arg), --long2 is another long form that takes two arguments (here 6 and 5), and --long3 another that takes no arguments.
The following code is a canonical example of the framework. It implements a command line utility that accepts a number of switches and requires one unswitched argument that is considered to be an input filename.
The implementation is split across three files. main.cpp contains the main function, whilst Application.h and Application.cpp contain the actual application implementation which is in a class called (for the sake of argument) Application . This code uses the STL.
Application.h :
#ifndef APPLICATION_H #define APPLICATION_H #include <vector> #include <string> class Application { public: Application(int argc, char *argv[]); virtual ~Application(); int go(); private: static const int version = 100; // version no of app (1.00) static const char[] name = "appname"; // name of app // Command switch cunningness struct Switch { typedef void (Application::*handler_t)(int argpos, char *argv[]); std::string lng; // long switch std::string srt; // short switch int nargs; // no. extra arguments following std::string help; // help text for switch handler_t handler; // pointer to handler member Switch(std::string l, std::string s, int n, std::string h, handler_t hd) : lng(l), srt(s), nargs(n), help(h), handler(hd) {} }; // continued on next page std::vector <Switch> switches; // These are the command switch handlers void handle_help(int argpos, char *argv[]); void handle_version(int argpos, char *argv[]); void handle_aardvark(int argpos, char *argv[]); std::string filename; }; #endif
main.cpp :
#include "Application.h" int main(int argc, char *argv[]) { Application app(argc, argv); return app.go(); }
Application.cpp :
#include "Application.h" using std::string; using std::vector; Application::Application(int argc, char *argv[]) { // First, we build a list of the switches understood by this program switches.push_back(Switch("help", "h", 0, "provide help", &handle_help)); switches.push_back(Switch("version", "ver", 0, "give version no", &handle_version)); switches.push_back(Switch("aardvark", "a", 1, "command with one parameter", &handle_aardvark)); // Now we parse the command line if (argc <= 1) { handle_version(0, argv); handle_help(0, argv); exit(0); } for (int n = 1; n < argc; n++) { bool done = false; for(vector<Switch>::iterator sw = switches.begin(); !done && sw != switches.end(); sw++){ if(argv[n] == string("-") + sw->srt || argv[n] == string("--") + sw->lng){ done = true; if(n + sw->nargs >= argc) { cerr << "Error in command format (" << argv[n] << " expects " << sw->nargs << " arguments)\n"; exit(1); } // Call appropriate handler through pointer (this->*(sw->handler))(n, argv); n += sw->nargs; } } // This command line utility needs a filename as an unswitched argument. // The following code implements this. if (!done) { filename = argv[n]; } } if (filename == "") { cerr << "No filename specified.\n"; exit(1); } } Application::~Application() { // Clean up in here ... } int Application::go() { // Do something useful ... return 0; } // This member function displays a nicely formatted help text // listing the command line usage of the program. void Application::handle_help(int, char*[]) { // You may wish to change the following descriptive text! cout << "Usage: " << name << " [OPTION]... [FILE]\n" << "Does this that and the other.\n\n" << "OPTIONs are:\n\n"; // Work out column widths for the nicely formatted output unsigned int srtsize = 0; unsigned int lngsize = 0; for(vector<Switch>::iterator n = switches.begin(); n != switches.end(); n++) { if (n->srt.size() > srtsize) srtsize = n->srt.size(); if (n->lng.size() > lngsize) lngsize = n->lng.size(); } srtsize += 2; lngsize += 2; // Produce the nicely formatted output for(vector<Switch>::iterator n = switches.begin(); n != switches.end(); n++) { cout << " -" << n->srt << string(srtsize-n->srt.size(), ' ') << string(" --") + n->lng + " " << string(lngsize-n->lng.size(), ' ') << n->help << endl; } cout << "\nSend bug reports to <pete.goodliffe@pacemicro.com>\n"; exit(0); } void Application::handle_version(int, char*[]) { cout << name << " version " << version/100 << "." << version%100 << " built on " << __DATE__ << "\n"; } void Application::handle_aardvark(int argpos, char *argv[]) { // To get here we are guaranteed that argv contains at least // this switch in argv[argpos] and the argument in argv[argpos+1] string arg = argv[argpos+1]; cout << "--aardvark argument is: " << arg << endl; }
This code will compile and create a simple command line 'utility', but you may wish to extend it's capabilities slightly (maybe you won't want the -- aardvark facility!)
As we can see the above Application class uses an internal vector of Switch es to store the table of command line switches it can accept. This table is populated in the constructor and then used to parse the command line (passed in argc and argv ). The table of switches is also used to generate the nicely formatted help text in the handle_help member function.
We use pointers to members in the Switch structure to store which member function is used to handle a particular command line switch. This requires that each handler conform to a particular signature.
Using a framework like this presents us with several benefits. By not hard-coding the command line argument parsing but using this more generic table system we can easily add new switches and extend the functionality of the utility without much extra work. We can now be assured that the help text will always reflect the command line interface.
I could have provided the code as a base class to inherit specific applications from. However, I prefer to leave it as a framework to build upon due to the kind of customisations it will need. For example, you may wish to add more unswitched command line arguments, or change the help text output format. This kind of change is better suited to modification than inheritance. The important thing to take away is the table of switches idiom rather than the code itself.
Conclusion
We have seen how to use C++'s pointer to member facility. Building on this tutorial we have seen an application of the language feature in real use.
Pointers to members are an extra tool to put into our C++ programming arsenal, to be used as and when appropriate.