Team LiB
Previous Section Next Section

15.5. Access Control and Inheritance

 
Image

Just as each class controls the initialization of its own members (§ 15.2.2, p. 598), each class also controls whether its members are accessible to a derived class.

 

protected Members

 

As we’ve seen, a class uses protected for those members that it is willing to share with its derived classes but wants to protect from general access. The protected specifier can be thought of as a blend of private and public:

 

• Like private, protected members are inaccessible to users of the class.

 

• Like public, protected members are accessible to members and friends of classes derived from this class.

 

In addition, protected has another important property:

 

• A derived class member or friend may access the protected members of the base class only through a derived object. The derived class has no special access to the protected members of base-class objects.

 

To understand this last rule, consider the following example:

 

 

class Base {
protected:
    int prot_mem;     // protected member
};
class Sneaky : public Base  {
    friend void clobber(Sneaky&);  // can access Sneaky::prot_mem
    friend void clobber(Base&);    // can't access Base::prot_mem
    int j;                          // j is private by default
};
// ok: clobber can access the private and protected members in Sneaky objects
void clobber(Sneaky &s) { s.j = s.prot_mem = 0; }
// error: clobber can't access the protected members in Base
void clobber(Base &b) { b.prot_mem = 0; }

 

If derived classes (and friends) could access protected members in a base-class object, then our second version of clobber (that takes a Base&) would be legal. That function is not a friend of Base, yet it would be allowed to change an object of type Base; we could circumvent the protection provided by protected for any class simply by defining a new class along the lines of Sneaky.

 

To prevent such usage, members and friends of a derived class can access the protected members only in base-class objects that are embedded inside a derived type object; they have no special access to ordinary objects of the base type.

 

public, private, and protected Inheritance

 

Access to a member that a class inherits is controlled by a combination of the access specifier for that member in the base class, and the access specifier in the derivation list of the derived class. As an example, consider the following hierarchy:

 

 

class Base {
public:
    void pub_mem();   // public member
protected:
    int prot_mem;     // protected member
private:
    char priv_mem;    // private member
};
struct Pub_Derv : public Base {
    // ok: derived classes can access protected members
    int f() { return prot_mem; }
    // error: private members are inaccessible to derived classes
    char g() { return priv_mem; }
};
struct Priv_Derv : private Base {
    // private derivation doesn't affect access in the derived class
    int f1() const { return prot_mem; }
};

 

The derivation access specifier has no effect on whether members (and friends) of a derived class may access the members of its own direct base class. Access to the members of a base class is controlled by the access specifiers in the base class itself. Both Pub_Derv and Priv_Derv may access the protected member prot_mem. Neither may access the private member priv_mem.

 

The purpose of the derivation access specifier is to control the access that users of the derived class—including other classes derived from the derived class—have to the members inherited from Base:

 

 

Pub_Derv d1;   //  members inherited from Base are public
Priv_Derv d2;  //  members inherited from Base are private
d1.pub_mem();  //  ok: pub_mem is public in the derived class
d2.pub_mem();  //  error: pub_mem is private in the derived class

 

Both Pub_Derv and Priv_Derv inherit the pub_mem function. When the inheritance is public, members retain their access specification. Thus, d1 can call pub_mem. In Priv_Derv, the members of Base are private; users of that class may not call pub_mem.

 

The derivation access specifier used by a derived class also controls access from classes that inherit from that derived class:

 

 

struct Derived_from_Public : public Pub_Derv {
    // ok: Base::prot_mem remains protected in Pub_Derv
    int use_base() { return prot_mem; }
};
struct Derived_from_Private : public Priv_Derv {
    // error: Base::prot_mem is private in Priv_Derv
    int use_base() { return prot_mem; }
};

 

Classes derived from Pub_Derv may access prot_mem from Base because that member remains a protected member in Pub_Derv. In contrast, classes derived from Priv_Derv have no such access. To them, all the members that Priv_Derv inherited from Base are private.

 

Had we defined another class, say, Prot_Derv, that used protected inheritance, the public members of Base would be protected members in that class. Users of Prot_Derv would have no access to pub_mem, but the members and friends of Prot_Derv could access that inherited member.

 

Accessibility of Derived-to-Base Conversion

 
Image

Whether the derived-to-base conversion (§ 15.2.2, p. 597) is accessible depends on which code is trying to use the conversion and may depend on the access specifier used in the derived class’ derivation. Assuming D inherits from B:

 

• User code may use the derived-to-base conversion only if D inherits publicly from B. User code may not use the conversion if D inherits from B using either protected or private.

 

• Member functions and friends of D can use the conversion to B regardless of how D inherits from B. The derived-to-base conversion to a direct base class is always accessible to members and friends of a derived class.

 

• Member functions and friends of classes derived from D may use the derived-to-base conversion if D inherits from B using either public or protected. Such code may not use the conversion if D inherits privately from B.

 

Image Tip

For any given point in your code, if a public member of the base class would be accessible, then the derived-to-base conversion is also accessible, and not otherwise.

 

 

Key Concept: Class Design and protected Members

In the absence of inheritance, we can think of a class as having two different kinds of users: ordinary users and implementors. Ordinary users write code that uses objects of the class type; such code can access only the public (interface) members of the class. Implementors write the code contained in the members and friends of the class. The members and friends of the class can access both the public and private (implementation) sections.

 

Under inheritance, there is a third kind of user, namely, derived classes. A base class makes protected those parts of its implementation that it is willing to let its derived classes use. The protected members remain inaccessible to ordinary user code; private members remain inaccessible to derived classes and their friends.

 

Like any other class, a class that is used as a base class makes its interface members public . A class that is used as a base class may divide its implementation into those members that are accessible to derived classes and those that remain accessible only to the base class and its friends. An implementation member should be protected if it provides an operation or data that a derived class will need to use in its own implementation. Otherwise, implementation members should be private.

 

 

Friendship and Inheritance

 

Just as friendship is not transitive (§7.3.4, p. 279), friendship is also not inherited. Friends of the base have no special access to members of its derived classes, and friends of a derived class have no special access to the base class:

 

 

class Base {
    // added friend declaration; other members as before
    friend class Pal; // Pal has no access to classes derived from Base
};
class Pal {
public:
    int f(Base b) { return b.prot_mem; } // ok: Pal is a friend of Base
    int f2(Sneaky s) { return s.j; } // error: Pal not friend of Sneaky
    // access to a base class is controlled by the base class, even inside a derived object
    int f3(Sneaky s) { return s.prot_mem; } // ok: Pal is a friend
};

 

The fact that f3 is legal may seem surprising, but it follows directly from the notion that each class controls access to its own members. Pal is a friend of Base, so Pal can access the members of Base objects. That access includes access to Base objects that are embedded in an object of a type derived from Base.

 

When a class makes another class a friend, it is only that class to which friendship is granted. The base classes of, and classes derived from, the friend have no special access to the befriending class:

 

 

// D2 has no access to protected or private members in Base
class D2 : public Pal {
public:
   int mem(Base b)
       { return b.prot_mem; } // error: friendship doesn't inherit
};

 

Image Note

Friendship is not inherited; each class controls access to its members.

 

 

Exempting Individual Members

 

Sometimes we need to change the access level of a name that a derived class inherits. We can do so by providing a using declaration (§3.1, p. 82):

 

 

class Base {
public:
    std::size_t size() const { return n; }
protected:
    std::size_t n;
};
class Derived : private Base {    //  note: private inheritance
public:
    // maintain access levels for members related to the size of the object
    using Base::size;
protected:
    using Base::n;
};

 

Because Derived uses private inheritance, the inherited members, size and n, are (by default) private members of Derived. The using declarations adjust the accessibility of these members. Users of Derived can access the size member, and classes subsequently derived from Derived can access n.

 

A using declaration inside a class can name any accessible (e.g., not private) member of a direct or indirect base class. Access to a name specified in a using declaration depends on the access specifier preceding the using declaration. That is, if a using declaration appears in a private part of the class, that name is accessible to members and friends only. If the declaration is in a public section, the name is available to all users of the class. If the declaration is in a protected section, the name is accessible to the members, friends, and derived classes.

 

Image Note

A derived class may provide a using declaration only for names it is permitted to access.

 

 

Default Inheritance Protection Levels

 

In §7.2 (p. 268) we saw that classes defined with the struct and class keywords have different default access specifiers. Similarly, the default derivation specifier depends on which keyword is used to define a derived class. By default, a derived class defined with the class keyword has private inheritance; a derived class defined with struct has public inheritance:

 

 

class Base { /* ...   */ };
struct D1 : Base { /* ...   */ };   // public inheritance by default
class D2 : Base { /* ...   */ };    // private inheritance by default

 

It is a common misconception to think that there are deeper differences between classes defined using the struct keyword and those defined using class. The only differences are the default access specifier for members and the default derivation access specifier. There are no other distinctions.

 

Image Best Practices

A privately derived class should specify private explicitly rather than rely on the default. Being explicit makes it clear that private inheritance is intended and not an oversight.

 

 

Exercises Section 15.5

 

Exercise 15.18: Given the classes from page 612 and page 613, and assuming each object has the type specified in the comments, determine which of these assignments are legal. Explain why those that are illegal aren’t allowed:

 

 

Base *p = &d1;  //  d1 has type Pub_Derv
p = &d2;        //  d2 has type Priv_Derv
p = &d3;        //  d3 has type Prot_Derv
p = &dd1;       //  dd1 has type Derived_from_Public
p = &dd2;       //  dd2 has type Derived_from_Private
p = &dd3;       //  dd3 has type Derived_from_Protected

 

Exercise 15.19: Assume that each of the classes from page 612 and page 613 has a member function of the form:

 

void memfcn(Base &b) { b = *this; }

 

For each class, determine whether this function would be legal.

 

Exercise 15.20: Write code to test your answers to the previous two exercises.

Exercise 15.21: Choose one of the following general abstractions containing a family of types (or choose one of your own). Organize the types into an inheritance hierarchy:

(a) Graphical file formats (such as gif, tiff, jpeg, bmp)

 

(b) Geometric primitives (such as box, circle, sphere, cone)

 

(c) C++ language types (such as class, function, member function)

 

Exercise 15.22: For the class you chose in the previous exercise, identify some of the likely virtual functions as well as public and protected members.


 
Team LiB
Previous Section Next Section