[ Team LiB ] Previous Section Next Section

Gotcha #46: Misunderstanding Contravariance

The rules for conversion of pointers to member are logical but often counterintuitive. An examination of an implementation of pointers to member often helps clear things up.

A pointer refers to a region of memory; it contains an address that can be dereferenced to access the memory. (The code that follows makes use of two reprehensible practices: public data and hiding of a base class nonvirtual function. This is done for illustration only and is not intended as an implicit recommendation of these practices. See Gotchas #8 and #71.)



class Employee { 


 public:


   double level_;


   virtual void fire() = 0;


   bool validate() const;


};


class Hourly : public Employee {


 public:


   double rate_;


   void fire();


   bool validate() const;


};


// . . .


Hourly *hp = new Hourly, h;


// . . .


*hp = h;


Note that the address of a data member of a particular object is not a pointer to member. It's a simple pointer that refers to a specific member of a specific object:



double *ratep = &hp->rate_; 


A pointer to member is not a pointer. A pointer to member is not an address of anything, and it doesn't refer to any particular object or location. A pointer to member refers to a specific member of an unspecified object. Therefore, an object must be supplied to dereference the pointer to member:



double Hourly::*hvalue = &Hourly::rate_; 


hp->*hvalue = 1.85;


h.*hvalue = hp->*hvalue;


The .* and ->* operators are binary dereference operators that dereference a pointer to member with a class object or class pointer, respectively (but see Gotchas #15 and #17). The pointer to member hvalue was initialized to refer to the rate_ member of the Hourly class, then dereferenced to access the rate_ members of the Hourly object h and the Hourly object referred to by hp.

A pointer to data member is generally implemented as an offset. That is, taking the address of a data member, as we did above with &Hourly::rate_, gives the number of bytes from the start of the class object at which the data member occurs. Typically, this offset value is incremented by 1, so that the value 0 can represent a null pointer to data member. Dereferencing a pointer to data member typically involves manufacturing an address by adding the offset (decremented by 1) stored in the pointer to data member to the address of a class object. The resultant pointer is then dereferenced to access the corresponding data member of the class object. For example, the expression



h.*hvalue = 1.85 


could be translated like this:



*(double *)((char *)&h+(hvalue-1)) = 1.85 


Let's look at another pointer to data member:



double Employee::*evalue = &Employee::level_; 


Employee *ep = hp;


Because an Hourly is-a Employee, we can dereference the evalue pointer to member with either type of pointer. This is the well-known implicit conversion from a derived class to its public base class:



ep->*evalue = hp->*evalue; 


An Hourly is substitutable for an Employee. However, an attempt to perform a similar conversion with pointers to member fails:



evalue = hvalue; // error! 


There is no conversion from a pointer to member of a derived class to a pointer to member of a public base class. However, the opposite conversion is legal:



hvalue = evalue; // OK 


This phenomenon is known as "contravariance"; the implicit conversions for pointers to member are precisely the inverse of those for pointers to classes. (Don't confuse contravariance with covariant return types; see Gotcha #77.) After a little reflection, the logic behind this somewhat counterintuitive rule is obvious. Since an Hourly is-a Employee, it contains an Employee subobject. Therefore, any offset within Employee is also a valid offset within Hourly. However, some offsets within Hourly are not valid for Employee. This implies that a pointer to member of a public base class may be safely converted to a pointer to member of a derived class, but not the reverse:



T SomeClass::*mptr; 


. . . ptr->*mptr  . . .


In the code snippet above, the pointer ptr can legally be a pointer to an object of type SomeClass or of any publicly derived class of SomeClass. The pointer to member mptr can contain the address of a member of SomeClass or the address of a member of any accessible base class of SomeClass.

Contravariance also applies to pointers to function member. It's just as counterintuitive and makes just as much sense, on reflection:



void (Employee::*action1)() = &Employee::fire; 


(hp->*action1)(); // Hourly::fire


bool (Employee::*action2)() const = &Employee::validate;


(hp->*action2)(); // Employee::validate


Implementations of pointers to function member vary widely but are typically small structures. The structure contains information necessary to distinguish virtual from nonvirtual members as well as other platform-specific information necessary for dealing with implementation-specific details of the structure of objects under inheritance. In the first call, through action1 above, we'll make an indirect virtual call and invoke Hourly::fire, because &Employee::fire is a pointer to a virtual member function. In the second call, through action2, we'll invoke Employee::validate, because &Employee::validate is a pointer to a nonvirtual function:



action2 = &Hourly::validate; // error! 


bool (Hourly::*action3)() = &Employee::validate; // OK


Contravariance again. It's illegal to assign the address of the derived class's validate function to a pointer to member of the base class, but it's fine to initialize a pointer to member of a derived class with the address of a base class member function. As with pointers to data member, the reason concerns safety of member access. The implementation of Hourly::validate may attempt to access data (and function) members not present in Employee. On the other hand, any members accessed by Employee::validate will also be present in Hourly.

    [ Team LiB ] Previous Section Next Section