Professor: Brad Lushman | Term: Fall 2023
Lecture 1
Intro to C++
In this course, we discuss the paradigm of object-oriented programming from 3 perspectives:
-
The programmer’s perspective - how to structure programs correctly, and how to lower the risk of bugs
-
The compiler’s perspective - what do our constructions actually mean, and what must the compiler do to support them?
-
The designer’s perspective - how can we use the tools that OOP provides to build systems? Basic SE
Assume knowledge of C from CS136
C:
C++
Notes:
-
main MUST return int - void main() illegal
-
return stmt - returns the status code to OS
- can be omitted from main (0 is assumed)
-
stdio, printf still available in C++
-
preferred I/O header <iostream>
- std::cout
-
std::endl = end of line & flush output buffer
-
using namespace std - lets your omit the std:: prefix
To compile use: g++20h iostream (creates gcm.cache directory)
Then can compile program with: g++20m file name
./a.out will display the program
Input/Output
3 I/O streams
-
cout/cerr — for printing to stdout/stderr
-
cin — reading from stdin
I/O operators
-
“put to” (output)
-
“get from” (input)
-
cerr x (produce value of x to output, from x to screen)
-
cin x (grab value of x from input, from screen to x)
E.g. add 2 numbers
In terminal: g++20m plus.cc -o plus, ./plus, (enter two numbers)
By default, cin skips leading whitespace (space/tab/newline)
What if bad things hapen, eg
-
input doesn’t contain an integer next
-
input too large/small to fit in a variable
-
out of input (EOF)
Statement fails
If the read failed: cin.fail() will be true
If EOF: cin.fail(), cin.eof() both true
But not until the attempted read fails!
Ex - read all ints from stdin & echo them, one per line, to stdout. Stop on bad input or eof
Note:
-
There is an implicit conversion from cin’s type (istream) to bool
-
lets you use cin as a condition
-
cin converts to true, unless the stream has had a failure
Note:
-
is C’s (and C++‘s) right bitshift operator
-
If a & b are ints, ab shifts a’s bits to right by b spots
-
e.g 21 3 21 = 10101 21 3 = 2
-
But when the LHS is an istream (i.e cin), >> is the “get from” operator
-
First example of overloading (same function has multiple implementations)
Recall:
-
21 3 - bitshift
-
cin x - input
First example of overloading (same function/operator with multiple implementations & the compiler chooses the correct implementation (at compile time)), based on the types of arguments.
Lecture 2
Operator
-
inputs: cin (istream): placeholder for data (several possible types)
-
output?: returns cin (istream)
This is why we can write cin x y z
Stepper (goes from left to right), where cin carries through the left (cin>>x = cin, cin>>y etc.)
Successful read, cin evaluates as true, evaluates as false if otherwise.
Final Example
Ex: Read all ints & echo on stdout until EOF and skip non-integer input.
The stream will not function after failure until you do this (clear).
What if we want to print a # in hexadecimal?
Hex is an I/O manipulator — puts stream into “hex mode” All subsequent ints printed in hex.
To go back to decimal mode: cout dec;
Note that manipulators like hex and dec set flags in the standard stream variables cout, etc. These are effectively global variables. I.e. changes you make to these flags affect the whole program
Good Practice: If you change a stream’s settings, change them back when you are done
Strings
In C: array & char (char* or char[]) terminated by \0.
-
Must explicitly manage memory — allocate more memory as strings get longer.
-
Easy to overwrite the \0 & corrupt memory.
C++ strings: import <string>, type std::string
-
Grow as needed (no need to manage the memory)
-
Safer to manipulate
e.g
”Hello” — C style string (character array [“H”,“e”,“l”,“l”,o”,\0]
s — C++ string created from the C string on initialization
String Operations
Equality/Inequality:
Comparisons: etc. (lexicographic)
Length: s.length() (it is )
Individual Characters: s[0], s[1], s[2], etc.
Concat:
Skips leading white space & stops at white space (i.e. read a word). i.e. If given “hello world” to the above program, only returns “hello”.
What if we want the white space? getline(cin,s)
- reads from the current position to the next new line into s
Streams are an abstraction — they wrap an interface of “getting and putting items” around the keyboard and screen.
Are there other kinds of “things” that could support this same “getting & putting” interface?
-
File
-
Read/write to/from a file instead of stdin/stdout
-
std::ifstream — a file stream for reading
-
std::ofstream — a file stream for writing
-
File access in C:
C++
f: Declaring & initializing the ifstream var opens the file.
Important The file (file.txt) is closed when f goes out of scope.
Anything you can do with cin/cout, you can do with ifstream/ofstream.
Lecture 3
Recall: Other applications of the stream abstraction
-
Files
-
Strings
- Extract data from chars in a string
std::istringstream
- Send data to a string as chars:
std::ostringstream
- Extract data from chars in a string
Convert string to #:
Example Revisited: Echo all #‘s, skip non-#s
This program picks up 123abc (as 123), but def456 fails to read any number.
Application: Consider processing the command line.
To accept command line args in C or C++, always give main the following params:
eg.
Note: The args are stored as C-style strings. Recommendation: Convert to C++ strings for processing
eg.
eg: Print the sum of all numeric args on the cmd line
Default Function Parameters
Note: Optional parameters must be last.
Also note: The missing parameter is supplied by the caller, not by the function
Why? The caller passes parameters by pushing them on the stack. The function fetches parameters by reading them off the stack. If a parameter is missing, the function as no way of knowing that. Would interpret whatever is in that part of the stack as the argument.
So instead, the caller must supply the extra parameter if it is missing. when you write printWordsInFile(); The compiler replaces this with printWordsInFile(“words.txt”)
For this reason, default arguments are part of a function’s interface, rather than its implementation.
defaults go in the interface file, not the implementation file.
Overloading
C:
C++: Functions with different parameter lists can share the same name
Correct version of neg, for each function call, is chosen by the compiler (i.e at compile-time) based on the # and type of arguments in the function call.
,
overloads must differ in #/types of arguments — just differing in the return type is not enough.
We’ve seen this already: (ints/strings) (shift/I/O)
Structures
Constants
Declare as many things constant as you can — helps catch errors
Lecture 4
Parameter Passing
Recall
Pass-by-value — inc gets a copy of x, mutates the copy. Thus the original is unchanged
Solution: If a function needs to mutate an argument, you pass a pointer.
X’s address is being passed by value. Increment changes the data address. Now visible to the caller.
Q:
Why cin x and not cin &x?
A: C++ has another pointer-like type, reference.
References (Important)
References are like constant pointers with automatic dereferencing.
y [10]
z [pointer to y (arrow to 10)]
z [] can’t change. Once pointing to y, it can’t change.
If the const was on the other side of the =, then y would be constant.
z = 12; ([NOT *z=12)
z = 12 changes the value associated with y (y[10] y[12]) now y == 12
int *p = &z;
Address of z gives the address of y.
In all cases, z acts exactly like y. Whatever changes happen to z are going to happen to y.
Z is an alias (“another name”) for y.
Q: How can we tell when & means “reference” and when it means “address of”.
A:
Whenever & occurs as part of a type (eg int &z). It always means reference.
When & occurs in an expression, it means “address of”. (or bitwise -and).
lvalue, rvalue x = y; Interested in the value of y, and the address/location of x (not it’s value).
Things you [can’t do with lvalue refs:
-
Leave them uninitialized e.g. int &x;
-
Must be initialized with something that has an address (an lvalue).
-
int &x = 3; is illegal. (3 doesn’t have a location)
-
int &x = y + z; is illegal (value of y + z has no location)
-
int &x = y; is legal
-
-
-
Create a pointer to a reference
-
int &*p; (start at the variable work backwards)
- int *&p = ; is legal
-
-
Create a reference to a reference
-
int &&r = ; is illegal
- denotes something different (later)
-
-
Create an array of references
- int &r[3] = {, , ,}; is illegal
What can you do?
- pass as function parameters
Why does cin x work? Takes x as a reference.
Q: Why is the stream being taken & returned as a reference? And what does returning by reference mean?
A: We need a better understanding of pass-by-value
Pass-by-Value
Pass-by-value, e.g. int f (int n) { … } copies the argument.
If the parameter is big, the copy is expensive
But what if a function does want to make changes to rb locally, but doesn’t want these changes to be visible to the caller?
Then the function must make a copy of rb.
But if you have to make a copy anyway, it is better to just use pass-by-value & have the compiler make it for you. It might be able to optimize something.
Advice: Prefer pass-by-const-ref over pass-by-value for anything larger than a pointer. Unless the function needs to make a copy anyway, then use pass-by-value.
Also:
So in the case of
the istream is being passed (and returned) by reference to save copying.
This is important because stream variables are not allowed to be copied. Dynamic Memory Allocation
C:
Instead: new/delete. Type-aware, less error prone.
Stack includes np Heap includes Node np points to Node.
Lecture 5
Recall:
-
All local variables reside on the stack.
- Variables are deallocated when they go out of scope (stack is popped)
-
Allocated memory resides in the heap
- Remains allocated until delete is called
-
If you don’t delete all allocated memory — memory leak
- Program will eventually fail — we regard this as an incorrect program
Arrays
Memory allocated with new must be deallocated with delete.
Memory allocated with new […] must be deallocated with delete [].
Missing these Undefined Behaviour (UB)
Returning by Value/Ptr/Ref
Q: Why was it OK for operator to return an istream reference
A: Because the reference is not to a local variable. The returned reference is the same reference that was passed as the parameter “in”, so it refers to something accessible to the caller.
Which should you pick? Return by value. Often not as expensive as it looks (we will see why later).
Operator Overloading
Can give meanings to C++ operators for types we create
E.g.
Special Case: Overloading and
Separate Compilation
Split programs into modules, which each provide
-
interface — type definitions, function headers
-
implementation — full definition for every provided function
Recall: declaration — asserts existence definition — full details — allocates space (variables / functions)
E.g: Interface (vec.cc)
main.cc
Implementation: vec-impl.cc (see line 9 in interface block)
Recall: An entity can be declared many times, but defined only once.
Interface files: start with export module \....;
Implementation files: start with module \....;
Compiling separately: g++ -c …cc
Above says, compile only, do not link, do not build exec.
Produces an object file (.o)
g++20m -c vec.cc
g++20m -c vec-impl.cc
g++20m -c main.cc
g++20m vec.o vec.-impl.o main.o -o main
./main Must be in dependency order
Dependency order: interface must be compiled before implementation, client.
Build tool support for compiling in dependency order (e.g. make) is still a work in progress.
Lecture 6
Classes
Can put functions inside of structs.
e.g: student.cc
student-impl.cc
client
A class is essentially a struct type that can contain function. C++ does have a specific class keyword (later).
An object is an instance of a class.
The function grade is a member function or (method)
:: is called the scope resolution operator.
C::f means f in the context of class C.
:: like . where LHS is a class (or namespace), not an obj.
What do assns, mt, final mean inside of Student::grade?
-
They are fields of the receiver objects — the object upon which the method was called
-
e.g
s.grade()
is a method call that uses s’s assns, int, final
Formally: methods take a hidden extra parameter called this — pointer to the receiver object.
e.g s.grade(); within grade(),
Can write
(Methods can be written in the class. We will often do this for brevity. You should put impls in a separate file).
Initialization Objects
Better — write a method that initializes a constructor (ctor)
If a constructor has been defined, these are passed as arguments to the constructor. If no constructor has been defined, it is C-style field-by-field initialization. C-style is only available if you have not written a constructor.
Alternative syntax: Student s = Student{60,70,80};
Looks like construction of an anonymous student obj (i.e Student{60,70,80}
) which is then copied to initialize s.
But it is not — semantically identical to Student s {60,70,80};
More on this later
Advantages of ctors: they’re functions!
-
Can write arbitrarily complex initialization code
-
Default parameters, overloading, sanity checks
e.g
It may look like Student newKid; calls a ctor, and Student newKid; does not. That is not correct. Whenever an object is created, a ctor is always called.
Q: What if you didn’t write one? e.g Vec v;
A: Every class comes with a default (i.e zero-arg) ctor (which just default-constructs all fields that are objects).
e.g
But the built-in default constructor goes away if you write any constructor.
e.g
Continue with this definition for now.
Now consider:
The built-in default ctor for Basis wants to default-construct all fields that are objects. v1, v2 are objects, but they have no default ctor. So basis cannot have a built-in default ctor.
Could we write our own?
This also does not work. Why? Too late. The body of the ctor can contain arbitrary code, so the fields of the class are expected to be constructed and ready to use before the constructor body runs.
Object Creation Steps
When an object is created: 3 steps
-
Space is allocated
-
Fields are constructed in declaration order (i.e ctors run for fields that are objects)
-
Ctor body runs
Initialization (i.e. construction) of v1, v2 must happen in step 2, not step 3. How can we accomplish this?
Member Initialization List (MIL)
Lecture 7
Recall: Member Initialization List (MIL)
Note: can initialize any field this way, not just object fields.
Default values for the MIL
Note:
Fields are initialized in the order in which they were declared in the class, even if the MIL orders them differently.
MIL: sometimes more efficient than setting fields in a constructor body
Consider:
Name default-constructed (to the empty string) in step 2 and reassigned it.
Versus
Name is initialized to the correct value from the beginning in step 2, and there is no reassignment in step 3.
More efficient.
MIL must be used:
-
For fields that are objects with no default ctor
-
For fields that are constant or references
MIL should be used as much as possible. Embrace MIL.
Recall once again:
How does this initialization happen?
-
The copy constructor
-
For constructing one object as a copy of another
Note: Every class comes with
-
default ctor (default-constructs all fields that are objects)
- lost if you write any ctor
-
copy ctor (just copies all fields)
-
copy assignment operator
-
destructor
-
move ctor
-
move assignment operator
Building your own copy ctor:
When is the built-in copy ctor not correct?
Consider:
p points to 1 on the heap which points to 2 also on the heap
Simple copy of fields only the first node is actually copied. (shallow copy).
If you want a deep copy (copies the whole list), must write your own copy ctor:
The copy ctor is called:
-
When an object is initialized by another object of the same type
-
When an object is passed by value
-
When an object is returned by value
The truth is more nuanced, as we will see.
Q: Why is this wrong:
A: Taking ‘other’ by value means ‘other’ is being copied, so the copy ctor must be called before we can begin executing the copy ctor ( recursion).
Note: Careful with ctors that can take one argument:
Single-arg ctors create implicit conversions:
E.g Node n4;
but also Node n = 4;
implicit conversion from int to Node.
Seen this before with: string s "Hello";
Implicit conversion through
single-arg ctor.
What’s the problem?
Danger
-
Accidentally passing an int to a function expecting a Node.
-
Silent conversion
-
Compiler does not signal an error
-
Potential errors not caught
Don’t do things that limit the compiler’s ability to help you! Disable the implicit conversion:
Destructors
When an object is destroyed (stack-allocated: goes out of scope, heap-allocated: is deleted) a method called the destructor (dtor) runs.
Lecture 8
Destructors
When an object is destroyed (stack-allocated: goes out of scope, heap-allocated: is deleted),
Method called destructor runs (dtor).
Classes come with a dtor (just calls dtors for all fields that are objects).
When an object is destroyed:
-
Dtor body runs
-
Fields’ dtors are invoked in reverse delaration order (for fields that are objects)
-
Space is deallocated
When do we need to write a dtor?
If np goes out of scope
-
The pointer (np) is reclaimed (stack-allocated).
-
The list is leaked
If we say delete np; calls *np’s dtor, which doesn’t do anything.
np [] [1][-][2][-][3][/]
1 is freed, 2,3 are leaked
Write a dtor to ensure the whole list is freed:
Now — delete np; frees the whole list
What happens when you reach the null pointer at the end of the list?
Deleting a null pointer is guaranteed to be safe (and to do nothing). The recursion stops.
Copy Assignment Operator
May need to write your own.
Why?
When writing operator=, ALWAYS make sure it behaves well in the case of self-assignment.
Q: How big of a deal is self-assignment? How likely am I to write ?
A: Not that likely. But consider if point at the same location.
Or a[i] = a[j] if happen to be equal (say, in a loop). Because of aliasing, it is a big deal!
Q: What’s wrong with if as a check for self-assignment
A: Exercise
An even better implementation.
Alternative: Copy + swap idiom
RValues & RValue References
Recall:
-
An lvalue is anything with an address
-
An l value reference (&) is like a constant ptr with auto-dereferencing. Always initialized to an lvalue
Now consider:
Lecture 9
RValues, RValue Refs
-
Compiler creates temporary object to hold the result of oddsorEvens
-
Other is a reference to this temporary
- Copy ctor deep-copies the data from this temporary
But
-
The temporary is just going to be discarded anyway, as soon as the start
Node n oddsOrEvens();
is done. -
Wasteful to have to copy from the temp
- Why not just steal it instead? — save the cost of a copy
-
Need to be able to tell whether other is a reference to a temporary object (where stealing would work) or a standalone object (where we would have to copy).
C++ - rvalue reference Node && is a reference to a temporary object (rvalue) of type Node.
Version of the ctor that takes a Node &&
Similarly:
Move assignment operator:
If you don’t define move operations, copying versions of them will be used instead.
Copy/Move Elision
Answer: Just the basic ctor. No copy ctor, no move ctor.
In some cases, the compiler is required to skip calling copy/move ctors.
In this example, makeAVec writes its result (0,0) directly into the space occupied by v in the caller, rather than copy it later.
Eg
This happens, even if dropping ctor calls would change the behaviour of your program (e.g. if ctors print something).
You are not expected to know exactly when elision happens — just that it does happen.
In summary: Rule of 5 (Big 5)
If you need to write any one of
-
copy ctor
-
copy assignment operator
-
dtor
-
make ctor
-
move assignment operator
Then you usually need to write all 5.
But note that many classes don’t need any of these. The default implementations are fine.
What characterizes classes that need the big 5, typically? ownership
- these classes are usually tasked with managing something (often memory), but there are other things that need managing (resources).
Notice:
Operator= is a member function. Previous operators we’ve written have been standalone functions.
When an operator is declared as a member function, this plays role of the first operand.
How do we implement k*v? Can’t be a member function — first arg not a Vec! Must be external:
Advice: If you overload arithmetic operators, overload the assignment versions of these as well, and implement, e.g. in terms of +=.
E.g
I/O Operators:
What’s wrong with this? Makes vec the first operand, not the second.
use as v cout | w (v cout)
So define operator , as standalone. Would have to put the stream on the right
Certain operators must be members:
-
operator=
-
operator[]
-
operator
-
operator()
-
operator T (where T is a type)
Lecture 10
Options:
-
Provide a default ctor
- This is not a good idea unless it makes sense for the class to have a default ctor
-
For stack arrays:
Vec moreVecs\[3\] = 0,0,1,1,2,4;
-
For heap arrays — create an array of pointers
Const Objects
Const objects arise often, especially as parameters.
What is a const object?
- Fields cannot be mutated
Can we call methods on a const obj?
Issue: The method may modify fields, violate const.
A: Yes — we can call methods that promise not to modify fields.
Eg:
Compiler checks that const methods don’t modify fields. Only const methods can be called on const objects.
Now consider: want to collect usage stats on Student objs:
-
Now can’t call grade on const students — it can’t be a const method.
-
But mutating numMethodCalls affects only the physical constness of student objects, not the logical constness.
-
Physical constness — whether actual bits that make up the object have changed at all.
-
Logical constness — whether the object should be regarded as different after the update.
Want to be able to update numMethod calls, even if the object is const:
Mutable fields can be changed, even if the object is const. Use mutable to indicate that the field does not contribute to the logical constness of the object.
Static Fields & Methods
numMethodCalls tracked the # of times a method was called on a particular object. What if we want the # of times a method is called over all student objects.
Eg: What if we want to track how many students are created?
Static members associated with the class itself, not with any specific instance (object).
Static member functions — don’t depend on the specific instance (no this parameter). Can only access static fields & other static functions.
Comparing Objects
Recall: string comparison in C.
Linear scan, char-by-char comparison.
Compare to string comparison in C++:
C++ version is easier to read. But one drawback.
Consider:
Two comparisons! Versus with strcmp
Can we achieve the same using C++ strings, i.e have one comparison that answers whether s1 >, =, < s2?
Introducing the 3-way comparison operator <=>
Side note:
Lecture 11
Recall:
How can we support <=>
in our own classes?
Now we can say
But we can also say v1 v2, etc. The 6 relational operators automatically rewritten in terms of <=>
.
Eg v1 v2 (v1 <=>
v2) 0
6 operators for free! But you can also sometimes get operator ⇐> for free!
When might the default behaviour not be correct.
Starship operator for Node
Invariants & Encapsulation
Consider:
What happens when these go out of scope?
m’s dtor tries to delete n, but n is on the stack, not on the heap! UB!
Class Node relies on an assumption for its proper operation: that next is either nullptr or was allocated by new.
This is an example of an invariant. Statement that must hold true, upon which Node relies.
We can’t guarantee this invariant — can’t trust the user to use Node properly.
Eg Stack — invariant — last item pushed is the first item popped. But not if the client can rearrange the underlying data.
Hard to reason about programs if you can’t rely on invariants.
To enforce invariants, we introduce encapsulation.
-
Want clients to treat objects as black boxes — capsules
-
Creates an abstraction — seal away details
- Only interact via provided methods
Eg
In general: want private fields; only methods should be public.
Better to have default visibility = private.
Switch from struct to class.
Difference between struct & class — default visibility. Struct is public, class is private.
Let’s fix our linked list class.
list.cc
list-impl.cc
Only List can create/manipulate Node obs now.
can guarantee the invariant that next is always either nullptr or allocated by new.
Iterator Pattern
-
Now we can’t traverse node to node as we would a linked list.
-
Repeatedly calling ith = time
-
But we can’t expose the nodes or we lose encapsulation.
SE Topic: Design Patterns
- Certain programming challenges arise often. Keep track of good solutions. Reuse & adapt them.
Solution — Iterator Pattern
-
Create a class that manages access to nodes
-
Will be an abstraction of a pointer — walk the list without exposing nodes.
Recall (c):
Lecture 12
Recall: Iterator Pattern
Client code
This now runs in linear time, and we are not exposing the pointers.
Midterm Cutoff Here
Shortcut: range-based for loop
This is available for any class with
-
methods begin & end that produce iterators
-
the iterator must support , unary *, prefix++
If you want to modify list items (or save copying):
Encapsulation (continued)
List client can create iterators directly:
Violates encapsulation because the client should be using begin/end. Don’t want client calling iterators directly.
We could — make Iterator’s constructor private then client can’t call List::Iterator
. But, then neither can list.
Solution
Give List privileged access to Iterator. Make it a friend.
Now List can still create Iterators, but client can only create Iterators by calling begin/end.
Give your classes as few friends as possible — weakens encapsulation.
Providing access to private fields. Accessor / mutator methods.
What about operator
-
needs x, y, but can’t be a member
-
if getX, getY defined — OK
-
if you don’t want to provide getX, getY — make operator a friend f’n.
Friendship does not go both ways.
Equality Revisited
Suppose we want to add a length() method to List: How should we implement it?
Options:
-
Loop through the nodes & count them.
-
Store the length as a field & keep it up to date. length with negligible additional cost to addToFront.
Option 2 is generally preferred.
But consider again the spaceship operator ⇐> in the special case of equality checking:
translates to
What is the cost of <=>
on 2 lists? O(length of shorter list).
But for equality checking, we missed a shortcut: Lists whose lengths are different cannot be equal.
In this case, we could answer “not equal” in time.
Operator <=>
gives automatic impl’s to all 6 additional operators, but if you write operator separately, the compiler will use that for both and != instead of <=>
. Lets you optimize your equality checks, if possible.
System Modelling
Visualize the structure of the system (abstractions & relationships among them) to aid design, implementation, communication.
Popular standard: UML (Unified Modelling Language)
Modelling class
Name | Vec |
---|---|
Fields(optional) | -x: Integer =y: Integer |
Methods (optional) | +getX: Integer +getY: Integer |
Access:
-
- private
-
+ public
Relationship: Composition of Classes
Recall:
Embedding one object (Vec) inside another (Basis) is called composition.
A basis is composed of 2 Vecs. They are part of a Basis, and that is the only purpose of those Vecs.
Relationship: a Basis “owns a” Vec (in fact, it owns 2 of them).
If A “owns a” B, then typically
-
B has no identity outside A (no independent existence).
-
If A is destroyed, B is destroyed.
-
If A is copied, B is copied (deep copy)
Eg A car owns its engine — the engine is part of the car.
-
Destroy the car destroy the engine
-
copy the car copy the engine
Implementation: Usually as composition of classes.
Modelling (see image)
A (diamond arrow) B A owns some # of B’s
Can annotate with multiplicities field names.
Aggregation
Compare car parts in a car (“owns a”) vs. car parts in a catalogue. The catalogue contains the parts, but the parts exist on their own.
”Has a” relationship (aggregation).
If A “has a” B, then typically
-
B exists apart from its association with A
-
If A is destroyed, B lives on
-
If A is copied, B is not (shallow copy) — copies of A share the same B.
Lecture 13
Recall: Aggregation.
eg: parts in a catalogue, ducks in a pond.
UML: [pond][diamond] 0…*[duck]
Typical Implementation: pointer fields
Case Study: Does a pointer field always mean non-ownership?
No! Let’s take a close look at lists & nodes.
Node |
---|
-data: Integer |
A Node owns the Nodes that follow it (Recall: implementation of Big 5 is a good sign of ownership).
But these ownerships are implemented with pointers.
We could view the List object as owning all of the Nodes within it.
What might this suggest about the implementation of Lists & Nodes in this case?
Likely — List is taking responsibility copying and construction/destruction of all Nodes, rather than Node.
Possible iterative (i.e loop-based) management of pointers vs. recursive routines when Nodes managed other Nodes.
Inheritance (Specialization)
Suppose you want to track a collection of Books.
For Textbooks — also a topic:
For comic books, want the name of the hero:
Comic |
---|
-title: string |
-author: string |
-length: integer |
-hero: string |
This is OK — but doesn’t capture the relationships among Book, Text, Comic.
And how do we create an array (or list) that contains a mixture of these?
Could
- Use a union
- Array of void * — pointer to anything
Not good solutions — break the type system.
Rather, observe: Texts and comics are kind of Books — Books with extra features.
To model that in C++ — inheritance
Derived classes inherit fields & methods from the base class.
So Text, Comic, get title, author, length fields.
Any method that can be called on Book can be called on Text, Comic.
Who can see these members?
title, author, length — private in Book — outsiders can’t see them.
Can Text, Comic see them? No — even subclasses can’t see them.
How do we initialize Text? Need title, author, length, topic. First three initialize the Book part.
Wrong for two reasons.
-
Title, etc. are not accessible in Text (and even if they were, the MIL only lets you mention your own fields).
-
Once again, when an object is created:
-
Space is allocated
-
Superclass part is constructed new
-
Fields are constructed in declaration order
-
Constructor body runs
-
So a constructor for Book must run before the fields of Text can be initialized. If Book has no default constructor, a constructor for Book must be invoked explicitly.
Good reasons to keep superclass fields inaccessible to subclasses.
If you want to give subclasses access to certain members: protected access:
Not a good idea to give subclasses unlimited access to fields.
Better: keep fields private, provide protected accessors/mutators
Relationship among Text, Comic, Book — called “is—a”
-
A Text is a Book
-
A Comic is a Book
classDiagram
Book <|-- Text
Book <|-- Comic
Now consider the method isHeavy — when is a Book heavy?
-
for ordinary books — > 200 pages
-
for Texts — > 500 pages
-
for Comics — > 30 pages
Lecture 14
Recall: isHeavy
bool isHeavy() const:
-
for Books: > 200 pages
-
for Texts: > 500 pages
-
for Comics > 30 pages
Now since public inheritance means “is a”, we can do this.
Q: Is b heavy I.e. b.isHeavy() — true or false? Which isHeavy runs? Book::isHeavy or Comic::isHeavy?
A: No b is not heavy, Book::isHeavy runs.
Why? Book b [title, author, length space on stack] = Comic [title, author, length, hero]. Tries to fit a comic object where there is only space for a Book object. What happens? Comic is sliced — hero field chopped off.
- Comic coerced into a Book
So Book b = Comic{…}; creates a Book and Book::isHeavy runs.
Note: slicing takes place even if the two object types are the same size. Having the behaviour of isHeavy depend on whether Book & Comic have the same size would not be good.
When accessing objects through pointers, slicing is unnecessary and doesn’t happen:
Compiler uses the type of the pointer (or ref) to decide which isHeavy to run — does not consider the actual type of the object (uses the declared type).
Behaviour of the object depends on what type of pointer or reference you access it through.
How can we make Comic act like a Comic, even when pointed to by a Book pointer, i.e. How can we get Comic::isHeavy to run?
Declare the method virtual.
Virtual methods — choose which class’ method to run based on the actual type of the object at run time.
E.g. My book collection
Accommodating multiple types under one abstraction: polymorphism. (“many forms”).
Note: this is why a function void f(istream) can be passed an ifstream — ifstream is a subclass of istream.
Danger!: What if we had written Book myBooks[20], and tried to use that polymorphically.
Consider:
What might c be now? (UB).
Never use arrays of objects polymorphically. Use array of pointers.
Destructor Revisited
Is it wrong that Y doesn’t delete x?
No, not wrong:
-
x is private. Can’t do it anyway
-
When an object is destroyed:
-
Destructor body runs
-
Fields are destructed in reverse declaration order
-
Superclass part is destructed (Ỹ implicitly calls X̃)
-
Space is deallocated
-
How can we ensure that deletion through a pointer to the superclass will call the subclass destructor? Make the destructor virtual!
Always — make the destructor virtual in classes that are meant to have subclasses. Even if the virtual destructor doesn’t do anything.
If a class is not meant to have subclasses, declare it final.
Pure Virtual Methods & Abstract Classes
2 kinds of student: regular & co-op.
What should we put for Student::fees?
Not sure — every student should be regular or co-op.
Can explicitly give Student::fees no implementation.
A class with a pure virtual method cannot be instantiated.
Student s; (will not compile) (needs to be either regular or co-op).
Called an abstract class. Its purpose is to organize subclasses.
Lecture 15
Abstract class
Subclasses of an abstract class are also abstract, unless they implement all pure virtual methods.
Non-abstract classes are called concrete.
UML:
-
virtual/pure virtual methods — italics
-
abstract classes — class name in italics
-
# protected : static
Inheritance and Copy/Move
-
It calls Book’s copy ctor
-
Then goes field-by-field (i.e default behaviour) for the Text part
-
Same for the other operations
To write your own operations:
Note: even though ‘other’ points at an rvalue, other itself is an lvalue (so is other.topic).
std::move forces an lvalue x to be treated as an rvalue, so that the “move” versions of the operations run.
Operations above are equivalent to the default — specialize as needed for Nodes, etc.
Now consider:
Partial assignment — copies only the Book part.
How can we prevent this? Try making operator= virtual.
Note: Different return types are fine, but parameter types must be the same, or it’s not an override (and won’t compile). Violates “is a” if they don’t match.
Assignment of a Book object into a Text object would compile:
If operator= is non-virtual — partial assignment through base class pointers.
If virtual — allows mixed assignment — also bad.
Recommendation: all superclasses should be abstract.
Rewrite Book hierarchy
classDiagram
abstract Book <|-- Normal Book
abstract Book <|-- Text
abstract Book <|-- Comic
Other classes — similar. Prevents partial & mixed assignment.
Note: virtual dtor MUST be implemented, even though it is pure virtual.
Because subclass destructors WILL call it as part of Step 3.
Templates
Huge topic — just the highlights
What if you want to store something else? Whole new class?
OR a template — class parameterized by a type.
Lecture 16
Recall:
client
or indeed
Compiler specializes templates at the source code level, and then compiles the specializations.
The Standard Template Library (STL)
Large # of useful templates
Eg dynamic-length arrays: vectors
But also:
If the type argument of a template can be deduced from its initialization, you can leave it out. <int> is deduced here.
To get an array of 4 5’s, we need to use round brackets.
Looping over vectors
Use iterators to remove items from inside a vector: Ex: Remove all 5’s from the vector v.
Attempt #1:
Why is this wrong? Consider
Note: After erase, it points at a different item. The rule is: after an insertion or erase, all iterators pointing after the point of insertion/erasure are considered invalid and must be refreshed.
Correct:
Design Patterns Continued
Guiding principle: program to the interface, not the implementation.
-
Abstract base classes define the interface
- work with base class pointers & call their methods
-
Concrete subclasses can be swapped in & out
- abstraction over a variety of behaviours
Eg Iterator pattern.
Then you can write code that operates over iterators.
Works over lists and sets.
Observer Pattern
Publish — subscribe model
One class: publisher/subject — generates data. One or more subscriber/observer classes — receive data and react to it.
Eg subject = spreadsheet cells, observers = graphs
- when cells change, graphs update
Can be many different kinds of observer objects — subject should not have to know all the details.
Observer Pattern
classDiagram
Subject o--|> Observer
Subject <|-- Concrete Subject
Concrete Observer o--|> Concrete Subject
Observer <|-- Concrete Observer
Subject: +notify(Observers&)
Subject: +attach(Observer)
Subject: +detach(Observer)
Observer: +notify
Concrete Observer: +notify()
Concrete Subject: +getState()
Sequence of method calls:
-
Subject’s state is updated.
-
Subject::notifyObservers() — calls each observer’s notify
-
Each observer calls ConcreteSubject::getState to query the state & reacts accordingly
Example: Horse races
Subject — publishes winners
Observers — individual bettors — declare victory when their horse wins.
Subject class
Observer class
main:
Lecture 17
Decorator Pattern
Want to enhance an object at runtime — add functionality/features.
Eg Windowing system
-
start with a basic window
-
add scrollbar
-
add menu
Want to choose these enhancements at runtime.
classDiagram
Component <|--o Decorator
Concrete Component --|> Component
Decorator --|> Component
Concrete Decorator A --|> Decorator
Concrete Decorator B --|> Decorator
Component: + operation
Concrete Component: +operation
Concrete Decorator A: +operation()
Concrete Decorator B: +operation()
Component — defines the interface — operations your objects will provide
Concrete Component — implements the interface
Decorators — all inherit from Decorator, which inherits from Component.
Therefore every Decorator is a Component and every Decorator HAS a Component.
Eg Window w/scrollbar is a kind of window and has a pointer to the underlying plain window.
Window w/scrollbar and menu is a window, has a pointer to the window w/scrollbar, which has a pointer to window. (similar to linked list).
All inherit from Abstract Window, so Window methods can be use polymorphically on all of them.
Eg Pizza.
Client Code.
User:
What happens when you go out of bounds? What should happen?
Problem Vector’s code can detect the error, but doesn’t know what to do about it. Client can respond, but can’t detect the error.
C Solution: Functions return a status code or sets the global variable errno. Leads to awkward programming. Encourages programmers to ignore error-checks.
Exceptions
C++ — when an error condition arises, the function raises an exception. What happens? By default, execution stops.
But we can write handlers to catch errors & deal with them.
vector<T>::at
throws an exception of type std::out.of.range
when it fails. We can handle it as follows.
Now consider:
What happens? Main calls h, h calls g, g calls f, f throws out-of-range.
Control goes back through the call chain (unwinding the stack) until a handler is found.
All the way back to main, main handles the exception.
No matching handler in the entire call chain program terminates.
A handler might do part of the recovery job, i.e execute some corrective code.
Lecture 18
Recall: An exception can do part of the recovery job, throw another exception.
Or rethrow the same exception.
A handler can act as a catch-all.
You can throw anything you want — don’t have to be objects.
When new fails: throws std::bad_alloc
. Never let a destructor throw or propagate an exception.
-
Program will abort immediately
-
If you want a throwing destructor, you can tag it with noexcept(false).
But — if a destructor throws during stack unwinding while dealing with another exception, you know have two active, unhandled exceptions, & the program will abort immediately.
Much more to come.
Factory Method Pattern
Write a video game with 2 kinds of enemies: turtles & bullets.
-
System randomly sends turtles & bullets. Bullets more common in harder levels.
-
Never know exactly which enemy comes next, so can’t call turtle/bullet directly.
-
Instead, put a factory method in Level that creates enemies.
- Method that “creates things”
Template Method Pattern
- Want subclasses to override superclass behaviour, but some aspects must stay the same.
Eg
There are red turtles & green turtles.
Subclasses can’t change the way a turtle is drawn (head, shell, feet), but can change the way the shell is drawn.
Generalization: the Non-Virtual Interface (NVI) idiom.
-
A public virtual method is really two things:
-
public:
-
an interface to the client
-
promises certain behaviour with pre/post conditions.
-
-
virtual:
-
an interface to subclasses
-
behaviour can be replaced with anything the subclass wants
-
-
Public & virtual making promises you can’t keep!
NVI says:
-
All public methods should be non-virtual.
-
All virtual methods should be private or protected
-
except the destructor
Generalizes Template Method
- every virtual method should be called from within a template method.
STL Maps — for creating dictionaries
Eg “arrays” that map strings to integers
Lecture 19
Recall:
Iterating over a map sorted key order.
p’s type is std::pair<string,int> (<utility>)
std::pair is implemented as a struct, not as a class. Fields are public.
In general: using ‘class’ implies you are making an abstraction, with invariants that must be maintained.
Struct signals that this is purely a conglomeration of values, no invariants, all field values valid.
Alternatively:
Structured bindings can be used on any structure(class) type with all fields public:
Eg
Or on a stack array of known size:
What should go into a module?
So far — each class gets its own module.
But a module can contain any # of classes & functions.
When should classes/functions be grouped together in a module and when should they be in separate modules?
Two measures of design quality — coupling & cohesion.
Coupling — how much distinct program modules depend on each other.
-
low:
-
modules communicate via function calls with basic parameters and results
-
modules pass arrays/structs back & forth
-
modules affect each other’s control flow
-
modules share global data
-
-
high:
- modules have access to each other’s implementation (friends)
High coupling changes to one module require greater changes to other modules. Harder to reuse individual modules.
Cohesion — how closely elements of a module are related to each other.
-
low:
-
arbitrary grouping of unrelated elements (e.g. <utility>)
-
elements share a common theme, otherwise unrelated, maybe some common base code (e.g. <algorithm>)
-
elements manipulate state over the lifetime of an object (e.g. open/read/close files)
-
elements pass data to each other
-
-
high:
- elements cooperate to perform exactly one task
Low cohesion poorly organized code — can’t reuse one part without getting other stuff bundled with it — hard to understand, maintain.
Goal: low coupling, high cohesion.
Special case: What if 2 classes depend on each other?
Impossible. How big would A & B be?
But
Sometimes one class must come before the other.
Eg
Need to know the size of C to construct D & E. C must come first.
Q: How should A & B be placed into modules?
A: Modules must be compiled in dependency order. One module can’t forward declare another module, nor any item within that module. Therefore, A & B must be in the same module.
(Makes sense, since A & B are obviously tightly coupled).
Decoupling the Interface (MVC)
Your primary program classes should not be printing things.
E.g
Bad design — inhibits code reuse.
What if you want to reuse ChessBoard, but not have it communicate via cout?
One solution: parameterize the class by a stream:
Still suffers from the same problem. Better — but what if we don’t want to use streams at all?
Your chessboard class should not be communicating with users at all.
Single Responsibility Principle: “A class should have only one reason to change”
I.e if distinct prats of the problem specification affect the same class , then the class is doing too much.
Each class should do only one job — game state & communication are two jobs.
Better:
-
Communicate with ChessBoard via params/results/exns.
-
Confine user communication to outside the game class
Q: Should main do the talking?
A: No. Hard to reuse or replace code if it is in main
Should have a class to manage communication, that is separate from the game state class.
Architecture: Model-View-Controller (MVC)
Separate the distinct notions of the data (or state — “model”) the presentation of data (“view”) and control or manipulation of the data (“controller”).
MVC image here
Model:
-
Can have multiple views (e.g. Text & graphics)
-
Doesn’t need to know their details
-
Classic Observer pattern (or could communicate through the controller)
Lecture 20
Recall: MVC
Controller:
-
Mediates control flow through model & view
-
May encapsulate turn-taking, or full game rules
-
May communicate with the user for input (or this could be the view)
Exception Safety
Consider:
No leaks — but what if g throws?
What is guaranteed?
-
During stack-unwinding all stack-allocated data is cleaned up — destructors run, memory is reclaimed
-
Heap-allocated memory is not reclaimed
Therefore, if g throws, C is not leaked, but *p is.
Error-Prone duplication of code. How else can we guarantee that something (eg delete p) will happen, no matter how we exit f? (normal or by exn)?
In some languages — “finally” clauses guarantee certain final actions — not in C++. Only thing you can count in in C++ — destructors for stack-allocated data will run. Therefore use stack-allocated data with destructors as much as possible. Use the guarantee to your advantage.
C++ Idiom: RAII — Resource Acquisition Is Initialization
Every resource should be wrapped in a stack-allocated object, whose job it is to delete it.
Eg Files
Acquiring the resource (“file”) = initializing the object(f)
The file is guaranteed to be released when f is popped from the stack (f’s destructor runs)
This can be done with dynamic memory.
-
Takes a T* in the constructor
-
The destructor will delete the pointer
-
In between — can dereference, just like a pointer
No leaks guaranteed
Alternative
Allocates a C object on the heap, and puts a pointer to it inside a unique pointer to object.
Difficulty:
What would happen if a unique_ptr were copied?
Don’t want to delete the same pointer twice.
Instead — copying is disabled for unique_ptrs. They can only be moved.
If you need to be able to copy pointers — first answer the question of ownership. Who will own the resource? who will have responsibility for freeing it?
- That pointer should be a unique_ptr. All other pointers should be raw pointers (can fetch the underlying raw pointer with p.get()).
New understanding of pointers:
-
unique_ptr — indicates ownership — delete will happen automatically when the unique_pointers go out of scope.
-
raw pointer — indicates non-ownership. Since a raw pointer is considered not to own the resource it points at and you should not delete it.
Moving a unique_ptr = transfer of ownership.
Pointers as parameters
Pointers as results:
Return by value is always a move, so f is handing over ownership of the C object to the caller.
The pointer returned by g is understood not to be deleted by the caller, so it might represent a pointer to non-heap data, or to heap data that someone else already owns. Rarely, a situation may arise that calls for true shared ownership, i.e. any of several pointers might need to free the resource.
- use std::shared_ptr
Shared pointers maintain a reference-count of all shared_ptrs pointing at the same object.
Memory is freed when the # of shared_ptrs pointing to it will reach 0.
Recall (Racket)
rest of l2 points to second element of l1
Lecture 21
Exception safety… What is exception safety?
It is not
-
exceptions never happen
-
all exceptions get handled
It is
- after an exception has been handled, the program is not left in a broken or unusable state
Specifically, 3 levels of exception safety for a function f:
-
Basic guarantee — if an exception occurs, the program will be in some valid state. Nothing is leaked no corrupted data structures, all class invariants maintained.
-
Strong guarantee — if an exception is raised while executing f, the state of the program will be as if f had not been called.
-
No-throw guarantee — f will never throw or propagate an exception and will always accomplish its task.
Eg
Is C::f exception safe?
-
If a.g() throws — nothing has happened yet. OK.
-
If b.h() throws — effects of g would have to be undone to offer the strong guarantee
- very hard or impossible if g has non-local side-effects
No, probably not exception safe.
If A::g and B::h do not have non-local side effects, can use copy & swap.
Better if swap was no-throw. Recall : copying pointers can’t throw.
Solution: Access C’s internal state through a pointer (called the pimpl idiom).
If either A::g or B::h offer no exception safety, then neither can f.
Exception Safety & the STL — Vectors
Vectors — encapsulate a heap-allocated array
- follow RAII — when a stack-allocated vector goes out of scope, the internal heap array is freed.
But
But
-
If the array is full (i.e size == cap)
-
allocate new array
-
copy objects over (copy ctor)
-
if a copy ctor throws*
-
destroy the new array
-
old array still intact
-
strong guarantee
-
-
-
delete old array (no throw)
-
*But — copying is expensive & the old array will be thrown away. Wouldn’t moving the objects be more efficient?
-
allocate the new array
-
move the objects over (move ctor)
-
if move constructor throws
-
original is no longer intact
-
can’t offer the strong guarantee
-
-
-
delete the old array (no-throw)
If the move constructor offers the no-throw guarantee, emplace_back will use the move constructor. Otherwise it will use the copy constructor, which may be slower.
So your move operations should offer the no-throw guarantee, and you should indicate that they do:
If you know a function will never throw or propagate an exception, declare it noexcept. Facilitates optimization.
At minimum: moves & swaps should be noexcept.
Casting
In C:
C-style casts should be avoided in C++.
If you must cast, use a C++ style cast:
4 kinds
- Static_cast — “sensible casts” — casts with a well-defined semantics.
Eg double int
superclass ptr subclass ptr
Lecture 22
Recall: Superclass subclass pointer
Taking responsibility that b actually points to a Text. “Trust me”.
-
Static_cast — “sensible casts” — casts with a well-defined semantics.
-
Reinterpret_cast — unsafe, implementation-specific, “weird” casts
-
Const_cast — for converting between const & non-const — the only C++ cast that can “cast away const”.
-
Static_cast — “sensible casts” — casts with a well-defined semantics.
-
Reinterpret_cast — unsafe, implementation-specific, “weird” casts.
-
Const_cast — for converting between const & non-const — the only C++ cast that can “cast away const”.
-
Dynamic_cast — Is it safe to convert a Book * to a Text *?
Depends on what pb actually points to. Better to do a tentative cast — try it & see if it succeeds.
If the cast works (*pb really is a Text, or a subclass of Text), pt points to the object.
If not — pt will be nullptr.
These are options on raw pointers. Can we do them with smart pointers?
Yes — static_pointer_cast, etc. Cast shared_ptrs to shared_ptrs.
Dynamic_casting also works with refs:
If b “points to” a Text, then t2 is a reference to the same Text.
If not ? (No such thing as a null reference). Throws std::bad_cast.
Note: Dynamic casting only works on classes with at least one virtual method.
Dynamic reference casting offers a possible solution to the polymorphic assignment problem:
Is dynamic casting good style?
Can use dynamic casting to make decisions based on an object’s runtime type information (RTTI)
Code like this is tightly coupled to the Book hierarchy, and may indicate bad design.
Why? What if you create a new kind of Book?
-
WhatIsIt doesn’t work anymore until you add a clause for the book type.
-
Must do this wherever you are dynamic casting
Better: use virtual methods
Note: Text::operator= does not have this problem (only need to compare with your own type, not all the types in the hierarchy).
So dynamic casting isn’t always bad design.
How can we fix whatIsIt?
Works by having an interface function that is uniform across all Book types. What if the interface isn’t uniform across the hierarchy?
Inheritance & virtual methods work well when
-
There is an unlimited number of potential specializations of a basic abstraction.
-
Each following the same interface
But what about the opposite case
-
Small number of specializations, all known in advance, unlikely to change
-
With different interfaces
In the first case — new subclass no effort at all
In the second case — new subclass rework existing code to accommodate the new interface, but that’s fine because you are not expecting to add new subclasses or you are expecting to put in that effort.
Eg
Interfaces not uniform. A new enemy type is going to mean a new interface & unavoidable work. So we could regard the set of Enemy classes as fixed, and maybe dynamic casting is justified.
But: in this case, maybe inheritance is the wrong tool. If you know that the enemy will only be a Turtle or Bullet, and you accept the work that comes with adding new Enemy types, then consider:
Lecture 23
Recall:
A variant is like a union but type-safe. Attempting to store as one type & fetch as another will throw.
If a variant is left uninitialized, the first option in the variant is default-constructed to initialize the variant.
Compiler error if first option is not default-constructible.
Options:
-
Make the first option a type that has a default ctor.
-
Don’t leave your variant uninitialized.
-
Use
std::monostate
as the first option. “Dummy” type that can be used as default.- Can be used to create an “optional” type: e.g. variant<monostate,T> = “T or nothing”. (Also: std::optional<T>).
How Virtual Methods Work
First note:
-
8 is space for 2 integers. No space for f method
-
Compiler turns methods into ordinary functions & separates them.
Recall:
isHeavy is virtual choice of which version to run is based on the type of the actual object — which the compiler won’t know in advance.
choice must be made at runtime. How?
For each class with virtual methods, the compiler creates a table of function pointers (the vtable).
C objects have an extra pointer (the vptr) that points to C’s vtable:
Calling a virtual method:
-
follow vptr to vtable
-
fetch ptr to actual method from table
-
follow the function pointer & call the function
-
(all of the above happens at run-time)
virtual function calls incur a small overhead cost in time.
Also: Having function adds a vptr to the object. classes with no virtual functions produce smaller obs than if some were virtual — space cost.
Concretely, how is an object laid out? Compiler-dependent. Why did we put the vptr first in the object and not somewhere else (e.g last)?
But…
Multiple Inheritance
A class can inherit from more than one class.
classDiagram
A <|-- C
B <|-- C
Challenges: Suppose
classDiagram
D --|> B
D --|> C
B --|> A
C --|> Also_A
D: +d
B: +b
C: +c
A: +a
Also_A: +a
B & C inherit from A.
Need to specify dobj.B::a or dobj.C::a.
But if B & C inherit from A, should there by one A part of D or two? (Two is the default).
Should B::a, C::a be the same or different?
What if we want
classDiagram
A -- B
A -- C
B -- D
C -- D
(“deadly diamond”)
Make A a virtual base class — virtual
E.g. IO stream hierarchy
How would this be laid out?
Distance from class to parent is not constant. It depends on the runtime type of object.
Solution: Distance to the parent object is stored in the vtable.
Diagram still doesn’t look like all of A,B,C,D simultaneously. But slices of it do look like A,B,C,D.
pointer assignment among A,B,C,D changes the address stored in the pointer.
Static/dynamic cast will also do this, reinterpret_cast will not.
Lecture 24
If C++ can’t determine T, you can tell it.
For what types T can min be used? For what types T does the body compile?
Any type for which operator < is defined.
Cutoff for Exam (I stopped paying attention here, something about algorithms)