0% found this document useful (0 votes)
4 views

week4OOP

The document provides a comprehensive overview of advanced object-oriented programming concepts in C++ as taught by Dr. Abdulrauf Gidado. It covers essential topics such as inheritance, constructors, destructors, and assignment operators, along with detailed examples and memory layout implications. The course emphasizes the importance of understanding special members and resource management in C++ programming.

Uploaded by

cameron.king1202
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
4 views

week4OOP

The document provides a comprehensive overview of advanced object-oriented programming concepts in C++ as taught by Dr. Abdulrauf Gidado. It covers essential topics such as inheritance, constructors, destructors, and assignment operators, along with detailed examples and memory layout implications. The course emphasizes the importance of understanding special members and resource management in C++ programming.

Uploaded by

cameron.king1202
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 83

COMP-3400-

30-R-2025W
Advanced
Object-Oriented
Systems Design
Using C++

Object-Oriented Programming

Dr. Abdulrauf Gidado with examples and figures


from Stroustrup, Bjarne (2023). A Tour of C++,
Third Edition. Addison-Wesley Professional,
Boston, MA. and Paul P. Slides
Outline

Basic C++ Object-Oriented Programming


• Inheritance Overview
• Member Overview
• Constructors
• Destructors
• Copy and Move Assignment Operators
Inheritance Overview
• C++ support for object-based/oriented programming
includes:
• support for both single and multiple inheritance of any
class/struct
• each type can be virtually or non-virtually inherited from
• when inheriting from a type, a scope access
applied to the inherited class’ members, i.e.,
public, protected, private
• unless inheritance is explicitly specified, C++ classes
and structures do not inherit from any other type
• unionS are not permitted to inherit from other types
• when porting code from other object-oriented
languages, inheritance code should be virtual public in
C++
Member Overview

• static member variables/functions are like global


variable/functions except their scope is constrained to
be within the type they are defined in
• when porting code from other object-oriented
languages, all non-static member functions
should be virtual in C++
Member Overview (con’t)

Every C++ struct, union, and class, can have


special members:
• a default constructor
• a copy constructor
• a move constructor (starting in C++11)
• a copy assignment operator
• a move assignment operator (starting in C++11)
• a destructor
Constructors

All C++ special members that are constructors:


• have no return type
• have the same name as the name of the class,
struct, or union they are defined in
• are not considered functions in the language
• e.g., they can only be invoked by creating an
instance of that type
Constructors (con’t)
w03/constructor-examples.cxx
1 #include <utility>
2 struct Type
3 {
4 int* p;
5
6 Type() : p(new int(42)) { } // default constructor
7 Type(int i) : p(new int(i)) { } // a constructor
8 Type(Type const& t) : p(new int(*t.p)) { } // copy constructor
9 Type(Type&& t) : p(t.p) { t.p = nullptr; } // move constructor
10
11 ~Type() { delete p; } // destructor
12 };
13
14 Type a; // a is default constructed
15 Type b=a, c(a); // b and c are copy constructed
16 Type d=std::move(a), e(std::move(b)), f(Type()); // d, e, and f are move constructed
17 Type y(59), z(60); // y and z invoke the int constructor
A default
constructor is a
constructor:
Constructors
(con’t) • with no arguments, and, is
invoked when an object is
created having no
arguments passed to its
constructor.
Constructors (con’t)
w03/default-constructor.cxx
1 struct Type
2 {
3 int* p;
4 // ...
5 Type() : // invoke constructors and initialize members after : before {
6 p(new int(42)) // construct int* with the result of new int(42)
7 { // after { all members have been constructed
8 }
9 //...
10 };
Constructors (con’t)

A copy constructor is a
constructor:
• with a T const& argument,
and,
• where T is the name of the class, struct, or union it is in
• is invoked when the compiler/programmer needs to create and
copy the data of another instance of the same type into the
created type.
Constructors (con’t)

w03/copy-constructor.cxx
1 struct Type
2 {
3 int* p;
4 // ...
5 Type(Type const& t) : // invoke constructors and initialize members after : before {
6 p(new int(*t.p)) // construct int* with the result of new int(42)
7 { // after { all members have been constructed
8 }
9 //...
10 };
Constructors (con’t)

A move constructor is a constructor:


• with a T&& argument, and,
• where T is the name of the class, struct, or union it is in
• is invoked when the compiler/programmer needs to create and move the
data of another instance of the same type into the created type.
Constructors (con’t)
w03/move-constructor.cxx
1 struct Type
2 {
3 int* p;
4 // ...
5 Type(Type&& t) : // invoke constructors and initialize members after : before {
6 p(t.p) // construct int* with the result of new int(42)
7 { // after { all members have been constructed
8 t.p = nullptr; // t refers to an object that still needs to be destroyed!
9 }
10 //...
11 };
Destructors
All destructors:
• have no return type
• have their name prefixed with a ~ in front of the class’, struct’s, or union’s
name they are defined in
• are considered void functions in the language
• Never invoke a destructor directly unless using “placement new”!
• should not emit exceptions
• are implicitly noexcept unless explicitly stated otherwise in code (starting with
C++11)
• are assumed by C++ programmers to release all owned resources
acquired during the lifetime of an object
• e.g., calling operator delete, close(fd), etc. as is needed
• i.e., “Resource Acquisition is Initialization” (RAII)
Destructors (con’t)

• are invoked when an instance object goes out-of-scope


• e.g., global variables, stack-allocated (i.e., automatic)
variables, members in an aggregate type that is being
destructed, etc.
• are invoked when the appropriate operator delete or
operator delete[] is called on dynamically-allocated object
• are explicitly invoked when a programmer needs to destroy an
object created with “placement new”
Destructors (con’t)

w03/destructor.cxx
1 struct Type
2 {
3 int* p;
4 // ...
5 ~Type()
6 {
7 delete p; // free memory associated with p
8 }
9 // ...
10 };
A copy assignment
Copy and
operator:
Move • is a member
Assignment assignment operator
Operators overload
• with a single T const&
argument.
Copy and Move Assignment Operators (con’t)
w03/copy-assignment.cxx
1 #include <utility> // for std::swap since C++11, <algorithm> before C++11
2 struct Type {
3 int* p;
4 Type(int const& i) : p(new int{i}) { }
5 Type(Type const& t) : p(new int{*t.p}) { }
6 Type& operator =(Type const& t) // copy assignment operator
7 {
8 Type local(t); // create a local copy of t
9 std::swap(p,local.p); // exchange pointers
10 return *this; // local will now be destroyed
11 }
12 };
13 int main()
14 {
15 Type a(42), b{52}, c = 62; // a and b invoke Type(int const&) constructor
16 a = b = c; // copy assignment
17 }
Copy and Move Assignment Operators (con’t)

A move assignment operator:


• is a member assignment operator overload
• with a single T&& argument.
Copy and Move Assignment Operators
(con’t) w03/move-assignment.cxx
1 #include <utility>
2 struct Type {
3 int* p;
4 Type() : p{new int{}} { }
5 Type(int const& i) : p{new int{i}} { }
6 Type(Type&& t) : p{std::move(t.p)} { }
7 Type& operator =(Type&& t) // move assignment operator
8 {
9 Type local(std::move(t)); // move t into *this
10 std::swap(p,local.p); // exchange pointers
11 return *this; // local will now be destroyed
12 }
13 };
14 int main()
15 {
16 Type a(11), b{12}, c = 13; // a, b, and c invoke Type(int const&) constructor
17 a = std::move(c); b = Type{}; // move assignments
18 }
Outline

2 Objects & Memory Layout Single


struct/class Layout
struct/class Layout: Virtual Member
Implications Other Object-Oriented Languages
When To Use And Not To Use
Inheritance Noteworthy Items
Member Initialization Lists
Resource Acquisition Is Initialization (RAII)
Objects & Memory Layout: Single struct/class Layout
Use of virtual: None
References: [3, §2.5.2], [1, [intro.memory], [basic.types]]
Assuming only one access level:
• Each non-static data member appears in memory in declaration order where
subsequent members are at a higher memory address than the previous
• for data members at the same access level
• such data members may not be positioned immediately after each other
• The compiler can add padding between/after members for alignment purposes.
• Excluding the extra space added for alignment purposes, the size of the
object is the sum of the sizes of all data members or 1, whichever is larger.
• In C++11, alignment [1, [basic.align]] can be queried and controlled via alignas
[1, [dcl.type.simple]], alignof [1, [expr.alignof]], align [1, [ptr.align]], and,
alignment_of [1, [meta.unary.prop.query]].
Objects & Memory Layout: Single struct/class
Layout (con’t)
Example:
w03/layout-ega.cxx
1 struct node // no inheritance
{
2
void add(char); // not virtual
3
char data_[10];
4
void remove(char); // not virtual
5
node *next_;
6
7 };

e.g., sizeof(node) ≥ sizeof(char[10]) + sizeof(node*)


e.g., sizeof(node) = sizeof(char[10]) + paddingchar[10] + sizeof(node*) +
paddingnode∗
struct/class
Layout: Virtual Member
Implications

Use of virtual: With at least one member function.


References: [3, §3.5.1]
struct/class layout is determined by the data members and the need to have a
compiler-generated table of member function pointers.
The functions the compiler stores in the table are the ones declared as virtual.
The compiler decides:
• how it chooses to represent, store, and access the table of function pointers,
and,
• where it chooses to place the pointer to such (i.e., at the beginning or the
end) within the struct/class, etc.
struct/class
Layout: Virtual Member
Implications (con’t)
Note:
• Constructors and static functions cannot be declared as virtual.
• A virtual destructor is allocated a slot. All descendants types’ destructors
will share the same slot as there can only be one destructor per type.
• Compiler will implicitly add code to types’ (implicit and/or explicit)
constructors to properly deal with any required internal state.
struct/class Layout: Virtual Member Implications
(con’t)
One way to implement virtual members:
• For each struct/class with at least one virtual function store a pointer
to an array of function pointers to those virtual functions in each
instance.
• ASIDE: Since C++ is statically-typed so all types are known at compile
time, thus, the compiler does not need to store a vtable pointer with
each instance of a type
—it can instead “hard-code” such where needed since it “knows” the
type being used in the program code. (Of course that type might not
be the underlying (derived/abstract) type being used...)
struct/class
Layout: Virtual Member
Implications (con’t)
Example X:
w03/layout-egbX.cxx
1 struct X
2 {
3 int x;
4 virtual void f();
5 virtual void g(char);
6 virtual void h(float);
7 };

An X instance X vtable
Slot 0: &X::f
vtblptr
x Slot 1: &X::g
Slot 2: &X::h

NOTE: One vtable is needed per type.

27 / 83
struct/class Layout: Virtual Member Implications
(con’t)
Example Y:
w03/layout-egbY.cxx
1 // struct X from previous example
2 struct Y : X
3 {
4 int y;
5 void g(char) override;
6 virtual void q(Y const&);
7 };

A Y instance Y vtable
Slot 0: &X::f
vtblptr
Slot 1: &Y::g
x
y Slot 2: &X::h
Slot 3: &Y::q
struct/class
Layout: Virtual Member
Implications (con’t)
Example Z:
w03/layout-egbZ.cxx
1 // struct X from previous example
2 // struct Y from previous example
3 struct Z : Y
4 {
5 int z;
6 void h(float) override;
7 virtual void r(Z&);
8 };
Z vtable
A Z instance
Slot 0: &X::f
vtblptr
Slot 1: &Y::g
x
Slot 2: &Z::h
y
Slot 3: &Y::q
z
Slot 4: &Z::r
struct/class
Layout: Virtual Member
Implications (con’t)
Example Y2:
w03/layout-egbY2.cxx
1 // struct X from previous example
2 struct Y2 : X
3 {
4 int y2;
5 using X::g; // Unhide X members named g
6 virtual void g(double); // F0. Different signature!
7 };

A Y2 instance Y2 vtable
Slot 0: &X::f
vtblptr
x Slot 1: &X::g
y2 Slot 2: &X::h
Slot 3: F0
struct/class
Layout: Virtual Member
Implications (con’t)
Example W:
w03/layout-egbW.cxx
1 struct W
2 {
3 int w;
4 virtual void f(); // F1
5 void f(long); // F2
6 virtual void f(int); // F3
7 virtual void f(double); // F4
8 };

A W instance W vtable
Slot 0: F1
vtblptr
w Slot 1: F3
Slot 2: F4

NOTE: Signature matters! F2 is not virtual!


struct/class
Layout: Virtual Member
Implications (con’t)
Example W2:
w03/layout-egbW2.cxx
1 // struct W from previous example
2 struct W2 : W
3 {
4 int w2;
5 void f() override; // F5
6 void f(long double d); // F6
7 };

A W2 instance W2 vtable
Slot 0: F5
vtblptr
w Slot 1: F3
Slot 2: F4
w2
Slot 3: F6
struct/class Layout: Non-Virtual Inheritance

• The memory layout of single-access level, non-virtual, singly-inherited


types is the concatenation of the memory layouts of all types in the
hierarchy starting with the base type of the hierarchy:
• w03/layout-egc1a.cxx
1 struct W { };
2 struct X : W { int x; };
3 struct Y : X { int y; };
4 struct Z : Y { int z; };
struct/class Layout: Non-Virtual Inheritance
(con’t)

• In terms of memory layout Z is equivalent to:


• w03/layout-egc1b.cxx
1 struct Z_mem_layout
2 {
3 int x;
4 int y;
5 int z;
6 };
struct/class Layout: Non-Virtual Inheritance
(con’t)
Notice that W requires zero (0) bytes of memory and so it is an empty base
class with respect to X, Y, and Z.
• A C++ compiler will render any empty base class in a single-
inheritance hierarchy so that it occupies zero bytes of
memory.
• This is called empty base optimization (EBO).
• Otherwise they are required to occupy at least one byte of memory
since the sizeof of a type must at least be one (1). [1, [expr.sizeof],
[class]]
struct/class Layout: Non-Virtual Inheritance
(con’t)

• The memory layout of single-access level, non-virtual, multiply-inherited


types is the concatenation of the memory layouts of all types in the
hierarchy in some unspecified order of the specified inheritance
appearance starting with the root types [1, [class.mi]] of the hierarchies:
• w03/layout-egd1a.cxx
1 struct Z { int z; };
2 struct Y { int y; };
3 struct A : Z { int a; };
4 struct B : A { int b; };
5 struct C : A, Y { int c; };
6 struct D : B, C { int d; };
struct/class Layout: Non-Virtual Inheritance
(con’t)

• In terms of memory layout D could be equivalent to:


• w03/layout-egd1b.cxx
1 struct D_mem_layout_wont_compile
2 {
3 int z; int a; int b; // D::B path
4 int z; int a; int y; int c; // D::C path
5 int d;
6 };
struct/class Layout: Non-Virtual Inheritance
(con’t)
• Unqualified access to names that are ambiguous through inheritance
are
• compile-time errors.
• Use the scope resolution operator (i.e., ::) to qualify the name by
specifying the type to resolve the ambiguity:
• w03/layout-egd1c.cxx
1 D d;
2 d.z = 1; // ERROR! Ambiguous!
3 d.B::z = 1; // Okay. Unique via B.
4 d.C::z = 2; // Okay. Unique via C.
5 d.y = 3; // Okay. Unambiguous.
struct/class Layout: Virtual Inheritance

• The memory layout of virtually inherited types must ensure that the
inheriting type will only have a single instance of each virtually
inherited type at that level otherwise all other aspects are the same as non-
virtual inheritance.
• Consider:
• w03/layout-ege1a.cxx
1 // Only A is inherited virtually below!
2 struct Z { int z; };
3 struct Y { int y; };
4 struct A : Z { int a; };
5 struct B : virtual A { int b; };
6 struct C : virtual A, Y { int c; };
7 struct D : B, C { int d; };
struct/class Layout: Virtual Inheritance (con’t)
• In terms of data members —not memory layout— D is equivalent to:
• w03/layout-ege1b.cxx
1 struct D_data_members
2 {
3 int z; int a; // D::A
4 int b; // D::B
5 int y; int c; // D::C
6 int d; // D
• 7 };

• since in the hierarchy both B and C virtually inherit from A and both B
and C are siblings at that level in the hierarchy.
• IMPORTANT: There is only one instance of A.
struct/class Layout: Virtual Inheritance (con’t)

Compilers have some discretion on how exactly to lay out memory.


• The memory layout chosen must be invariant across all possible uses!

This problem can be solved by storing “pointer” offset(s) to the virtually


inherited base type(s).
struct/class Layout: Virtual Inheritance
(con’t)
Consider again:
w03/layout-ege1a.cxx
1 // Only A is inherited virtually below!
2 struct Z { int z; };
3 struct Y { int y; };
4 struct A : Z { int a; };
5 struct B : virtual A { int b; };
6 struct C : virtual A, Y { int c; };
7 struct D : B, C { int d; };

A possible (RAM inefficient) memory layout might be…


struct/class Layout: Virtual Inheritance (con’t)
A D instance
Z::z

A::a

A* B:: ptr1

B::b

A* C:: ptr2
Y::y
C::c

D::d
struct/class Layout: Virtual Inheritance
(con’t)
Does multiple virtual inheritance look complicated?
This is one of the reasons why most OOP languages don’t support general
multiple inheritance.
Some OOP languages do support limited multiple inheritance, e.g.,
• no data members,
• declared but not defined function members, and/or
• all inheritance must be virtual which eliminates some layout and access
issues.
struct/class Layout: Virtual Inheritance (con’t)

• Unqualified access to names that are ambiguous through inheritance


are compile-time errors.
• Since virtual inheritance ensures there is only one copy of a base
class, accesses that would have been ambiguous with non-virtual inheritance
may become unambiguous…
struct/class Layout: Virtual Inheritance
(con’t)
• Given:
• w03/layout-ege1a.cxx
1 // Only A is inherited virtually below!
2 struct Z { int z; };
3 struct Y { int y; };
4 struct A : Z { int a; };
5 struct B : virtual A { int b; };
6 struct C : virtual A, Y { int c; };
7 struct D : B, C { int d; };
struct/class Layout: Virtual Inheritance
(con’t)
• Then there are no issues with this:
• w03/layout-ege1c.cxx
1 D d;
2 d.z = 1;
3 d.a = 2;
4 d.y = 3;

• But realize that in the worst case accesses to A’s and Z’s members
through B, C, and/or D are now via an indirection/computed memory
offset.
struct/class Layout: Virtual Inheritance (con’t)

Know that multiple virtual inheritance can introduce ambiguities,


e.g.,
• when constructing virtual base objects
• when calling member functions defined in or accessible via the virtual base
from a derived type
struct/class Layout: Virtual Inheritance (con’t)
Ambiguous Construction Case…
When there is only one virtual base instance for two or more types,
then which type pathway should initialize the virtual base instance?
Consider:
w03/layout-ege1d.cxx
1 struct A { /* ... */ };
2 struct B : virtual A { /* ... */ };
3 struct C : virtual A, Y { /* ... */ };
4 struct D : B, C { /* ... */ };

Q. When creating an instance of D, should B’s or C’s constructor initialize A?


A. In general the compiler cannot determine which constructor should be
used as it is ambiguous.
struct/class Layout: Virtual Inheritance (con’t)
w03/layout-ege1e.cxx
1 struct A {
2 A(int a) : a_(a) { }
3 int a_;
4 };
5 struct B : virtual A {
6 B(int b) : A(b+1), b_(b) { }
7 int b_;
8 };
9 struct C : virtual A {
10 C(int c) : A(c-1), c_(c) { }
11 int c_;
12 };
13 struct D : B, C {
14 D(int d) : B(d*2), C(d/2), d_(d) { } // UH OH! ERROR!
15 int d;
16 };
struct/class Layout: Virtual Inheritance
(con’t)
Solution: The programmer must explicitly invoke the virtual base
constructor in the class where it is ambiguous:
w03/layout-ege1f.cxx
1 // struct A, B, and C as before
2 struct D : B, C
3 {
4 D(int d) :
5 A(d*2+1), B(d*2), C(d/2), d_(d)
6 {
7 }
8 int d;
9 };

i.e., invoking A’s constructor in D’s initialization list resolves the ambiguity.
NOTE: Note that A’s int is not a value that B’s or C’s constructor would have
set it to!
struct/class Layout: Virtual Inheritance
(con’t)
Ambiguous Member Function Case…
This case is resolved in one of two ways:
• by qualifying the call using the scope resolution operator —done the
same way as qualified data member access, or,
• by overriding the member function in the type where it is
ambiguous and setting the code for that function to properly invoke the
correct base/bases member function(s).
struct/class Layout: In General

C++ permits all combinations of virtual and non-


virtual member functions and all combinations of
virtual and non-virtual inheritance, if any,
regardless of access permissions (i.e., public,
protected, or private) when writing structs or
classes.
• NOTE: virtual private member functions are
possible and they can be overridden in the
derived type since they occupy a slot!
struct/class Layout: In General (con’t)

When virtual inheritance and virtual member functions are


used together, this further complicates the memory layout of the
type resulting in layouts that combine the techniques
previously discussed.
To avoid wasting memory in each object instance, C++
compilers typically will store all/most type information (e.g.,
virtual base offsets, virtual member function tables, and any other
type-related information) via a “ vtableptr” discussed earlier.
struct/class Layout: In General (con’t)

If C++ compilers can determine that a specific virtual function or data member
in some virtual base will always be directly called, then it will emit code to
directly call/access it.
• When writing OOP polymorphic code, don’t assume that these cases will
occur.

Advice: If you are not writing OOP (i.e., run-time) polymorphic code,
then seriously ask yourself why you are using virtual in your code.
struct/class Layout: Overhead

Using virtual inheritance requires at least one additional indirection


relative to non-virtual inheritance to access any member accessible via the
virtual base type.

One (1) additional indirection, i.e., the indirect function call itself, is needed if the
offset is not stored in the vtable.
• Pro: Fast since member offset pointer is extremely likely to be on the same L1
cache page as the instance.
• Con: RAM to store the offsets in each instance plus the constructor overhead
to set such in each instance.
struct/class Layout: Overhead (con’t)
Two (2) additional indirections are needed if the offset is stored in the vtable.
• Pro: Only one vtable pointer in each instance is required: constructors
need only set one pointer.
• Con: Extra cache page and indirection.
• The first indirection is to access the vtable. The vtable might not be
close to the instance data and might be on another cache page.
(The required offset data is likely to be located on the cache page.)
• The last indirection to access the member is also very fast since it
is a relative address to the instance itself —which hopefully is
already be in the cache.
struct/class Layout: Overhead (con’t)
Using a virtual member function requires at least one additional
indirection
relative to a non-virtual function call:
One (1) indirection is needed to call the function if the function pointer is directly
stored in the object.
• Pro: Fast since function pointer is extremely likely to be on the same L1 cache
page as the instance.
• Con: RAM to store the list of function pointers in each instance plus the
constructor overhead to set such in each instance.

NOTE: Each indirect function pointer call is slower than a direct call.
struct/class Layout: Overhead (con’t)
Two (2) indirections are needed to call the function if the function pointer is stored in
the vtable.
• Pro: Only one vtable pointer in each instance is required: constructors need only
set one pointer.
• Con: An extra cache page and indirection:
• The vtable is not likely to be close to the instance data and will need to use an
additional cache page.
• All slots are extremely likely to be located on the vtable cache page, so the slot
(array) indirection will be negligible (only) for direct virtual function calls since
the compiler already knows the slot offset at compile time. (If indirect then add
another indirection.)
• The last indirection is the indirect function call itself.
struct/class Layout: Overhead (con’t)
Accessing a virtual member function through a virtual member base can
require three to four indirections and likely at least an additional cache
page access (for the vtable).
• Is your code also using pointers to everything as well? Hmmmm…
• There are some cases where the compiler can apply some optimizations —but,
in general, this overhead is very real.

Indirections resulting in cache misses are costly.


• Try to design your algorithms to access the one type of object at a time or in
small groups of the same types to increase cache hits.

Advice: Only use virtual to exploit true object-oriented, i.e., run-


time, polymorphism.
Other Object-Oriented Languages
C++ is “special”: Traditional and other object-oriented languages do not
support
non-virtual inheritance and non-virtual member functions.
When converting code from those languages to C++, do the following
before optimizing the code:
• make all inheritance virtual and public,
• make all abstract functions pure virtual, and,
• make all non-static member functions virtual,
• i.e., all static member functions remain static
• i̇.e., make all object methods virtual
• i.e., keep all static variables/methods static
• i.e., keep all non-static variables/methods non-static
• represent all uses of variables that are implicitly references as
references, or a suitable owning smart pointer type, e.g.,
std::shared_ptr or std::unique_ptr.
When To Use And Not To Use Inheritance

public inheritance should only be used to model Is-A or


Is-Completely-Substitutable-For-A relationships for types.
Inheritance use needs to follow the Liskov Substitutability Principle
(LSP):

Liskov Substitutability Principle [2]

If for each object o1 of type S there is an object o2 of type T such that for
all programs P defined in terms of T, the behaviour of P is unchanged
when o1 is substituted for o2, then S is a subtype of T.
When To Use And Not To Use Inheritance
(con’t)
Don’t use public inheritance when: [4, 5]
• non-public inheritance is sufficient
• membership, i.e., Has-A, is sufficient
• declaring as a member variable; encapsulation is nearly always sufficient
• the relationship is Is-Almost-A, e.g.,
• A Square cannot, in practice, inherit from a Rectangle since a
Rectangle’s equivalent of set_width() and set_height() members will
have adverse unacceptable side effects when applied to a Square.
• i.e., Square fails the LSP with respect to Rectangle.
• the relationship is Is-Implemented-In-Terms-Of since such is really a
Has-A
relationship.
When To Use And Not To Use Inheritance
(con’t)
Only use private inheritance to model Has-A relationships if and only if:
• one needs access to protected base class members, and/or,
• one needs to override a virtual function;

Otherwise use a member variable to model the Has-A relationship. [4, 5]


• Private inheritance demotes all public and protected base members to
private access.
When To Use And Not To Use Inheritance (con’t)

Only use protected inheritance to model Is-A relationships for derived


types only and no access to everything else in the base classes.
In practice, there are few use cases where protected inheritance is useful.
• Protected inheritance demotes all public base members to protected
access.
this

Given a user-defined struct, class, or union type, T, then in any constructor or


non-static member function or in the destructor of T a constant pointer to the
current instance of T, called this, is defined.
Internally this is passed to the constructor, destructor, or non-static member
function as an implicit argument.
In most cases, this-> can be omitted in front of member names inside constructors,
destructors, and non-static members.
• You will need to use this-> if the member is in a base class that is dependent on a
template parameter.
this (con’t)
Member functions can be adorned with cv-qualifiers, i.e., const, volatile, and
const volatile, and/or with an Lvalue or Rvalue reference decorations.
• Such adornments adorn the type T that this points to within that
member function and therefore affect member access.
w03/layout-egf1a.cxx
1 struct A {
2 void setValue(int); // *this is "A"
3 int getValue() const; // *this is "A const"
4 int getValue() volatile; // *this is "A volatile"
5 int getValue() const volatile;
6 // *this is "A const volatile"
7 std::string getString() const&; // *this is "A const&"
8 std::string getString() &&; // *this is "A&&"
9 // etc.
10 };
Abstract Classes [1, [class.abstract]]
C++ has no special syntax to denote an abstract class.
An abstract class is a class that has at least one pure virtual function
within it or via inheritance.
• A pure virtual is called an abstract function in other object-oriented
languages.

An abstract class can inherit from a class that is not abstract.


By value instances of abstract classes cannot be created —
however references/pointers to abstract classes are allowed.
A base type’s pure virtual function that has been overridden in a non-
pure manner is no longer pure.
Pure virtual Functions

• C++ permits abstract member functions to be declared. These are called


pure virtual functions and are written syntactically by prefixing the
prototype with virtual and suffixing it with = 0:
• w03/layout-egg1a.cxx
1 class abstract
2 {
3 // Always have a virtual destructor in an
4 // abstract/polymorphic class...
5 virtual ~abstract() { }
6
7 // Pure virtual (abstract) function...
8 virtual int move(int x, int y) = 0;
9 };
Pure virtual Functions (con’t)

C++ permits pure virtual functions to be defined —although the function and
type are still considered pure and the function must be explicitly invoked in a
derived type if it is to be executed…
Pure virtual Functions (con’t)
w03/layout-egg1b.cxx
1 class position
2 { // abstract class due to pure virtual functions
3 public:
4 virtual ~position() { } // ensure proper destruction
5 virtual int getX() const = 0;
6 virtual int getY() const = 0;
7 virtual void setX(int x) = 0;
8 virtual void setY(int y) = 0;
9 virtual void move(int dx, int dy) = 0;
10 };
11
12 void position::move(int dx, int dy) {
13 setX(getX()+dx); setY(getY()+dy);
14 }
Pure virtual Functions (con’t)
w03/layout-egg1c.cxx
1 // euclidean does not override all pure virtual
2 // functions and so is still an abstract class...
3 class euclidean : public position
4 {
5 public:
6 void move(int dx, int dy) override
7 {
8 position::move(dx, dy);
9 }
10 };

NOTE: The above example is a bit contrived. It is also very rare to write definitions
for pure virtual functions. It works since the pure virtual requires a slot and the
definition causes the function pointer to be set on that slot.
struct and class There are no differences between
struct or class in C++

except for their scope access default.


By default:
• structs inherit publicly and members are declared publicly, and,
• classes inherit privately and members are declared privately.

Rule of thumb: Use structs as one would in C and classes for OOP classes
and virtually all other user-defined objects.
Member Initialization Lists
All versions of C++ have member initialization lists.
C++ member initialization lists are used in constructors to invoke base
class constructors and to construct member variables.
If any base classes or members are omitted from the list, then they are implicitly
default constructed.
The member initialization list appears starting with a colon, “:”,
immediately after the close parenthesis, “)”, ending the constructor
argument list.
It ends with the opening brace, “{”, of the constructor definition’s body.
The allowed syntax is that of declaring comma-separated unnamed
variables to invoke constructors or and using the member name instead of a
type to invoke members’ constructors.
Member Initialization Lists (con’t)
w03/mem-init-list1.cxx
1 struct base {
2 base() : b_(12) { } // Construct b_ with 12.
3 int b_;
4 };
5 struct derived {
6 derived() :
7 base{}, d_(231) // Default construct base; d_ = 231
8 { }
9 int d_;
10 };
11 void func(derived const&) { }
12 derived d; // Default constructed
13 derived d2{}; // Default constructed
14 func(derived{}); // Unnamed & def. constructed.
15 // Next line is the "most vexing parse"...
16 derived d3(); // This is a function pointer declaration.

COMP-3400-30-R-2024W Week 3: Object-Oriented Programming


Member Initialization Lists (con’t)

A reason member initialization lists are needed is because once the open
curly brace of the constructor code block is encountered all base classes and
all members have been successfully constructed. Thus, the list
allows the programmer to control the construction process of base classes and
members.
Your code’s ordering of construction should be as follows:
• base classes then
• member variables (in order of declared appearance in the class)
std::initializer_list

std::initializer_list is most commonly used with container and class types


—but it can also be used with functions. [1, [support.initlist]]
It was add to C++11 to make it possible to uniformly initialize all types and
to support a form of the C-style brace-list initialization syntax.
The std::initializer_list type is a container object with iterators defined in
<initializer_list> and it allows the programmer to iterate through a list of
compile-time specified objects as a result of using the uniform initialization
brace syntax.
std::initializer_list (con’t)

The list can be empty, or, it can be filled with literals/variables provided they are all of
the same type. No implicit conversions or narrowings will occur. [1, [dcl.init.list]]
NOTE: If a type has a single-argument constructor with an std::initializer_list
defined, then the std::initializer_list constructor will be preferred if the uniform
initialization brace syntax is used to construct the type.
std::initializer_list (con’t)
w03/init-list1.cxx
1 #include <initializer_list>
2 #include <iostream>
3 #include <vector>
4
5 // Invokes std::vector<int>(std::initializer_list<int>)...
6 std::vector<int> v{ 1, 5, 23, -123, 45, 34 56 };
7 template <typename T>
8 T sum(std::initializer_list<T> l)
9 {
10 T retval{};
11 for (auto const& element : l)
12 retval += element;
13 return retval;
14 }
15 // ...
16 std::cout << sum({1, 2, 3}) << '\n'; // Outputs: 6
std::initializer_list (con’t)
w03/init-list2.cxx
1 #include <initializer_list>
2 #include <list>
3
4 struct my_list
5 {
6 my_list(std::initializer_list<int> l) :
7 l_(l.begin(), l.end())
8 // i.e., Pass iterators to list<int> constructor
9 {
10 }
11
12 list<int> l_;
13 };
14
15 // Construct my_list with some ints...
16 my_list wow{ 1, -53, 3, 123, 45, 234 };
Resource Acquisition Is Initialization (RAII)
Resource Acquisition Is Initialization is an extremely important
design pattern in C++. It involves writing objects to acquire and
release resources as follows:
• In the constructor(s), obtain the required resources.
• In the destructor, always free all obtained resources over the lifetime of the
object.
• It is usually preferable to declare objects by value on the call stack to
ensure the destructor is automatically called.

TIP: Instead of manually dynamically allocating memory and freeing it later,


write a class to manage that in the constructor and the destructor.
• Even easier: this is what std::unique_ptr and std::shared_ptr are for!
Resource Acquisition Is Initialization (RAII)
(con’t)
In C++ when objects are declared by value on the call stack their
destructors are always called:
• when they go out-of-scope normally, and,
• when they go out-of-scope when the stack is being unwound from a thrown
exception.
Thus cleaning up resources, even in the presence of an exception, is made
simple by this technique.
• Look at earlier “destructor” slides’ code example(s). Note the constructor,
destructor, and throw.
This technique is used extensively in C++ libraries. You should use
it in your code as well.
References
1 ISO/IEC. Information technology – Programming languages – C++. ISO/IEC
14882-2020, IDT. Geneva, Switzerland, 2020.
2 Barbara Liskov. “Data Abstraction and Hierarchy”. In: SIGPLAN Notices 23 (5
May 1988).
3 Bjarne Stroustrup. Design and Evolution of C++. Boston, MA: Addison-Wesley,
1994. ISBN: 978-0-201-54330-8.
4 Herb Sutter. Exceptional C++. Boston, MA: Addison-Wesley, 2000. ISBN:
978-0-201-61562-3.
5 Herb Sutter. More Exceptional C++. Boston, MA: Addison-Wesley, 2002. ISBN:
978-0-201-70434-1.
6 Slides from Paul P.

You might also like