C++’s .* and ->* operators: Part 2
Welcome back to “Strange C++.” Today we will continue the exploration of C++‘s pointer-to-member features, begun in Part 1 of this series, by using the .*
and ->*
operators to access pointers to member functions—that is, methods.
Last time in Part 1 we looked at how member pointers helped us to “cache” the choice of a float member in a hypothetical 3D vector class, Vector3D
. Then we used this member pointer storing the choice of component to add a float to an arbitrary component of a std::vector
of Vector3D
s.
Now our goal for this article is to extend our code acting on vectors: instead of just adding to an arbitrary component of Vector3D
, we’d like to perform an arbitrary operation instead. This means that along with the selection of component, we’d like to also store a selection of operation to perform on that component. Then we’d like to apply that operation to a Vector3D
without needing expensive1 conditionals or switch statements.
So let’s sketch up some code for how you’d solve a particular problem without using pointers to member functions. Here’s a quick class that represents an arbitrary operation on a Vector3D
, and stores which component to operate on:
// Represents an operation done against one component of a Vector3D class Operation { protected: float Vector3D::*componentPtr; // store the component selection using a member pointer public: Operation(float Vector3D::* const componentPtr) : componentPtr(componentPtr) { } void setComponent(float Vector3D::* const componentPtr) { this->componentPtr = componentPtr; } virtual void op(Vector3D &vector3D, float f) const = 0; // interface to use the operation };
Given the requirements and what we know about member pointers from the last article, this seems like a reasonable way to store the “arbitrary component” selection in a class. To realize the “arbitrary operations” part of the deal, you’d have to derive from this class into the actual operations, in a textbook example of polymorphism:
class OperationAdd : public Operation { public: OperationAdd(float Vector3D::* const componentPtr) : Operation(componentPtr) { } void op(Vector3D &vector3D, float f) const { vector3D.*componentPtr += f; // specialize the operation as addition } }; class OperationSub : public Operation { public: OperationSub(float Vector3D::* const componentPtr) : Operation(componentPtr) { } void op(Vector3D &vector3D, float f) const { vector3D.*componentPtr -= f; // specialize the operation as subtraction } };
Now, using polymorphism is a perfectly natural way to solve the problem for a C++ programmer. Just implement the interface by overriding op()
and passing the component member pointer to the parent constructor: easy. Note the use of Operation::componentPtr
to access the “cached” component selection. This might be how you’d want to use these classes:
void applyOperation(std::vector<Vector3D> &vs, const Operation &operation, float amount) { for(std::vector<Vector3D>::iterator v_i = vs.begin() ; v_i != vs.end() ; v_i++) { operation.op(*v_i, amount); // apply the operation to every Vector3D } } int main() { std::vector<Vector3D> vs(5, Vector3D(0, 2, 3)); // create five <0, 2, 3>s std::copy(vs.begin(), vs.end(), std::ostream_iterator<Vector3D>(std::cout, " ")); std::cout << std::endl; // print out the list OperationAdd vectorAdd(&Vector3D::z); // create an "addition to .z" operation std::cout << "Adding to component z..." << std::endl; applyOperation(vs, vectorAdd, 1.f); // apply "addition of 1 to .z" to all vectors std::copy(vs.begin(), vs.end(), std::ostream_iterator<Vector3D>(std::cout, " ")); std::cout << std::endl; // print out the list again OperationSub vectorSub(&Vector3D::y); // create an "subtraction from .y" operation std::cout << "Subtracting from component y..." << std::endl; applyOperation(vs, vectorSub, 3.f); // apply "subtraction of 3 from .y" to all vectors std::copy(vs.begin(), vs.end(), std::ostream_iterator<Vector3D>(std::cout, " ")); std::cout << std::endl; // print out the list again return EXIT_SUCCESS; }
Example output:
<0, 2, 3> <0, 2, 3> <0, 2, 3> <0, 2, 3> <0, 2, 3> Adding to component z... <0, 2, 4> <0, 2, 4> <0, 2, 4> <0, 2, 4> <0, 2, 4> Subtracting from component y... <0, -1, 4> <0, -1, 4> <0, -1, 4> <0, -1, 4> <0, -1, 4>
But there’s a lot of code duplication for the OperationAdd
and OperationSub
classes, isn’t there? You have to write a heck of a lot of code to account for what is essentially one character of difference between each operation.
In addition, as a design decision, shouldn’t selecting “which component” be similar to selecting “which operation?” Why does the latter require instantiating a new object of another subclass of Operation
, yet the former requires only a call to Operation::setComponent
?
To amend this, I’d like to introduce the member function pointer. It is exactly what it sounds like: a pointer to a member function, also known as “method.” A member function pointer can point to any method that has the same parameter list, return type, and const-ness as specified in the pointer’s type. Let’s see an example:
class Example { public: void printA() { std::cout << 'A' << std::endl; } void printB() { std::cout << 'B' << std::endl; } static void printC(); }; void Example::printC() { std::cout << 'C' << std::endl; } void printD() { std::cout << 'D' << std::endl; } int main() { // member function pointers: Example example; void (Example::*printMethod)() = &Example::printA; // declare a pointer to printA (example.*printMethod)(); // prints 'A' printMethod = &Example::printB; // reassign pointer to printB (example.*printMethod)(); // prints 'B' // regular function pointers: // even though printC is part of Example, a regular function will do void (*printFunc)() = Example::printC; // see how the "&" isn't needed printFunc(); // prints C printFunc = printD; // here too, "&" is omitted printFunc(); // prints D }
I think it’s pretty self-explanatory, but let’s go through it anyways. void (Example::*printMethod)()
is the declaration of the “printMethod
” pointer variable. This syntax should be ringing bells in your head if you know member pointer syntax and function pointer syntax. Note, however, that we can’t use just “Example::printA
” to initialize the pointer; we need to use the address-of operator (&
) as well. I’m not entirely sure why there is such a discrepancy between the syntaxes used for normal function and member functions. The fact that the parameter list has greater precedence than the .*
and ->*
operators leads to another unfortunate syntax: parentheses around the class instance, .*
or ->*
, and the member function pointer.
Now let’s try putting member function pointers to use in our Vector3D
example. It’s really as simple as changing Operation
to explicitly store the selected operation member function, rather than implicitly in the case of polymorphism (more on this later):
class Operation { protected: // a pointer to a const member function with params (Vector3D &, float) returning void void (Operation::*operationPtr)(Vector3D &, float) const; // store the component selection using a member pointer float Vector3D::*componentPtr; public: Operation(void(Operation::*operationPtr)(Vector3D &, float) const, float Vector3D::* const componentPtr) : operationPtr(operationPtr), componentPtr(componentPtr) { } void setComponent(float Vector3D::* const componentPtr) { this->componentPtr = componentPtr; } void setOperation(void(Operation::* const operation)(Vector3D &, float) const) { this->operationPtr = operation; } void op(Vector3D &vector3D, float f) const { (this->*operationPtr)(vector3D, f); // call the member (add or sub) } void add(Vector3D &vector3D, float f) const { vector3D.*componentPtr += f; } void sub(Vector3D &vector3D, float f) const { vector3D.*componentPtr -= f; } };
All we’ve done here is add the two example operations (addition and subtraction) as methods of Operation
, as well as a new member, operationPtr
, which points to either add
or sub
2. We’ve kept the op()
interface the same, so the other difference is how we construct the Operation
instance and how to change the operation selection:
Operation vectorOp(&Operation::add, &Vector3D::z); // create an "addition to .z" operation applyOperation(vs, vectorOp, 1.f); // apply "addition of 1 to .z" to all vectors vectorOp.setOperation(&Operation::sub); // note the use of & operator vectorOp.setComponent(&Vector3D::y); applyOperation(vs, vectorOp, 3.f); // apply "subtraction of 3 from .y" to all vectors
Note how the lines of vectorOp.setOperation(&Operation::sub)
and vectorOp.setComponent(&Vector3D::y)
look so… symmetrical. We can now move independently in the space of operations from the space of Vector3D
components. This is known as orthogonality, which is just a fancy way of saying that this code feels nice to use.
Now about the technicals: if you understand the mechanism behind polymorphism, you’ll recognize that this solution is very similar to said mechanism. In fact, it’s likely more efficient. A virtual function call requires dereferencing the VPTR, looking up the appropriate member function from VTABLE (which you’ll now recognize is simply an array of member function pointers), and calling the pointer retrieved. This solution skips straight to calling the member function pointer—an elegant way to strip away unnecessary functionality and code duplication.
So there you have it, folks. I started writing these two articles when I looked online at the explanations for how to use member pointers and noticed that every single one of them showed completely pointless examples. They all seemed to describe pointers to members and member functions as an oddity, a silly way to rename a class’s contents.
Hopefully I’ve proven them otherwise and you now have an idea of how to utilize these little guys to construct cleaner, faster code.
Stay tuned for more “Strange C++.†Maybe. Actually, don’t count on it.
You should read “C++‘s .* and ->* operators: Part 1″ if you haven’t already.
- “Expensive” meaning conditionals can cause additional per-element overhead when batch processing due to branch misprediction and the subsequent pipeline eviction, in superscalar pipelined architectures. [▲]
- Actually, it can point to
op
as well, which would cause an infinite recursive loop. Oops. [▲]
Very Good site, thank yo mister, it’s help’s me!