CS247: Software Engineering Principles

Back to UWaterloo

Readings

Software Engineering is a collection of practices and priciples, tools, techniques, etc. that aims to:

C++

Initialization lists; setting values in body of constructor vs initialization list is slightly different B() : a(3) {}

We cannot leave const variables uninitialized

ADT Design

Operator Overloading

C++ 11 introduced "move" semantics

Non-member

A Non-member function is a critical function of the ADT that is declared outside of the class

istream& operator>> (istream &is, RationalNumber &r) {
int num, den; char slash;
is >> num >> slash >> den;
if (is.fail()) return is;
r = RationalNumber(num, den);
return is;
}


What has to be a member?
- accessors + mutators, constructors, destructor, copy/move
- assignment, [] 
- virtual methods

What cannot be a member?
- I/O operations, or operations that don't want first argunment to be the class type 
  - think stream operations and type conversions
  
We always prefer non-member and non-friend
- less code affected when implementations change
- more secure since they are forced to use accessors & mutators

#### Type Coversions
Type conversions will lose private data

How does a compiler find a conversion function?

1. with exact matches
2. lossless conversion via "promotion" (bool to int, char to int, float to double)
3. standard converson (double to int)
4. user-defined conversions

We have defined some level of precedence, but if there are two or more matches at the same level, the compiler will throw an error because the choice is ambiguous
- if one function is better for at least one argument, and same for the rest, then it will win

Implicit Type Conversions:

class X { public: X(int); X(string) };
class Y { public: Y(int) };
class Z { public: Z(X); };
X f(X);
Y f(Y);
Z g(Z);
X operator+ (X,X);

// now for resolution:
strings { "MACK" };
f(X(1)); // X(1) give X; f recieves X and outputs X; OK
f(1); // ERROR: f is looking for X type, not int
g(X(s)) // OK
g(Z(s)) // OK
```

Header file is effectively global

Apply the override keyword as a sanity check, so that the compiler can catch it

final says you cannot override in a derived class

Attribute-based ADTs

are primarily composed of "virtual data members", accessors and mutators (focused on the information in it)

Value vs. Entity Objects, Information Hiding

Entity has a unique identity (but may have same data), and thus objects are not equal, even with same attribute values

Value object represents a value of an ADT, managing data and immutability

Design of Entity ADTs

An operation on an enitity object should reflect a real-world event

Example: Card game

Mutable Value-based ADTs like Dates can be problematic if they can be referenced from two variables

// fake deep copy + over writing original object data via operator "="

class X {
  public:
  X(const X&) = delete;
  X& operator=(const X&) = delete;
} // how to disable C++11 and up
class Rational::Impl {
  int numerator_, denominator_; // private data fields
  public:
    Impl(int n, int d): numerator_ {n}, denominator_ {d} {} // ...
}

Rational::Rational(int n, int d): // start initializer list
  rat_ {new Rational::Impl {n,d} {
  if(d==0) throw "Panic, denominator=0";
  }

Tutorial

Copy and swap idiom is a way of avoiding code duplication & errors

using namespace std;
  std::swap
  
int a = 1; int b = 2;
swap(a,b);
// a=2; b=1;

// copy and swap
void Node::swap(Node & other) { // reference
  using std::swap;
  swap(data, other.data);
  swap(next,  other.next);
}

Node & Node::operator=(const Node & other) { // returns Node reference
  Node tmp{other};
  swap(tmp);
  return *this;
}

TODO Big three five zero

Special Member Functions

Member functions are provided by default by the compiler

Virtual data member example: class cube stores length, width + height, but also returns surface area and volume

Design example: License plates have 2-8 chars, and are unique

class Point {
    double coeffs[2];
  public:
    Point(): coeffs{} {}; // first sets coeffs to {0,0}, second is body of ctor
    //...

Comipler-Generated Default Constructors + Destructors

when no ctor is explicited declared, compiler will generate a default one based on memberwise initialization

need a destructor if object acquires resources, or maybe log, or inheritance hierarchy -> virtual destructor

Copy constructors make new objects whose value is equal to existing obj, used by compiler to copy objects of the ADT

Money n{m}; //calls copy constructor
Mony p = m; //same as above

Copying with pointers

TODO
Copy Swap idiom; using move semantics -- goal is to provide safe copy

Move Constructor makes new object whose value is equal to existing obj, but does not preserve the value of existing obj

Move assignment

Program decomposition

Why do we decompose? So work can be done independently, faster recompilation, easier resue of components and allows for code evolution

Prefer declarations (forward declarations) over definitions where you can

Global constants declared, possibly in multiple header files, but defined in one location

Cyclical dependencies is a compiler problem

ADT1.cc
#include "ADT1.h"

ADT1.h
#include "ADT2.h"

ADT2.h
#include "ADT1.h"

When must you include? When a compiler must know how much space to allocate (like with inheritance or inline code). We can't fix inheritance but we can turn an object into a pointer/reference and move inlined code to a implmentaiton file

Forward declarations notify compiler that data or function will be declared in the future

Build Process

  1. cpp preprocessor; then sends preprocessed source code
  2. cc1plus compiler; then sends assembly code
  3. as assembler; then sends object code
  4. ld linker; takes libraries and other object files, outputs executable

Extract dependency relationships, as specified by include statements. Then we can determine what needs to be recompiled based on file changes.

Namespaces

Lookup rules:

  1. local
  2. innermost scope, moving outwards
  3. namespace of all function arguments
namespace X {
  int i, j ,k;
}

int k;
void f1() {
  int i = 0; // local
  using namespace X;
  i++; // local i, since namespace is superseded by local declarations
  j++; // X::j
  k += 1; // compiler error, ambiguous
  ::k += 1; // global k
  X::k += 1; // X::k
}

void f2() {
  int i = 0;
  using X::i; 
  using X::j;
  using X::k;'
  i += 1; // using keyword overrides local
  j += 1; // X::j
  k += 1; // X::k overrides global, obviously
}

// not relevant on midterm: header order matters
// header1.h
namespace A {
  int f(double);
}

// header2.h
namespace B {
  using A::f;
  void g();
}

// herader3.h
namespace A {
  int f(int);
}

// main, different include orders:
1,2,3 => f(double) // notice first definition, since 2 doesn't have f(int) signature
2,1,3 => none
1,3,2 => both visible, better match is picked
3,2,1 => f(int)

Week 3: Interface Specification

Week 4: Defensive Programming

Possible Failures

Assertions, use them to document and check assumptions

Exceptions, objects thrown to represent the ocurrene of an error

Type of guarantees: Basic, strong, no throw

Smart Pointers that handle certain classes of problems using more type safety than C pointers and they should be able to do the correct and obvious thing during object deletion

Sink and source idiom for complicated (think factory thats responsible for creating something because it's so complicated). Source creates it, sink absorbs it and takes ownership

Shared pointer, shared_ptr supports shared ownership of a referent

weak_ptr works with a shared_ptr and shares ownership, but does not contribute to the reference count

The RAII Programming Idiom, Resource Acquisition is Initialization: equates resource management with the lifetime of an object

noexcept is a funciton declaration that they will not throw exceptions

Week 5: Reprsentation Invariant, Abstraction Function

Interface specification is a contract betweeen a modules provider and the client programmer, documenting each other's expectations

Example: Set representation as an array, where size of the set is tracked, while the array is of fixed allocation

  1. No duplicate elements: forall i,j : O to size - 1 | i ≠ j => elements[i] ≠ elements[j]
  2. only nullptr above index size: forall i > size-1 | elements[i] == nullptr

Representation invariants for linked list

Linked list implementation note: We can change the representation invariant, affecting what edge cases we need to deal with.

Inductive reasoning: ensure that constructors hold invariance.

Designing an abstraction function is important to know

UML

Software Model, using UML

Design Patterns 3

Composite design pattern gives the client access to all member types through a compund object via a uniform interface

Design Patterns 4: Iterators

Iterator is any object that has the ability to iterate through elements using ++ and dereference

Design Patterns 5: Decorator and Factory

STL Containers

Three main data containers:

  1. emplace
  2. sequence containers: vector deque list forward_list std::array
  3. Container adapters: stack queue priority_queue
  4. ordered associative containers: set map multiset multimap
  5. unordered associative containers: unordered_set unordered_map unordered_multiset
  6. non-member cbegin/cend/rbegin/rend/crbegin/crend

encouraged to define adapter classes that wrap around STL container classes for special needs

Generic Algorithms

STL algorithms usually process a sequence of data elements

C++ Lambdas, bind, mem_fn

C++ Templates

Exam Review

Documentation Example for a set: