Granting Access, Friends and Virtual Base Classes
|
Learning
Objectives After completing this module,
students should be able to: •
Grant access to inherited members in private and protected
inheritance with using-declarations. •
Recognise the legacy access-declaration syntax and its status. •
Use friend functions, friend classes and friend member functions,
and state the rules of friendship. •
Explain the diamond problem and solve it with virtual base
classes. •
Describe constructor and destructor order and the memory cost of
virtual inheritance. |
|
Important: The topic “Granting
Access” of the syllabus means restoring access to members that were made
less accessible by protected or private inheritance (Section 1). Friend
functions and classes (Section 2) are a related but different way of giving
access to selected outside code, and they are included in this module because
they are often taught together. |
1. Granting Access to Inherited
Members
1.1 The problem
In private (or protected) inheritance, the public members of the
base class become private (or protected) in the derived class (Module 1).
Outside code can then no longer use them through a derived object, even when
the derived class would like some of them to stay public.
Granting access means making selected inherited members accessible
again, with the same access as the section of the derived class in which the
declaration is written.
1.2 The using-declaration
class Derived : private Base
{
public:
using
Base::member; // member is public
again in Derived
};
Program 4.1: Restoring and raising access.
#include <iostream>
using namespace std;
class Base {
public:
int pub =
1;
void show()
const { cout << "Base::show()\n"; }
protected:
int prot =
2;
private:
int priv =
3;
public:
int
getPriv() const { return priv; }
};
/* Private inheritance: every public member of Base
becomes private in Derived.
using-declarations in the public section grant access again. */
class Derived : private Base {
public:
using
Base::pub; // public
again
using
Base::show; // public
again
using
Base::prot; // a
protected member becomes public
// using
Base::priv; // error:
priv is inaccessible in Derived
};
int main()
{
Derived d;
d.pub = 10;
d.show();
cout
<< "pub = " <<
d.pub << "\n";
cout
<< "prot = " << d.prot << " (raised from
protected to public)\n";
//
d.getPriv(); //
error: not restored, still private
return 0;
}
Output:
Base::show()
pub = 10
prot = 2 (raised from protected to public)
Figure 4.1: Private
inheritance hides all members; using-declarations in a public section grant
access again.
1.
Because Derived
inherits privately, pub and show()
would be private. using Base::pub;
and using Base::show; in the public section make them public again.
2.
using
Base::prot; makes a protected member of the base class public
in Derived: a using-declaration may raise
access, as long as the member is accessible to Derived
in the first place.
3.
The private member priv
can never be granted: it is not accessible in Derived.
Trying to grant a private member, or calling a function that has not been
restored, gives errors:
error: ‘int Base::priv’ is private within this
context
error: ‘int Base::getPriv() const’ is inaccessible
within this context
|
Rule |
Explanation |
|
Access = the section of the declaration |
The member gets the access of the section (public, protected or private)
where the using-declaration appears |
|
Only accessible members |
A base member that is private in the base class cannot be
granted |
|
Names, not signatures |
using
Base::f; names all
overloads of f together |
|
Can lower access too |
A using-declaration in the private section of a publicly
derived class makes the member private in the derived class |
|
Scope |
Applies to the derived class and to classes derived from it |
1.3 The legacy access declaration
Older C++ code uses a shorter syntax without the using
keyword, called an access declaration: just the qualified name of the
member.
Program 4.2: An access declaration.
// Legacy syntax: an access declaration (no 'using'
keyword).
#include <iostream>
using namespace std;
class Base {
public:
int value =
5;
};
class Derived : private Base {
public:
Base::value;
// access declaration (deprecated)
};
int main()
{
Derived d;
cout
<< d.value << "\n";
return 0;
}
|
Version Note: Access
declarations (Base::value;) are
deprecated in favour of using-declarations (using
Base::value;). They have been
deprecated in the C++ standard for a long time, and compilers still accept
them with a warning. GCC 13 gives the same warning in every mode from -std=c++98
to -std=c++20: |
warning: access declarations are deprecated in favour
of using-declarations; suggestion: add the ‘using’ keyword [-Wdeprecated]
Older books (including the book on which this syllabus is based) describe
only the access declaration. Know the old form for examinations, but write using in new code.
2. Friend Functions and Friend
Classes
A friend is a function or class that is allowed to access the private and protected
members of another class, although it is not a member of it. The class itself grants
friendship with the keyword friend.
2.1 Friend function
Program 4.3: A non-member function reads a
private member.
#include <iostream>
using namespace std;
class Student {
private:
int marks;
public:
Student(int
m) : marks(m) {}
friend void
displayMarks(const Student &s); //
not a member function
};
void displayMarks(const Student &s)
{
cout
<< "Marks: " << s.marks << "\n"; // private member is accessible
}
int main()
{
Student
s(85);
displayMarks(s);
return 0;
}
Output:
Marks: 85
displayMarks() is declared inside
Student with friend but defined outside; it is not called with the dot
operator and has no this pointer, so
the object is passed as an argument.
2.2 Friend class
If a class is declared a friend, all member functions of that
class can access the private members of the class that granted friendship.
Program 4.4: An Auditor class that may
inspect an Account.
#include <iostream>
using namespace std;
class Account {
private:
double
balance;
public:
Account(double b) : balance(b) {}
friend
class Auditor;
};
class Auditor {
public:
void
inspect(const Account &a) const
{
cout
<< "Account balance: " << a.balance <<
"\n";
}
};
int main()
{
Account
account(25000.50);
Auditor
auditor;
auditor.inspect(account);
return 0;
}
Output:
Account balance: 25000.5
Figure 4.2: Account
grants friendship to Auditor. The friendship goes in one direction only.
2.3 Friend member function
Friendship can be given to one member function of another class
instead of the whole class. The other class must be declared first, and the
friend function must be defined after the class that grants the friendship is
complete.
Program 4.5: Only Printer::show()
is a friend of Box.
#include <iostream>
using namespace std;
class Box; // forward
declaration
class Printer {
public:
void
show(const Box &b) const; //
defined after Box is complete
};
class Box {
private:
int side;
public:
Box(int s)
: side(s) {}
// only
this function is a friend
friend void
Printer::show(const Box &b) const;
};
void Printer::show(const Box &b) const
{
cout
<< "Side of the box: " << b.side << "\n";
}
int main()
{
Box b(7);
Printer p;
p.show(b);
return 0;
}
Output:
Side of the box: 7
2.4 Friend operator functions
Friend functions are commonly used to overload operators that must have a
stream or another type as their left operand, such as operator<<
for output.
Program 4.6: An overloaded output operator
as a friend.
#include <iostream>
using namespace std;
class Point {
private:
int x, y;
public:
Point(int
a, int b) : x(a), y(b) {}
friend
ostream &operator<<(ostream &os, const Point &p);
};
ostream &operator<<(ostream &os, const
Point &p)
{
return os
<< "(" << p.x << ", " << p.y
<< ")";
}
int main()
{
Point p(3,
4);
cout
<< "Point: " << p << "\n";
return 0;
}
Output:
Point: (3, 4)
2.5 Rules of friendship
|
Rule |
Meaning |
|
Granted, not taken |
A class gives friendship; a function cannot declare itself
a friend of another class |
|
Not symmetric |
If A makes B a friend, B can access A’s members, but A
cannot access B’s |
|
Not transitive |
A friend of a friend is not a friend |
|
Not inherited |
A friend of the base class is not automatically a friend of
its derived classes |
|
Not a member |
A friend function has no this
pointer and is not affected by the access specifier in which the declaration
is written |
Friendship is not inherited: if F
is a friend of Base it may access the private
members of Base, but not the private members
that Derived adds:
error: ‘int Derived::b’ is private within this
context
|
Note: Friends weaken
encapsulation. Use them for closely related helpers, operators and testing,
not as a general way around access rules. |
3. Virtual Base Classes
3.1 The diamond problem
Suppose Student and Employee are both derived from Person,
and TeachingAssistant is derived from
both. Then a TeachingAssistant object reaches Person through two paths (hybrid inheritance, the diamond).
Figure 4.3: The
diamond: TeachingAssistant inherits Person twice.
With ordinary inheritance the object contains two separate copies
of Person: one inside the Student
part and one inside the Employee part. This causes two problems: ambiguity
when a member of Person is named,
and duplicate data.
Program 4.7: Two copies of id.
#include <iostream>
using namespace std;
class Person {
public:
int id =
100;
};
class Student : public Person {};
class Employee : public Person {};
class TeachingAssistant : public Student, public
Employee {};
int main()
{
TeachingAssistant obj;
// cout
<< obj.id; // error:
request for member 'id' is ambiguous
obj.Student::id = 1;
// two separate Person subobjects
obj.Employee::id = 2;
cout
<< "Student path ID : " << obj.Student::id <<
"\n";
cout
<< "Employee path ID: " << obj.Employee::id <<
"\n";
cout
<< "sizeof(TeachingAssistant) = " << sizeof(obj) <<
"\n";
return 0;
}
Output:
Student path ID : 1
Employee path ID: 2
sizeof(TeachingAssistant) = 8
The two id values are independent (1 and
2); the size 8 is two int
variables. Naming obj.id directly, or
converting to Person *, is an error:
error: request for member ‘id’ is ambiguous
error: ‘P’ is an ambiguous base of ‘TA’
3.2 The solution: virtual inheritance
If the intermediate classes inherit the common base virtually, all
of them share a single copy of it. The keyword virtual
is written in the derivation of the intermediate classes:
class Student
: virtual public Person { ... };
class Employee : virtual public Person { ... };
class TeachingAssistant : public Student, public
Employee { ... };
Program 4.8: A TeachingAssistant with one
shared Person.
#include <iostream>
#include <string>
using namespace std;
class Person {
public:
string
name;
Person() :
name("Unknown") { cout << "Person constructor\n"; }
};
class Student : virtual public Person {
public:
int marks =
85;
};
class Employee : virtual public Person {
public:
int
employeeId = 501;
};
class TeachingAssistant : public Student, public
Employee {
public:
TeachingAssistant()
{
name =
"Rahul"; //
one shared Person: no ambiguity
cout
<< "TeachingAssistant constructor\n";
}
void
display() const
{
cout
<< "Name: " << name << "\n";
cout
<< "Marks: " << marks << "\n";
cout
<< "Employee ID: " << employeeId << "\n";
}
};
int main()
{
TeachingAssistant obj;
obj.display();
return 0;
}
Output:
Person constructor
TeachingAssistant constructor
Name: Rahul
Marks: 85
Employee ID: 501
•
“Person constructor” appears once: only one
Person subobject exists.
•
name
can be used without qualification in TeachingAssistant
because there is only one name
and no ambiguity.
•
The virtual
keyword is applied where Student
and Employee are declared, before
the diamond is formed. It affects the derived classes of every later
combination.
3.3 Constructors of virtual base classes
Since the shared base may be reached through several paths, the most
derived class is responsible for initializing it. Initializer calls to the
virtual base in the intermediate classes are ignored when they are part of a
larger object.
Program 4.9: Person(100)
is called by TeachingAssistant; Person(10)
and Person(20) are used only when a
Student or Employee is created on its own.
#include <iostream>
using namespace std;
class Person {
public:
int id;
Person(int
n) : id(n) { cout << "Person: " << id <<
"\n"; }
};
class Student : virtual public Person {
public:
Student() :
Person(10) { cout << "Student constructor\n"; }
};
class Employee : virtual public Person {
public:
Employee()
: Person(20) { cout << "Employee constructor\n"; }
};
class TeachingAssistant : public Student, public
Employee {
public:
TeachingAssistant() : Person(100)
// the most-derived class initializes it
{
cout
<< "TeachingAssistant constructor\n";
}
};
int main()
{
TeachingAssistant obj;
cout
<< "Final ID: " << obj.id << "\n";
cout
<< "--- creating a Student directly\n";
Student s;
cout
<< "Student's Person ID: " << s.id <<
"\n";
return 0;
}
Output:
Person: 100
Student constructor
Employee constructor
TeachingAssistant constructor
Final ID: 100
--- creating a Student directly
Person: 10
Student constructor
Student's Person ID: 10
When TeachingAssistant is constructed,
its own initializer Person(100) is used
once and the calls Person(10) and Person(20) in Student
and Employee are skipped. A
stand-alone Student uses Person(10). If the virtual base has no default
constructor, every class derived from it (directly or indirectly) must
be able to initialize it.
3.4 Order of construction and destruction
Virtual base classes are constructed first, before the non-virtual
bases, and are destroyed last.
Program 4.10: Top is a virtual base of Left
and Right; Bottom derives from both.
#include <iostream>
using namespace std;
class Top {
public:
Top() {
cout << "Top constructor\n"; }
~Top() {
cout << "Top destructor\n"; }
};
class Left : virtual public Top {
public:
Left() {
cout << "Left constructor\n"; }
~Left() {
cout << "Left destructor\n"; }
};
class Right : virtual public Top {
public:
Right() {
cout << "Right constructor\n"; }
~Right() {
cout << "Right destructor\n"; }
};
class Bottom : public Left, public Right {
public:
Bottom() {
cout << "Bottom constructor\n"; }
~Bottom() {
cout << "Bottom destructor\n"; }
};
int main()
{
Bottom b;
return 0;
}
Output:
Top constructor
Left constructor
Right constructor
Bottom constructor
Bottom destructor
Right destructor
Left destructor
Top destructor
Top is constructed once and
before Left and Right
(in declaration order); destruction is the exact reverse.
3.5 The cost of virtual inheritance
To find the shared subobject at run time, a virtual-inheritance class
stores hidden information (typically a pointer to a table of offsets, vptr). Accessing the virtual base therefore needs one
more indirection, and objects become larger.

Figure 4.4: Typical
object layouts of the diamond: ordinary and virtual inheritance.
Program 4.11: Comparing the object sizes.
#include <iostream>
using namespace std;
/* Ordinary diamond */
struct P1 { int id; };
struct S1 : P1 {};
struct E1 : P1 {};
struct TA1 : S1, E1 {};
/* Virtual diamond */
struct P2 { int id; };
struct S2 : virtual P2 {};
struct E2 : virtual P2 {};
struct TA2 : S2, E2 {};
int main()
{
cout
<< "Ordinary inheritance: sizeof(TA1) = " << sizeof(TA1)
<< " (two copies of
P1::id)\n";
cout
<< "Virtual inheritance : sizeof(TA2) = " << sizeof(TA2)
<< " (one copy of
P2::id plus bookkeeping pointers)\n";
return 0;
}
Output (the sizes are compiler-specific;
this is GCC on a 64-bit system):
Ordinary inheritance: sizeof(TA1) = 8 (two copies of P1::id)
Virtual inheritance : sizeof(TA2) = 24 (one copy of P2::id plus bookkeeping
pointers)
|
Feature |
Ordinary
inheritance |
Virtual
inheritance |
|
Copies of the common base in the final object |
One for each path (two in a diamond) |
Exactly one |
|
Ambiguity on common-base members |
Yes |
No |
|
Who initializes the common base |
Each intermediate class |
The most derived class |
|
Object size and access speed |
Smaller; direct access |
Larger; extra indirection |
|
Conversion derived → virtual base |
Fixed offset |
Computed at run time |
|
Note: A static_cast
from a virtual base class down to a derived class is not allowed; use dynamic_cast
(Module 7). |
Use virtual inheritance only where a real diamond exists. The C++
Standard Library itself uses it in the stream classes (iostream
derives from istream and ostream, which share a virtual ios
base).
4. Quick Revision
•
Granting access: using
Base::member; in the desired section of the derived class restores (or
raises, from protected) access; private base members cannot be granted. The old
form Base::member; is deprecated.
•
A friend function/class may access private members;
friendship is granted, not symmetric, not transitive and not inherited.
•
Diamond inheritance gives two copies of the common
base; virtual inheritance gives one.
•
The most derived class initializes a virtual base;
virtual bases are constructed first and destroyed last.
5. Important Examination
Questions
Short-answer questions
1.
What is meant by granting access to inherited members?
How is it done?
2.
What is an access declaration? Why is it deprecated?
3.
What is a friend function? Write its syntax.
4.
State three rules of friendship.
5.
What is the diamond problem?
6.
What is a virtual base class? Why is it used?
7.
Who initializes a virtual base class?
Long-answer and
programming questions
1.
Explain how access to inherited members can be restored
in private inheritance, with a program.
2.
Explain friend functions, friend classes and friend
member functions with programs.
3.
Explain the diamond problem and solve it with virtual
base classes using a program.
4.
Show the order of constructor and destructor calls in a
program with a virtual base class.
5.
Compare ordinary and virtual inheritance with respect
to object size, ambiguity and initialization.
End of Module 4
Next: Module 5 – Polymorphism and
Virtual Functions
No comments:
Post a Comment