CSS 343: Notes from Lecture 15 (DRAFT)
Administrivia
Resource Management: RAII
-
programs manage resources, which must be allocated before use
and released after use (otherwise you get a resource
leak or a dangling pointer)
-
memory
-
file descriptors
-
mutexes
-
some languages try to automate memory management
-
Java: garbage collection
-
Python: reference counting
other resources still require manual management
-
C++ requires explicit (manual) memory management
-
smart pointers: classes that mimic pointer behavior but
provide additional behavior
-
iterators
-
std::shared_ptr
Constructors, Destructors, & RAII
-
object construction & destruction:
-
object construction: members (including the hidden base
class) are constructed in declaration order, then its
constructor is called
-
object destruction: destructor is called, followed
by the destructors of each member in reverse order of
costruction
-
heap-allocated objects (via
new)
must be explicitly destructed by calling
delete
on the pointer
-
local variables are constructed when control reaches their
variable definition and destructed when control leaves the
scope where they are defined:
#include <iostream>
using namespace std;
class Name {
public:
Name(const string& value) : value_(value) {
cout << " constructing Name " << value_ << endl;
}
~Name() {cout << " destructing Name " << value_ << endl;}
const string& str() {return value_;}
private:
string value_;
};
class Weapon {
public:
Weapon(const string& artifact) : artifact_(artifact) {
cout << " constructing Weapon " << artifact_ << endl;
}
~Weapon() {cout << " destructing Weapon " << artifact_ << endl;}
private:
string artifact_;
};
class Player {
public:
Player(const string& weapon) : weapon_(weapon) {cout << " constructing player" << endl;}
~Player() {cout << " destructing player" << endl;}
private:
Weapon weapon_;
};
class Hobbit : public Player {
public:
Hobbit(const string& name, const string& weapon)
: Player(weapon), name_(name) {
cout << " constructing Hobbit " << name_.str() << endl;}
~Hobbit() {cout << " destructing Hobbit " << name_.str() << endl;}
private:
Name name_;
};
int main() {
cout << "enter main scope" << endl;
Hobbit frodo("Frodo Baggins", "Sting");
cout << "entering inner scope" << endl;
{
cout << "entered inner scope" << endl;
Hobbit bilbo("Bilbo Baggins", "");
cout << "leaving inner scope" << endl;
}
cout << "left inner scope" << endl;
cout << "leaving main scope" << endl;
return 0;
}
enter main scope
constructing Weapon Sting
constructing player
constructing Name Frodo Baggins
constructing Hobbit Frodo Baggins
entering inner scope
entered inner scope
constructing Weapon
constructing player
constructing Name Bilbo Baggins
constructing Hobbit Bilbo Baggins
leaving inner scope
destructing Hobbit Bilbo Baggins
destructing Name Bilbo Baggins
destructing player
destructing Weapon
left inner scope
leaving main scope
destructing Hobbit Frodo Baggins
destructing Name Frodo Baggins
destructing player
destructing Weapon Sting
-
RAII
(resource acquisition is initialization):
create object that allocates the resource in the constructor and
releases the resource in the destructor
-
the system automatically invokes the destructor when control
leaves the scope of the object
Inheritance & Polymorphism
-
polymorphism: behavior is determined by the type of the data
rather than the type of the variable
-
variable of one
type
holds data of a different
type
-
In C++ this is restricted to pointers (and references) that
point to objects of classes that are
derived
from the type that the pointer points to
-
in C++, polymorphic behavior is controlled via
virtual methods
-
other languages decouple type hierarchy (inheritance) and
polymorphic behavior (e.g. duck typing)
-
C++ (and Java) language design decisions based on providing
compile-time type checking, which catches errors
-
inheritance: a subtype (or subclass) is
derived
from a parent class (super-type or base class)
-
a derived type has all the behavior of the base type except
what is overridden or added
-
in C++, when you override a non-virtual method and cast
the pointer the base type, the base method is called;
when you override a virtual method and cast the pointer
to the base type, the derived method is called
-
the original ideas were to factor out commonality and allow
ease-of-extension
Yo-Yo Problem
Problem: programmer bouncing up and down a long, complicated
inheritance graph.
Best practices:
-
keep inheritance graph as shallow as possible
-
prefer composition
(has-a)
over inheritance
(is-a)
Liskov Substitution Principle
The basic concept of inheritance is that the subtype receives all
the properties of the base type. Old-school inheritance model is
that the subtype inherits the implementation. Modern use of
inheritance is that the subtype is a kind of the base type ("is-a"
or "isa").
Inheritance should never be used when the sublcass restricts the
freedom implicit in the base class; it should only be used when
the subclass adds extra details to the concept represented by the
base class.
The Liskov substitution principle codifies this concept: if S is a
subtype of T then objects of type T may be replaced by objects of
type S. This is also known as "strong behavioral subtyping".
Classes should inherit the interface (abstract base classes)
rather than the implementation.
Use composition
(Has-A)
and
delegation
to implement implementation inheritance.
For example, a priority queue is
not
a kind of
vector,
even though the binary heap is implemented
using
a vector.
Circle-Ellipse Problem
-
an ellpse is a figure with two focal points
-
the excentricity of the ellipse is a measure of the distance
between the foci
-
as the focal points move closer together, the ellipse
becomes more circular
-
a circle is an ellipse with the two focal points equal
-
if the ellipse class has a method
stretch_x()
which increases the eccentricity, it cannot be applied to the
circle
-
therefore a
Circle
class cannot be a subclass of an
Ellipse
class
-
also known as the square-rectangle problem: a square is a kind
of rectangle
There are a variety of potential solutions to the problem:
-
have
mutator
method return a success or failure value (or raise an exception
on failure)
-
make objects immutable; mutator returns new object
-
allow weaker contract on ellipse (e.g.
stretch_x()
is permitted to change the y axis.
Principles of Object-Oriented Design: S.O.L.I.D
-
Single Responsibility: every class should be responsible for one
thing only (the class should be cohesive, i.e. the pieces stick together)
-
Open-Closed principle:
-
class should be open for extension, closed for modification
-
extreme view: once completed, the implementation of a class
should only be changed for bug fixes
-
for new/changed features, create a new class
-
Liskov substition: as discussed
-
Interface segregation:
-
no client should depend on modules it does not use
-
interfaces are split so clients can use subsets (decoupling)
-
e.g.
graph_maker
only requires
Graph::add_vertex
and
Graph::add_edge
methods, but not
find_minimum_path
-
dependency inversion:
-
high-level modules shold not depend on low-level modules:
both should depend on abstractions
-
abstractions should not depend on details; details depend on
abstractions
-
convertional architecture: high-level design is
decomposed
into lower-level components
-
decoupling allows low-level components to be reused and
high-level components to use different low-level components
-
example: mock objects used for testing (no need for the
overhead of a database connection if you use a fake database
connection with desired behavior--for testing)
Object-Oriented Patterns
See the Design Patterns book (also known as the Gang of Four book:
http://en.wikipedia.org/wiki/Design_Patterns.
Not to be confused with this gang of four:
http://en.wikipedia.org/wiki/Gang_of_Four