Skip to content

Latest commit

 

History

History
1505 lines (930 loc) · 51.5 KB

10. Advanced C++.md

File metadata and controls

1505 lines (930 loc) · 51.5 KB

10. Advanced C++

Pimpl Idiom (March 24)

Consider the class for an XWindowing (windowing system)

// xwindow.h
class XWindow {
    Display *d;
    Window w;
    int s;
    GC gc;
    unsigned long colours[10];
 public:
    ...
};

What if we need to add or change a private member? (i.e., changing the heading file) All clients must recompile. Seems unnecessary.

Would be better to hide these details away. The solution is to used the Pimpl idiom (pointer to implementation).

Since it is an idiom, it is a programming technique, not a design pattern.

Motivation: Our header file have to show the private data fields and methods, accesible to the clients. They are part of the structure, and take up space. Also, any time we change the implementation details, even of the private information and/or methods, the client has to recompile their code that includes the header file.

In other words, it is not just sufficient to link in the new .o file, we need to recompile everything and linking again.

Lets create a second class XWindowImpl, we can replace all of the private data fields with a pointer to a class that we forward-declare in our header file.

Since we are no longer declaring data fields whose types are defined in the X11 library, we can also move that particular include statement to our implementation file.

The client never sees the contents of the implementation file, they just receives the header file and the .o file, they won't know how we actually implemented things.

Every reference to the original data fields in the implementation file now need to go through the pointer to the struct to access the fields declared there:

// XWindowImpl.h

#include <X11/Xlib.h>
// we define XWindowImpl as a struct and take advantage of public accessibility of the data fields.
struct XWindowImpl {
    Display *d;
    Window w;
    int s;
    GC gc;
    unsigned long colours[10];
};

// xwindow.h
// here we redefine XWindowImpl as a class
class XWindowImpl;
class XWindow {
    XWindow Impl *pImpl;	// pointer to our implementation class
  public:
    // no change
};

Note: it is perfectly legal in C++ to redefine a class as a struct, and vice versa in the implementation file.

// xwindow.cc
#include "window.h"
#include "XWindowImpl.h"

XWindow::XWindow(...) : pimpl{new XWindowImpl} {
    // constructor
    ...
   	pImpl->d = XOpenDisplay(nullptr);
    ...
}

XWindow::~XWindow() {
    XFreeGC(pImpl->d, pImpl->gc);
    XCloseDisplay(pImpl->d);
    delete pImpl;
}

In other methods, replace the fields d, w, s, ... with pImpl->d, pImpl->w, pImpl->s,...

If you confine all private fields within XWindowImpl, then adding/changing fields doesn't change the size of XWindow, and clients won't need to recompile only XWindow (and XWindowImpl) do. (i.e., changing fields in the XWindowImpl's interface (.h) won't need to recompile client file)

Alternate example: from cppreference.com

Pimpl is a C++ programming technique that removes implementation details of a class from its object representation by placing them in a separate class, accessed through an opaque pointer:

// widget.h (interface)

class widget {
    struct impl;	// foward declaration of the implementation class
    std::experimental::propagate_const<std::unique_ptr<impl>> pImpl;
    // const-forwarding pointer wrapping, unique-ownership opaque pointer to the foward-declared implementation class
};

// widget.cc (implementation)
struct widget::impl {
    // implementation details
};

This technique reduce compile-time dependencies.

Because private data members of a class participate in its object representation, affecting size and layout, and because private member function of a class participate in overload resolution, any change to those implementation details requires recompilation of all users of the class (including header files).

pImpl removes this compilation dependency, changes to those implementation (of members) do not cause recompilation.

Also thank you Matthieu 🙏

以及感谢此帖

Measures of Design Quality

Coupling 耦合度(Coupling)是一个类与其他类关联、知道其他类的信息或者依赖其他类的强弱程度的度量。

  • How much distinct program modules depend on each other
  • low coupling
    • (low) modules communicate via calls with basic parameters and results
    • modules pass arrays/structs back and forth
    • modules accept each other's control flow
    • modules share global data
    • (high) modules have access to each other's data directly (i.e. friend)
  • high coupling
    • Higher coupling means changes to one modules require greater changes to others. Makes it harder to reuse individual modules.

Cohesion 聚合度(Cohesion)是对一个类中的各个职责之间相关程度和集中程度的度量。

  • How closely elements of a module are related to each other
  • low cohesion
    • (low) arbitrary grouping of relatively unrelated elements (<utility>)
    • elements share some common theme, otherwise unrelated. Perhaps they share some base code (<algorithm>)
    • elements manipulate state over the lifetime of an object or resource (e.g., opening, reading, closing files)
    • elements pass data to each other to achieve work
    • (high) elements cooperate to perform exactly one task (<vector>)
  • high cohesion
    • low cohesion means poorly organized code, and harder to understand and maintain

Goal:

Low coupling and High cohesion!!

If we apply the goal of low coupling and high cohesion consistently in our designs, then we're effectively applying the design principle called the single responsibility principle (SPR). All of the design patterns that we have seen decoupled classes by introducing abstract base classes, which let the client "program to the interface, and not the the implementation".

Your primary goal of classes should not be printing/displaying things

Example: ChessBoard

class ChessBoard {
    ...
    cout << "Your move!" << endl;
    cin >> c; // read move
};

// this is questionable design, it limits code reuse.

What if I want to communicate via a different output stream?

Could instead construct the object with the stream I want...better, but what if I don't want streams at all, what if I want graphics?

Your chess board should be a chessboard - Chessboards don't talk! Your chessboard shouldn't be handling communications at all!

Single Responsibility Principle: "A class should have only one reason to change"

Game state and communication are the reasons! Chessboard shouldn't worry about both.

Better: the chessboard should provide an interface for checking/mutating its state via parameters/returns/exceptions when necessary. Confine the actual user interface outside of the chessboard class.

Question: Should main.cc handle all communication, and then call the related chessboard methods?

No, hard to reuse code in main. We should make a class distinct from the game class whose job is to handle communication.

Now, introducing...[drunroll]

Architecture: Model-View-Controller (MCV)

In MVC, the program state, presentation logic, and control logic are all separated.

Separate the distinct notions of the data (aka the state "Model"), the presentation of the data ("view"), and the control or manipulation of data ("Controller").

The Model:

  • manages application data and its modification (doesn't know anything about how the data is presented to the user)

  • can have multiple view (text and graphics)

  • doesn't need to know about their details

  • can be implemented via observer pattern

    • We would have a link between view and model, or can communicate through the controller

The Controller:

  • Mediates control flow between model and view
  • may encapsulate turn taking or even full game rules (trade off with model)
  • may communicate with user for input (or could be in the view)

The View:

  • Manages the interface to present data. It decides how the data from the model should be presented to the user according to the capabilities of the device (e.g., a graphical interface, a text-based interface, a conversational interface, etc.)

By decoupling, presentation and control MVC promotes reuse.

10-1

The Model and View in MVC are a classic implementation of the Observer design pattern, where the Model is the subject and the View is the observer.

Example:

In chess game, we could display a graphical representation of the board, the player would drag a piece to make a move.

  1. the view would have some method that responds to a mouse drag. This method would delegate handling the player action to the Controller.

    void ChessView::handleMouseDrag(Point start, Point end) {
        controller->handleMouseDrag(start, end);
    }
  2. the controller should translate the user interface event to game model event. So, the controller should identify what piece was dragged into what board position. Then, ask the model to handle this game action.

    void ChessController::handleMouseDrag(Point start, Point end) {
        Piece piece = findPiece(start);
        Position position = findBoardPosition(end);
        model->movePieceTo(piece, position);
    }
  3. the model contains the logic to handle the game action, first, it must validate if the move is a valid one. If it is, then the move should be executed by updating the state. Finally, the views(observers) are notified of the change.

    void ChessModel::movePieceTo(Piece piece, Position position) {
        if (isValidMove(piece, position)) {
            // update the state of the internal fields to put the piece in the new
            // position; check other consequences of the new piece position,
            // e.g., if the player won the game, captured an enemy piece, etc.
            notifyView();
        }
    }
    
    void ChessModel::notifyViews() {
        // subject implementation of the observer design pattern
        for (int i = 0; i < view.size(); i++) {
            views.at[i].update();
        }
    }
  4. Each view has a concrete implementation of the update method (observer design pattern), which should display the new state of the game board.

    void ChessView::update() {
        // read the updated state from the model and
        // redraw the game board in the user interface
    }

Some of the benefits of this approach are increasing cohesion (as each class has only one responsibility), decreasing coupling (as each class communicates with the other just via a pubic interface), and making reuse easier.

Exception Safety

Consider:

void f() {
    myClass mc;
    myClass *p = new myClass;
    g();
    delete p;
    // does this function have memory leak?
    // under normal circumstances: no memory is leaksed, p is deleted on the last line of function
    // mc is stack-allocated, so the destructl will automatically run during stack unwinding after the end of f's execution
    // but what happens if g throws an exception...
}

If g throws an exception, what is guaranteed?

During stack unwinding, all stack allocated data is deallocated: destructor run, memory is reclaimed (so mc will still be deleted)

But pointers to heap allocated memory are not freed. (the last line od function will not execute, so p is leaked)

Therefore, if g throws, p is leaked, mc is not.

We could try adding an exception handler:

void f() {
    myClass mc;
    myClass *p = new myClass;
    try {
        g();
    } catch (...) {
        delete p;
        throw;	// rethrow the exception to continue stack unwinding
    }
    delete p;
    // this is ugly! repeated code.
    // Furthermore, do we want to wrap every potential error throwing code in this?
    // No!
}

What we want is: here's some code that will run no matter how this function exists (normally or by exception). We want to guarantee that something (here, delete p;) will happen.

In some languages, a "finally" clause exists to do this. But we just said, what c++ promises, and it didn't say anything about a final clause. It did, however; say something about code that's guaranteed to run with an exception is thrown: only destructors of stack allocated data are guaranteed to run.

C++ idiom: RAII - Resource Acquisition Is Initialization

Resources should never be acquired except through the initialization of an object whose job it is to maintain that resource. E.g., we work with files without knowing what a file pointer is.

It guarantees that resouces will be freed at the end of the function.

RAII and Smart Pointers (March 29)

Recall:

consider:

void f() {
    MyClass mc;
    MyClass *p = new MyClass;
    g();
    delete p;
}

Leaks p if g throws an exception!

Could wrap all potentially error throwing code in

try { ... } catch { delete p; }

But! this is error prone and ugly, lots of repeated code. C++ promises if an exception is thrown, the stack is unwund, and destructors of stack-allocated objects will run... so ...

RAII - resource acquisition is initialization.

Resources should only be obtained via the initialization of objects whose job it is to manage them.

Example: files

{
    ifstream f{"myfile.txt"}
    ...
} // file is closed

Acquiring the recourse (file pointer) occurred by initializing the object f.

The file is guaranteed to be released (closed) when f is popped from the stack (f's destructor runs) (when f is out of scope).

Now consider:

MyClass *f() {
    MyClass mc;
    MyClass *p = new MyClass;
    ... // mutate object p points at
    g();
    return p;
}

This can be doen with dynamic memory:

#include <memory>
class std::unique_ptr<T> 

class std::unique_ptr<T> is a class that holdes a T * which you supply in the constructor.

std::unique_ptr<MyClass> p { new MyClass };
// still calling new! prefer we never call new at all!
MyClass *x = new MyClass;
unique_ptr<MyClass> p1{x};
unique_ptr<MyClass> p2{x};
// prefer we never call new at all! above is not RAII

unique_ptr means this is the only one pointer to the object. When we are trying to delete the pointer, there will be a double delete.

Prefer we never use new at all.

Even better:

unique_ptr<MyClass> p = make_unique<MyClass>();

std::make_unique<T> is a function which takes as its parameters the constructor paramaters for type T.

Returns a unique_ptr, it is the one that calls new.

Example:

class MyClass {
    ...
  public:
    MyClass(int x, int y) ... ;	// here1
    MyClass(int *p) ... ;		// here2
    
};


// here1: 
auto p = make_unique<MyClass>(3, 5);

// here2:
int x;
auto p = make_unique<MyClass>(&x);
// use p normally as pointers

Now, let's talk about the difficulty: does it make sense to copy something that is unique?

we say move, not copy, because unique things are not meant to be copied.

unique_ptr<C> p { new C(...) }

unique_ptr<C> q = p; // compile error!

unique_ptrs are unique! i.e. canNOT be copied!

They can only be moved.

Sample implementation:

// basicimpl.cc

#include <utility>
#include <iostream>

// explicitly disallowing the compiler for providing a default implementation for this:
unique_ptr(const unique_prt<T> &other) = delete;
unique_ptr<T> &operator=(const unique_ptr<T> &other) = delete;

We can tell compiler not to use built in copy construct and copy assignment operator by using = delete;, after function.

Example: things we CANNOT do:

void f() {
    int *p = new int{5};
	unique_ptr<int> p1{p};
	unique_ptr<int> p2{p};	// create two unique pointers for the same integer
}	// p1, p2 out of scope

// BAD! This results in double free!

When p1 is out of scope, its destructor runs. Then when p2 is out of scope, its destructor also runs, destroying the memory that has been already destroyed by p1. Hence double free! Make sure you never do this.

Copying pointers

What if you need to be able to copy pointers?

The first question you need to answer is: Who owns it?

  • Who will own the resource? Who has the responsibility of freeing it?

  • Other pointer should be a unique_ptr. all others can be raw pointers

  • can fetch the raw pointer with std::unique_ptr<T>::get() // a raw pointer who doesn't have the ownership of the unique pointer but stores it

  • std::unique_ptr<Cell> p{...};
    s.attach(p.get());

If there is true ownership, i.e. any of several pointers might need to free the resource -- the last one that is being deleted destructs the object.

  • use std::share_ptr<T>, which also have make_share<T> (...);
{ 
    auto p1 = make_share<MyClass>();	// allocates space for MyClass
    if (...) {
        auto p2 = p1; // two pointers point at the same object
    }	// p2 goes out of scope, the object is not deleted (because p1 still exists and points at that memory)
}	// here, p1 is popped, and the object is deleted

Q: How do destructor know when to delete. and when to not delete?

A: Have a count of the shared pointers:

Shared_ptrs maintain a reference count - a count of all shared_ptrs pointing at the same object.

The memory is freed when the number of shared_ptrs pointing at it reaches 0.

Use the type of ptr that accurately reflects the ptrs ownership role: Dramatically fewer opportunities for leaks.

More about Smart Pointers

(These are not included in the lecture, but previous from course notes)

Why is it hard to manage memory?

  • You have two options for storing something: heap or stack. Local variables allocate their space on the stack, and to use heap storage, instead put a pointer on stack, and use new and delete to allocate and reclaim its space.
  • the problem with stack-based storage: it's too limiting in object lifetime: all stack-allocated values are destroyed when the relevant function returns, all object must exist for exactly the duration of the function that declares them.
  • heap-allocated values do not have this restriction, but they are difficult to corretly manage, control flow can be extremely complicated.

The C++ STL offers two wrapper classes for pointers, which gives greater flexibility than stack storage, handle most lifetimes for objects, as well as providing protection to assure that those lifetimes are correctly implementated.

unique_ptr

A unique_ptr wraps a pointer, and it is guarantted to be the only pointer to the heap-allocated object.

Since this unique_ptr is unique, its own destructor can delete the object pointer to. The pointed-to object is deleted when its unique_ptr goes out of scope.

Note that the automatic type of p is unique_ptr<Class>

unique_ptrs enforce their uniqueness. Thus, a unique_ptr cannot be copied; to do so would make it non-unique. Since unique_ptrs cannot be copied, they cannot be passed by value

We must instead move it.

To move a unique_ptr is to transfer the actual, underlying pointer to another unique_ptr, and remove it from its starting unique_ptr.

In this way, the "ownership" of the object may change, if we were to attempt to use p after the move, it would no longer be available:

#include <memory>
class Point {
  public:
    Point(double sx, double sy) : x{sx}, y{sy} {}
    double x, y;
};

double pointDistance(std::unique_ptr<Point>);

double myDistance(double x, double y) {
    auto p = std::make_unique<Point>(x, y);
    // here, we could access p->x and p->y, just as if p was a pointer
    double ret = pointDistance(std::move(p));
    ret += p->x; // segmentation fault!
    return ret;
}

In this way, unique_ptrs have a restricted lifetime, like stack values, but with much more control. The object is deleted when unique_ptr goes out of scope, like stack-allocated values, and we do not need to worry about explicitly deleting the object, or tracking all of the particular paths through the program, including exceptions, that may need to be handled to make sure deletion occurs.

But, because we can move a unique_ptr and transfer the ownership and extend (or contract) the lifetime, our values may have arbitraily long lifetime, like heap-allocated values.

Though unique_ptrs cannot be passed by value (because they cannot be copied), they can safely be passed by reference. But it is not common to do this. The get method of unique_ptrs gets the underlying pointer, but it is still considered owned by the unique_ptr. That is, when the unique_ptr goes out of scope, the object will be deleted, even if you have extracted the pointer with get.

Usage:

double pointDistance(Point *);

double myDistance(double x, double y) {
    auto p = make_unique<Point>(x, y);
    return pointDistance(p.get());
}

Be careful when to use get to assure that the underlying pointer is not retained after the unique_ptr has gone out of scope.

Consider the owndership when considering whether to use unique_ptrs:

  • if the pointer has a unique owner, use unique_ptr
  • if there is no single owner, you need something more sophisticated: the shared_ptr

shared_ptr

shared_ptr is a pointer that is shared, and it controls the lifetime of the object it points to. Unlike a unique_ptr, you may have multiple shared_ptr pointing to the same object.

shared_ptr can be copied and passed by value, exactly like normal * pointers, but don't need to be explicitly deleted.

How is this possible? shared_ptrs used a technique called reference counting to determine when there are not more shared_ptrs pointing to a given object. When you copy a shared_ptr, it increases an internal reference count by 1. When a shared_ptr goes out of scope, the internal reference count is reduced by 1. When it reaches 0, it is deleted.

All of this reference count is handled automatically in the constructor, copy constructor, and destructor for shared_ptr itself. So usually, all a programmer needs to do is use shared_ptr instead of raw pointers.

Improving the above example using shared_ptrs:

double pointDistance(std::shared_ptr<Point>);

double myDistance(double x, double y) {
    auto p = std::make_share<Point>(x, y);
    return pointDistance(p);
}

p does not need to be removed, but can simply be shared, so p remains usable:

double myDistance(double x, double y) {
    auto p = std::make_share<Point>(x, y);
    double ret = pointDistance(p);
    ret += p->x; // no problem here
    return ret;
}

The summarized example:

double pointDistance(std::shared_ptr<Point> p2) {
    return sqrt(p2->x*p2->x + p2->y*p2->y);
}

double mydistance(double x, double y) {
    auto p = std::make_shared<Point>(x, y); // p's reference count is 1
    double ret = pointDistance(p); // During pointDistance, p's reference count is 2
    // As soon as pointDistance ends, p's reference count is reduced to 1 again
    ret += p->x;
    return ret; // After returning, p's reference count is reduced to 0, so the Point is deleted
}
std::shared_ptr<Point> lastPoint;

double pointDistance(std::shared_ptr<Point> p2) {
    // Calling this function increases p2's reference count by 1
    double dx = p2->x - lastPoint->x,
           dy = p2->y - lastPoint->y;
    double ret = sqrt(dx*dx + dy*dy);

    /* This assignment increases p2's reference count by 1, but also decreases
     * the reference count of the previous object pointed to by lastPoint. If
     * the prevoius reference count was reduced to 0, then the previous
     * lastPoint would additionally be deleted. */
    lastPoint = p2;

    return ret; // Returning reduces p2's reference count by 1, but the increase from lastPoint remains
}

double mydistance(double x, double y) {
    auto p = std::make_shared<Point>(x, y); // p's reference count is 1

    /* Calling pointDistance increases p's reference count to 2 by copying it
     * to p2. pointDistance then increases it to 3, by copying it to lastPoint. */
    double ret = pointDistance(p);
    /* When pointDistance returns, p's reference count is reduced to 2, because
     * p2 goes out of scope. */

    return ret;
    /* When this function returns, p's reference count is reduced to 1, so it
     * is not deleted. It is still referenced by lastPoint. Its reference count
     * only reaches 0 when lastPoint is replaced, and at that point, it is
     * deleted. */
}
Caveats of shared_ptrs

shared_ptrs come with important caveats that must be understood.

Consider the implementation of a graph:

class GraphNode {
    string name;
    vector<stared_ptr<GraphNode>> vertices;
    
  public:
    GraphNode(sname) : name(sname) {}
    
    void addVertex(shared_ptr<GraphNode> to) {
        vertices.push_back(to);
    }
}

This seems like a fine way of storying graph nodes and vertices, but in many real example, it will leak memory, failing to delete the graph nodes.

Why? consider this example:

void graphWork() {
    auto root = make_shared<GraphNode>("Node 1");
    auto n2 = make_shared<GraphNode>("Node 2");
    root->addVertex(n2);
    ... // some graph work
    return;
}

Adding the vertex from root to n2 increases n2's reference count to 2, because it now has the additional reference from root's vertices, similarly, is also increases root's reference count to 2.

Returning from graphWork reduces each of them by 1, because the root and n2 variables have gone out of scopt. But, 2 - 1 = 1, so these two objects are never deleted, and their memory is leaked.

This problem is called cylic reference, and is a fundamental flaw in the reference counting technique.

The only solutions to it are:

  1. reorganize your data so that no such cycles exists
  2. not to use reference counting (shared_ptrs) at all

Usually, complex data structures like this are implemented with normal * pointers, and controlled manually.

The second major caveat with shared_ptrs is that counting reference is extra work, and thus a performance penalty.

Like unique_ptrs, the shared_ptrs have a get method, allowing you to get the underlying pointer. This underlying pointer is unprotected, and you can only guarantee that it isn't deleted by making sure you retain a shared_ptr for at least as long as the * pointer.

shared_ptrs also have a use_count method, which returns the reference count. This is occasionally useful for debugging, otherwise it should never be used.

Summary

Both unique_ptrs and shared_ptrs are so-called smart pointers.

Many other programming languages, such as Racket, use a technique called garbage collection to get rid of the need for explicit memory management at all, but at a performance cost similar to (but usually better than) shared_ptrs. However, garbage collection is infeasible for C++.

RAII: Resource Acquisition Is Initialization

Resources that much be explicitly cleaned up, such as heap-allocated objects, are bound to resources which are cleaned up automatically.

It is actually cleanup that is simplified by RAII, not acquisition.

RAII is the concept that you should always design your classes so that object lifetimes are bound to other objects.

Recall the GraphNode example, using normal pointers:

class GraphNode {
    string name;
    vector<GraphNode *> vertices;
  public:
    GraphNode(string name) : name(sname) {}
    
    void addVertex(GraphNode *to) {
        vertices.push_back(to);
    }
}

It would be exceptionally difficult to properly delete an entire graph, decause deleting a node does not delete every not it references.

How shall we redefine out graph type so that the lifetime of the nodes is properly managed?

Solution: have a surrounding Graph type, and bind the nodes to the Graph itself:

class Graph {
  public:
    shared_ptr<GraphNode> createNode(string name) {
        auto node = make_shared<GraphNode>(name, nodes.length);
        nodes.push_back(node);
        return node;
    }
    
    shared_ptr<GraphNode> getNode(int index) {
        return nodes[index];
    }
    
    void addVertex(shared_ptr<GraphNode> from, shared_ptr<GraphNode> to) {
        from->addVertex(to->getIndex());
	}
	
  private:
    vector<shared_ptr<GraphNode>> nodes;
}


class GraphNode {
    string name;
    int index;
    vector<int> vertices;
    
  public:
    GraphNode(string sname, int index) : name{sname}, index{sindex} {}
    
    int getIndex() {
        return index;
    }
    
    void addVertex(int to) {
        vertices.push_back(to);
    }
    
    vector<shared_ptr<GraphNode>> getVertices(shared_ptr<Graph> graph) {
        vector<shared_ptr<GraphNode>> ret;
        for (auto index : vertices) {
            ret.push_back(graph->getNode(index));
        }
        return ret;
    }
}

In this example, instead of GraphNodes referring directly to other GraphNodes, which created the problem of cyclic dependencies, GraphNodes refer only to indices.

The association between indices and actual GraphNodes is only in a single, surrounding Graph object, and vertices can only be resolved to nodes using both GraphNodes and Graph. This is certainly more complicated to write, but it vastly simplifies memory management, since the lifetime of all nodes is tied to Graph, and that Graph can be tied to something with a fixed lifetime, such as a shared_ptr.

The principle of RAII is to build structures such as these, that assure that the lifetime of all objects in a system are connected.

The "R" in RAII stands for "Resource", and heap memory is not the only resource that a program may interact with. The RAII principles are also applied to any resource which must be properly cleaned up.

Consider files:

int getIntFromFile(string name) {
    ifstream f(name);
    int ret;
    f >> ret;
    return ret;
}

In C++ with RAII, while we have explicitly opened the file, by initializing an ifstream, we do not need to explicitly close the file.

This is because ifstream's destructor closes the underlying file, so when f goes out of scope, the file is closed automatically.

Consider several uses of a vector, and how they interact with RAII and memory allocation:

vector<Graph> d;	
vector<Graph *> p;
vector<unique_ptr<Graph>> u;
vector<shared_ptr<Graph>> s;

d:

  • adheres to RAII principles, but might be difficult to use.
  • since the type of the vector is a Graph, destroying the vector will destroy every Graph

p:

  • does not adhere to RAII principles, when p is destroyed, al of its pointers are lost, but the underlying, heap-allocated Graphs are not deleted.
  • we need to loop over it and delete every element to clean up p
  • if an exception is thrown while p is inscope, and we didn't explicitly clean up p in the exception handler, then this memory would be leaked

u:

  • adheres to RAII principle, but might be difficult to use.
  • we cannot copy the unique_ptr out of the vector to use it conveniently
  • we can move the graph out of the vector, but this will set the pointer to nullptr in the vector, rather than actually removing it

s:

  • adheres to RAII principles, and is relatively easy to use.
  • auto g = s[x] will increase a reference count.
  • the Graph will only be destroyed when all references have gone out of scope.
  • using the Graph itself only increases that single reference count

Exception Safety Continued (March 29)

Back to exception safety: There are 3 levels of exception safety for a function f:

  1. Basic Guarantee - if an exception occurs during this function, the program will be in some valid, unspecified state. "valid" means nothing is leaked and class invariants are maintained
  2. Strong Guarantee - if f throws or propagates an exception, the state of the program will be as if f had not been called
  3. No-throw Guarantee - f will never throw an exception and will always complete task

At a minimum, the basic guarantee is expected of any function. Therefore, unless the function documentation mentions that it provides a strong or a no-throw guarantee, we can always expect that it will offer at least the basic guarantee. This also means that, when you are writing a function, you must implement it to at least offer a basic exception guarantee. This means that any function that you write should at least guarantee that, if an exception occurs, nothing will be leaked and class invariants will be maintained (unless you can guarantee that the function will never throw an exception).

Consider:

void f() {
    MyClass mc;
    auto p = make_unique<MyClass>();	// RAII
    g();
}

p's destructor will be called automatically, no memory leaks.

So, using RAII is a good way to avoid memory leaks and ensure a basic exception safety. However, memory leaks are not the only concern for exception safety. It is also necessary to maintain the class invariants. If a function normally makes several changes to the state of a class (non local side effects), an invariant may be broken if some of the changes are kept whereas others are not.

Now, try implement a strong guarantee:

class A { ... }
class B { ... }
class C {
    A a;
    B b;
  public:
    void f() {
        a.g(); // A::g offers strong guarantee
        b.h(); // B::h offers strong guarantee
    }
};

Is C::f() exception-safe?

If a.g() throws, nothing is happened, so that is ok.

If a.g() succeeds, and b.h() throws, the effects of a.g() must be undone to offer the strong guarantee. That is very hard, or impossible, if a.g() has non-local side effects (e.g. mutating a global variable, or static variable, or printing things/generates output). The state has changed in an irreversible way.

So C::f() is probably not exception safe, basic guarantee at best, no guarantee if calling a.h() without b.h() violates class C's invariants.

If A::g and B::g have no non-local side effects. We can fix this using an old idiom: copy and swap.

class C{
    A a;
    B b;
  public:
    void f() {
        A atemp = a;
        B btemp = b;
        atemp.g();
        btemp.h();
        // if either of these throws, the originals are still intact
        a = atemp;
        b = btemp;	// but what if copy assignment throws?
        
	}
};

Because copy assignment operator could throw, we don't have exception safety yet. (if b = btemp; throws , a has already been modified). It would be better if we could guarantee the "swap" put was a no-throw operation. A non-throwing swap is at the heart of exception safety in C++.

Observation: Copying pointers never throw

If we cannot guarantee that the assignment operation won't throw an exception, then a very good solution is to use the Pimpl Idiom:

struct CImpl {
    A a;
    B b;
};


class C {
    // solution: use the pimpl idiom
    unique_ptr<CImpl> pImpl;
    void f() {
        auto temp = make_unique<CImpl>(*pImpl); // just dereference the unique ptr and make a new one.
        temp->a.g();
        temp->b.h();
        std::swap(pImpl, temp);	// no-throw
    }
};

We are making a temporary copy of the data of our object (because make_unique<CImpl>(*pImpl) passes *pImpl as the constructor parameter, so it's invoking the copy constructor) and invoking A::g and B::h on the copy. So, if an exception is thrown by any of those methods, we don't have to worry because our object is still not modified. After both methods finish executing, we just swap the pointers between our pImpl field and the temp object.

The pointer swap operation never throws an exception because it's just a swap of the memory addresses stored on each one of the already-allocated variables.

Now exception safe. If A::g does not have non-local side effects.

After we done all of this, C::f() offers the strong guarantee: if it executes normally, all the modifications to A and B will be permanent, if it throws an exception at any moment, the temp object will automatically destroyed as part of stack unwinding, but our object's pImpl data will remain unchanged, as if f had never been executed.

Note that the pimpl idiom is not the only way to accomplish this, it is only one of the possible ways.

The point is that a pointer swap operation never throws an exception. So, if you have pointers to objects, you can always create new pointers to temporary copies, modify the copies, and finally swap the original pointers with the copies. Although using pImpl is an option, you can also just have regular pointers or smart pointers to the objects you need and make it work without using pImpl.

In fact, pImpl may not be the best solution if your data includes a collection (vector, map, etc.) because making a temporary copy of everything would mean copying the whole collection, which could be inefficient if the collection contains a large number of elements and only a few of them need to be modified. If that's the case, it would be better to just make copies of the objects you need to modify instead of using pImpl and copying everything.

Generally, a function or method can only offer a strong guarantee if all the functions or methods that it calls offer a strong or a no-throw guarantee.

When a function or method offers a strong guarantee, you should always document it.

No-throw Guarantee

(From course notes)

Every function in C++ is either no-throwing or potentially throwing.

Non-throwing functions guarantee that they will never throw or propograte an exception. Therefore, if an exception is thrown by a non-throwing function, the program is automatically terminated.

In general, the default constructor, copy constructor, move constructor, copy assignment operator, move assignment operator, and destructor are non-throwing, although there are some exceptions to this rule.

Any other function will be potentially throwing unless you declare it with noexcept

void f() noexcept;	// the function f() does not throw

You can also pass an expression to noexcept. true means function is non-throwing

void f() noexcept(true); // the function f() does not throw
void f() noexcpet(false); // the function f() is potentially throwing

When you're writing a function that you know that can never throw an exception, it is a good idea to declare it with noexcept. Using non-throwing functions allow other function to also offer the no-throw or strong guarantee.

Example:

class MyClass {
    int x;
  public:
    int getX() const noexcept {	// does not change anything, so never throws
        return x;
    }
    void setX(int v) noexcept { // only copies an int value, so never throws
        x = v;
    }
};

Pay special attention to the move constructor and move assignment operator. If all they do is swap basic values or pointers, they will never throw an exception. If that's the case, always declare them with noexcept. Doing so allows collection classes such as std::vector to be more efficient when storing objects of that class.

Exception Safety and STL vectors (March 31)

STL vectors:

  • encapsulates a heap-allocated array
  • follow RAII - when a stack-allocated vector goes out of scope, the internal heap-allocated array is freed (doesn't mean all members of the array is freed, but the array itself is freed) (you can use unique ptr or shared ptr to solve this problem)
void f() {
    vector<MyClass> v;
    ...
} // here, v goes out of scope, array is freed, MyClass objects all are destroyed, so their destructors run

BUT,

void g() {
    vector<MyClass*> v;
    ...
    v.emplace_back(new MyClass{...});
    ...
} // v goes out of scope, array of pointers is freed, but pointers don't have destructors, MyClass objects are leaked

So, you have to free them manually, e.g.

for (auto &p :v) delete p;

Or, don't use raw pointers, and use smart pointers

void h() {
    vector<unique_ptr<MyClass>> v;
    ...
} // if v goes out of scope, array is freed, unique_ptr objects are in the array, so they are destroyed, their destructors run, their destructors delete the MyClass objects
// no memory leaks!

Therefore, using vectors of smart pointers instead of vectors of regular pointers is a good way to write exception-safe functions.

Consider now the method vector<T>::emplace_back

  • offers the strong guarantee
  • if the array is full (i.e., if size == cap(acity)), it
    • allocate a new, larger array
    • copy objects over (copy ctor)
      • if a copy ctor throws (strong guarantee)
        • destroy the new array,
        • the old array still intact
      • if it doesn't throw
        • delete the old array, replace it with the new, larger array

But, copying is expensive, and if successful, the old data will be thrown away.

Wouldn't moving from the old array to the new array be more efficient?

  • Allocate new larger array
  • move objects over (move constructor)
  • delete old array

The problem: if the move constructor throws, then emplace_back can't offer the strong guarantee because old array no longer intact. But emplace_back does offer it (a strong guarantee).

Therefore, if the move ctor offers the no-throw guarantee, emplace_back will move objects. Otherwise, it will use the copy ctor, which may be slower.

So, your move operation should provide the no-throw guarantee, if possible, and you should indicate that they do.

class MyClass {
  public:
    MyClass( MyClass &&other ) noexcept {...}; // noexcept tells compiler that the function does not throw
    MyClass &operator=( MyClass &&other ) noexcept {...};
};

If you do this, then whenever vector::emplace_back needs to resize its dynamic array for more capacity, it can use your classes' move operations to quickly move all the data from the old array to the new, larger array, and still guarantee a strong exception safety.

In general, if you follow the pImpl idiom, writing non-throwing moves and swaps is trivial.

If you know a function will never throw or propagate an exception, declare it as noexcept. This facilitates optimization. At a minimum, moves and swaps should be noexcept.

Casting (March 31)

Casts allow you to convert a piece of data from one type to another.

In C:

Node n;
int i = (int) n;
int *ip = (int *) &n; // a cast, force c++ to treat a Node * as an int *;
//		c-style cast

Casts should be avoided, and in particular, C-style cast should be avoided in C++. If you must cast, use a C++ style cast.

OOP creats relationships between types that do not exist in C, e.g., an A* may be a perfectly valid B* is A and B are related types.

static_cast : for well-defined "sensible casts" examples:

  • double to an int

    double d = 0.5;
    void f(int x);
    void f(double d);
    f(static_cast<int>(d)); // calls the int version of f
  • superclass pointer to derived class pointer

    Book *b = new Text(...);
    Text *t = static_cast<Text *>(b);

    You are taking responsibility that b actually points at a text... if you're wrong, you're lying to the compiler... so not a good idea except in cases like above.

static_cast is a promise by the programmer to the compiler that we know the type we're casting to is correct, and a promise by the compiler to the programmer that, so long as that's true, the resulting pointer will behave correctly.

Consider the assignment operator problem:

class Book {
    int length;
    string title;
    string author;
  public:
    ...
    virtual Book &operator=(const Book &o) {
        length = o.length;
        title = o.title;
        author = o.author;
        return *this;
    }
};

class Comic {
    string hero;
  public:
   	...
    Comic &operator=(const Book &o) override {
        Book::operator=(o);
        Comic &c = static_cast<Comic &>(o);
        hero = c.hero;	// what if c doesn't point at a comic???
        return *this;	// No good!
	}
};
  

dynamic_cast : dynamic_cast is a checked cast

is it safe to cast a Book * to a Comic * ?

Book *pb = ...;
Comic *pc = dynamic_cast<Comic *>(pb);

What is the value of pc?

It depends on what pc actually points at. If pb points at a Comic, then pc == pb, else pc == ullptr.

If pb actually points at a comic, the dynamic_cast returns back the original pointer, if not, it returns the nullptr! (makes it possible to check at runtime whether the type was correct)

dynamic_cast works on pointer and references. If the reference cast fails, it throws an exception.

We now fix our code: Do the cast first, if it throws, it throws.

class Comic {
    string hero;
  public:
   	...
    Comic &operator=(const Book &o) override {
        Comic &c = dynamic_cast<Comic &>(o);
        Book::operator=(o);
        
        hero = c.hero;
        return *this;
	}
};

Even though we can implement a polymorphic assignment operator now, we haven't solved the problem. We just move the issue for the client to deal with by raising an exception.

So in general, polymorphic assignment still doesn't make sense. We still prefer, unless absolutely necessary, to disallow it as before. (Just because we can do something, doesn't mean we should).

dynamic_cast only works for types that have at least one virtual method, the compiler must be able to access the virtual function table pointer in the object to access run-time type information.

We also have:

  • reinterpret_cast: is not really a step up from C-style casts. It is unsafe, implementation-dependent, weird conversions. Most uses of it result in undefined behaviour. Just don't do it! Zero reason to use it in this class.
  • reinterpret_cast causes a value to be reinterpreted as another type.
Student s;
Turtle *t = reinterpret_cast<Turtle *>(&s);
// force a student * to be treated as a turtle *

t->beStruckBy( Stick{...} );	// makes no sense, undefined behaviour
  • const_cast is for converting between const and non-const. It is the only C++-style cast that can cast away constness.
void g(int *p) {
    // suppose we know g does not actually modify &p (the int)
}

void f(const int *p) {
    g(const_cast<int *>(p));	// you can cast the constness away if you are certain that the ptr won't modify the value
}

But, these operations, static_cast, dynamic_cast, or const_cast, have been working on raw pointers.

Can these work on smart pointers? Yes! There are special versions in header <memory>. Those are called

  • static_pointer_cast

  • dynamic_pointer_cast

  • const_pointer_cast.

They can cast shared_ptrs to shared_ptrs.

Warning: dynamic_cast: We can now do things like,

void whatIsIt(shared_ptr<Book> b) {
    if (dynamic_pointer_cast<Comic>(b)) cout << "Comic";
    else if (dynamic_pointer_cast<Text>(b)) cout << "Text";
    else cout << "Normal Book";
}
// there is a runtime cost to dynamic type checking, and to reference counting

Code like this is highly coupled to the Book hierarchy. This likely indicates bad design. If your design is good, you should rarely, if ever, need to use dynamic_cast to identify run-time types. It's just NOT a good idea.

There is no equivalent for unique_ptrs, since unique_ptrs's uniqueness make them less likely to lose type information, and thus less likely to require recovering that type information with a cast.

Fixing the polymorphic assignment problem with dynamic casting

Recall:

We introduced three different options to deal with the copy/move assignment operatins together with inheritance

  1. Public non-virtual operations do not restrict what the programmer can do but allow partial assignment
  2. Public virtual operations do not restrict what the programmer can do but allow mixed assignment
  3. Protected operations in an abstract superclass that are called by the public operations in the concrete subclasses prevent partial and mixed assignments, but prevent the programmer from making assignments using base class pointers

However, our example for option 2 (public virtual operations) created another problem: the program would crash if the programmer attempted to do a mixed assignment, like:

Text t {...};
Comic c {...};
t = c;	// Use Comic object to assign Text object, REALLY BAD

There is a better way to implement the copy/move assignment operators that allows the program to better deal with the error above. We need to use dynamic casting and exception handlers.

Remember that when implementing the copy/move operators polymorphically, we need to pass the right side object as a reference to an object with the type of the superclass.

We can use a dynamic cast to safely attempt to treat the object passed by reference as the parameter to the copy/move assignment operators, which is of type Book&, as a Text&. If the object passed as parameter is not an instance of a Text, the dynamic cast will throw a std::bad_cast exception. So, this is how the implementation of the two methods would look like:

Text &Text::operator=( const Book &rhs ) {
    if (this == &rhs) return *this;
    Book::operator=(rhs);
    // Attempt to treat rhs as a Text object using a dynamic_cast
    // If rhs is not a Text, an exception will then thrown
    const Text &rhst = dynamic_cast<const Text&>(rhs);
    topic = rhst.topic;
    return *this;
}

Text &Text::operator( Book &&rhs ) {
    if (this == &rhs) return *this;
    Book::operator=(std::move(rhs));
    // Attempt to treat rhs as a Text object using a dynamic_cast
    // If rhs is not a Text, an exception will then thrown
    Text &rhst = dynamic_cast<Text&>(rhs);
    topic = std::move(rhst.topic);
    return *this;
}

Note that this fix will not completely eliminate the mixed assignment issue. If the programmer tries to do a mixed assignment (e.g., by trying to use a Book of Comic object to assign it into a Text variable), the compiler will not detect the problem. However, the exception std::bad_cast will be thrown when the dynamic cast is attempted.

But the advantage of this solution is that the exception can now be handled, so the program can recover as appropriate instead of crashing:

// Trying to assign a Text from a Book will raise a std::bad_cast exception
try {
    Book b1("Programming for Beginners", "Niklaus Wirth", 200);
    Text t2("Programming for Kids", "Bjarne Stroustrup", 300, "C++");
    Book *pb1 = &b1;
    Book *pb2 = &t2;
    *pb2 = *pb1; // std::bad_cast will be thrown
    printTextBook(t2, "Text 2");
} catch (std::bad_cast r) {
    cerr << "Error trying to assign a Text object" << r.what() << endl;
}