Dec. 14, 2008, 2:08 a.m.
posted by nogood
Item 15. Pointers to Class Members Are Not PointersIt's unfortunate that pointers to class members have the term "pointer" in their descriptions, because they don't contain addresses and don't behave like pointers. The syntax for declaring a pointer to member is really not too horrible (if you're already resigned to the declarator syntax for regular pointers): int *ip; // pointer to an int int C::*pimC; // pointer to an int member of C All you have to do is use classname::* rather than a plain * to indicate you're referring to a member of classname. Otherwise, the syntax is the same as for a regular pointer declarator. void * * *const* weird1; void *A::*B::*const* weird2; The name weird1 has the type pointer to const pointer to pointer to pointer to void. The name weird2 has the type pointer to const pointer to a member of B to a pointer to a member of A, which is a pointer to void. (This is just an example, and you wouldn't normally expect to see a declaration this complex or this silly.) A regular pointer contains an address. If you dereference a pointer, you get the object at that address: int a = 12; ip = &a; *ip = 0; a = *ip; A pointer to member, unlike a regular pointer, does not refer to a specific memory location. Rather, it refers to a particular member of a class but not to a particular member of a particular object. Mechanically, it's usually clearest to consider a pointer to data member to be an offset. This is not necessarily the case, because the C++ standard says nothing about how a pointer to data member should be implemented; it says only what its syntax and behavior must be. However, most compilers implement pointers to data members as integers that contain the offset of the member referred to, plus one. (The offset is incremented so that the value 0 can represent a null pointer to data member.) The offset tells you how many bytes from the start of an object a particular member is located.
class C {
public:
//...
int a_;
};
int C::*pimC; // pointer to an int member of C
C aC;
C *pC = &aC;
pimC = &C::a_;
aC.*pimC = 0;
int b = pC->*pimC;
When we set the value of pimC to &C::a_, we're effectively setting pimC with the offset of a_ within C. Let's be clear: Unless a_ is a static member, using & in the expression &C::a_ does not give us an address; it gives us an offset. Note that this offset applies to any object of type C; that is, if the member a_ can be found 12 bytes from the start of one C object, it will be found 12 bytes from the start of any other C object. Given an offset of a member within a class, we need the address of an object of that class in order to get to the data member at that offset. That's where the unusual-looking .* and ->* operators enter. When we write pC->*pimC, we are requesting that the address in pC be augmented by the offset in pimC in order to access the appropriate data member in the C object referred to by pC. When we write aC.*pimC, we are requesting that the address of aC be augmented by the offset in pimC in order to access the appropriate data member in the C object referred to by pC. Pointers to data members are not as commonly used as pointers to member functions, but they are handy for illustrating the concept of contravariance. There is a predefined conversion from a pointer to a derived class to a pointer to any of its public base classes. We often say that there is an is-a relationship from the derived class to its public base classes, and this relationship often arises naturally from an analysis of the problem domain (see Polymorphism [2, 3]). Therefore, we can state (for example) that a Circle is-a Shape tHRough public inheritance, and C++ backs us up by providing an implicit conversion from Circle * to Shape *. No implicit conversion exists from a Shape * to a Circle * because such a conversion would not make sense; many different types of Shape may exist, and not all of them are Circles. (It also just sounds silly to say, "A Shape is a Circle.") In the case of pointers to class members, the opposite situation holds: There is an implicit conversion from a pointer to a member of a base class to a pointer to a member of a publicly derived class, but there is no conversion from a pointer to a member of a derived class to a pointer to a member of any of its bases. This concept of contravariance seems counterintuitive until we remember that a pointer to data member is not a pointer to an object; it's an offset into an object.
class Shape {
//...
Point center_;
//...
};
class Circle : public Shape {
//...
double radius_;
//...
};
A Circle is-a Shape, so a Circle object contains a Shape subobject. Therefore, any offset within Shape is also a valid offset within Circle. Point Circle::*loc = &Shape::center_; // OK, base to derived However, a Shape is not (necessarily) a Circle, so the offset of a member of Circle is not (necessarily) a valid offset within a Shape. double Shape::*extent = &Circle::radius_; // error! derived to base It makes sense to say that a Circle contains all the data members of its Shape base class (that is, it inherits those members from Shape), and C++ backs us up with an implicit conversion from a pointer to member of a Shape to a pointer to member of a Circle. It doesn't make sense to say that a Shape contains all the data members of a Circle (Shape doesn't inherit anything from Circle), and C++ reminds us of that by disallowing the conversion from pointer to member of Circle to pointer to member of Shape. |
- Comment