C++’s .* and ->* operators: Part 1

Wel­come to the newest install­ment of “Strange C++.” Today we’ll be dis­cussing the .* and ->* oper­a­tors in C++.

If you’re like me, then you prob­a­bly spend all of your free time read­ing C++ FAQ Lite for some rea­son. Of course I don’t do that, because that’d be kind of weird and anti­so­cial, but I fig­ure peo­ple like me are prob­a­bly weird and anti­so­cial. Let’s be mod­est here.

At any rate, you’d have come across the rules for which C++ oper­a­tors can and can’t be over­load­ed. Those in the “can’t” camp are the ternary oper­a­tor ?:, the scope spec oper­a­tor ::, the mem­ber-of oper­a­tor ., and final­ly the .* oper­a­tor1. What the Bjarne Strous­trup is that?

In few words, the .* oper­a­tor access­es an object’s mem­ber point­ed to by a mem­ber point­er. The ->* point­er does the same thing, only it access­es the mem­ber of a point­er to an object, not an object or its ref­er­ence. Unfor­tu­nate­ly, this does noth­ing to clar­i­fy the operator’s use and intro­duces the addi­tion­al ques­tion: what is a mem­ber point­er? This is the heart of the arti­cle, and I will explain through exam­ples.

Let’s say we had the fol­low­ing class, rep­re­sent­ing a vec­tor in R3:

class Vector3D {
public:
	float x;
	float y;
	float z;

	Vector3D(float x, float y, float z) :
		x(x), y(y), z(z) {
	}

	friend std::ostream &operator<<(std::ostream &os, const Vector3D &v) {
		return os << '<' << v.x << ", " << v.y << ", " << v.z << '>';
	}
};

This is how you could use mem­ber point­ers, tak­ing note of the use of the .* oper­a­tor:

int main() {
	Vector3D v(0, 2, 3);
	std::cout << v << std::endl; // output: <0, 2, 3>

	float Vector3D::*componentPtr = &Vector3D::x; // member pointer to the x member
	v.*componentPtr += 1.f; // "dereference" it against instance v and add 1.f to it
	std::cout << v << std::endl; // output: <1, 2, 3>

	componentPtr = &Vector3D::y; // now point it to the y member
	v.*componentPtr *= 2.f; // "dereference" it against instance v and mul it by 2.f
	std::cout << v << std::endl; // output: <1, 4, 3>

	return EXIT_SUCCESS;
}

Now you might say: “Well, that looks like a whole lot of gib­ber­ish sym­bols to me. What does it mean and what does it do?”

Well, first we’re declar­ing and ini­tial­iz­ing a vari­able called componentPtr. What componentPtr does is point to a mem­ber of Vector3D of the type float. Take a cue from its dec­la­ra­tion syn­tax: note that the aster­isk, indi­cat­ing a point­er (or indi­rec­tion), comes after “Vector3D::” in “float Vector3D::*componentPtr.” Take this to mean that it’s a point­er that can move with­in the space of the Vector3D:: scope.

Also take anoth­er clue from the ini­tial­iza­tion: it’s ini­tial­ized to &Vector3D::x—the address of the x mem­ber in any Vector3D instance, not say, &v.x. So, when we use the point­er lat­er, we need to spec­i­fy which Vector3D instance it goes with: v.*componentPtr

Aside
I think of mem­ber point­ers as off­sets from the “this” point­ers of instances. Imag­ine each instance as an instance of a packed POD (Plain Ol’Data) struc­ture that con­tains two 32-bit floats, x then y, and a point­er to the struc­ture points to its begin­ning. So, v.y would be the same thing as *static_cast<float *>(static_cast<uintptr_t>(&v) + sizeof(float)) and v.x would be *static_cast<float *>(&v). Indeed, that expres­sion will work on many com­pil­ers (though you obvi­ous­ly shouldn’t use it).

Thus, I imag­ine componentPtr to be the “+ sizeof(float)” con­stant off­set that marks how to use the object’s address to access the desired mem­ber. If you didn’t get any of the last two para­graphs, just pre­tend you didn’t read them, because it’s not too impor­tant.
End aside

So now you see how to use mem­ber point­ers and how they work. But you’re prob­a­bly think­ing, “Xo, what the heck would com­pel any sane C++ pro­gram­mer to use mem­ber point­ers? In your last exam­ple, why couldn’t some­one just use v.x and v.y instead of tak­ing a point­er to them?”

Well first off, there aren’t real­ly any sane C++ pro­gram­mers. If you’re think­ing that you could be an excep­tion to this, you’re prob­a­bly not sane or you might not be a C++ pro­gram­mer. May­be both.

Sec­ond­ly, the idea behind them is that they’re vari­ables. You use them the same way you would any oth­er vari­able: to store results that you’ve com­put­ed or to pass data around. This means if you can boil down a long set of com­pu­ta­tions into a choice between mem­bers of a class, then you can avoid need­ing to com­pute it repeat­ed­ly and instead just pass a mem­ber point­er around.

Let’s see an exam­ple. We’re try­ing to write a func­tion that takes in a std::vector of Vector3Ds and adds a float to a com­po­nent of each one. Now, which com­po­nent is deter­mined at run­time, so we have to pass it as a para­me­ter to the func­tion:

void add(std::vector<Vector3D> & vs, int componentSelect, float amount) {
	for(std::vector<Vector3D>::iterator v_i = vs.begin() ; v_i != vs.end() ; v_i++) {
		switch(componentSelect) { // switch to determine which component to modify
		case 0:
			v_i->x += amount;
			break;
		case 1:
			v_i->y += amount;
			break;
		case 2:
			v_i->z += amount;
			break;
		default:
			break;
		}
	}
}

int main() {
	int componentSelect; // 0 means .x, 1 means .y, 2 means .z
	componentSelect = std::rand() % 3; // make a decision as to which one

	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
	add(vs, componentSelect, 1.f); // add 1.f to a component of each Vector3D
	std::copy(vs.begin(), vs.end(), std::ostream_iterator<Vector3D>(std::cout, " "));
	std::cout << std::endl; // print out the list again

	return EXIT_SUCCESS;
}

Exam­ple out­put:

<0, 2, 3> <0, 2, 3> <0, 2, 3> <0, 2, 3> <0, 2, 3>
<0, 2, 4> <0, 2, 4> <0, 2, 4> <0, 2, 4> <0, 2, 4>

The above naïve way involves this intVector3D com­po­nent con­ver­sion for each ele­ment in the con­tain­er. This is slow and involves a lot of code copy­ing & past­ing. Mov­ing the switch state­ment out­side of the loop helps with the slow, but makes the code look even worse.

Mem­ber point­ers to the res­cue:

void add(std::vector<Vector3D> & vs, int componentSelect, float amount) {
	static float Vector3D::* const componentPtrs[3] = {&Vector3D::x, &Vector3D::y, &Vector3D::z};
	float Vector3D::*componentPtr = componentPtrs[componentSelect]; // select from array
	for(std::vector<Vector3D>::iterator v_i = vs.begin() ; v_i != vs.end() ; v_i++) {
		*v_i.*componentPtr += amount; // note: very little code duplication
	}
}

Now you see that we can use mem­ber point­ers to store the result of the com­po­nent selec­tion (the switch state­ment) and then oper­ate on data using this stored result.

Note that I used *v_i.*componentPtr += amount; instead of v_i->*componentPtr += amount;. This is because v_i is a STL iterator, not a Vector3D *. This is where you see the ->* oper­a­tor (indi­rect .*) diverge from deref­er­ence-then-.*, as iterators do not need to define a ->* oper­a­tor but do need to define a deref­er­ence (*) oper­a­tor. If you real­ly want to use ->* (and who doesn’t?), you’ll need to con­vert the iter­a­tor to a point­er first:

for(std::vector<Vector3D>::iterator v_i = vs.begin() ; v_i != vs.end() ; v_i++) {
	Vector3D *v = &*v_i;
	v->*componentPtr += amount; // note the use of ->*
}

This is isn’t even the only way we could have done this: there are also a few tricks with tem­plates or STL com­po­nents like <functional> we could have used to the same effect, with vary­ing amounts of ease-of-use and com­pile-time vs. run-time flex­i­bil­i­ty.

This is just anoth­er exam­ple of the bewil­der­ing flex­i­bil­i­ty C++ offers between per­for­mance and code read­abil­i­ty, or if you like, between com­pile-time code exe­cu­tion and run-time code exe­cu­tion.

Stay tuned for more “Strange C++.” In the next issue, we’ll dis­cuss using mem­ber point­ers to point to mem­ber func­tions, and how exact­ly using them will ben­e­fit our C++ appli­ca­tions.

You should read “C++’s .* and ->* oper­a­tors: Part 2.”

  1. Of course, this is not includ­ing the indi­rect ver­sions of the lat­ter two, -> and ->* []