Beginner's Guide
    First Steps
    Input & Output
    Custom Types – Part 1
    Diagnostics
    Standard Library – Part 1
    Function Objects
    Standard Library – Part 2
    Code Organization
    Custom Types – Part 2
    Generic Programming
    Memory Management
    Software Design Basics

    Basic Custom Types / Classes Basic Custom Types Classes

    Fundamental Types
    void, bool, char, int, double, …
    Simple Aggregates

    Main Purpose: grouping data

    • aggregate: may contain one/many fundamental or other aggregate-compatible types
    • no control over interplay of constituent types
    • trivial if only (compiler generated) default construction / destruction / copy / assignment
    • standard memory layout (all members laid out contiguous in declaration order), if all members have same access control (e.g. all public)
    More Complex Custom Types

    Main Purpose: enabling correctness/safety guarantees

    • custom invariants and control over interplay of members
    • restricted member access
    • member functions
    • user-defined construction / member initialization
    • user-defined destruction / copy / assignment
    • may be polymorphic (contain virtual member functions)

    Why Custom Types? Why?

    • invariants = behavior and/or data properties that never change
    • avoid data corruption by controlling/restricting access to data members
    • restrict input/output values of functions by using dedicated types
    • easy-to-use interfaces that hide low-level implementation details
    • stable interfaces that are not affected by changing internal implementations
    • reusable abstractions for commonly needed facilities (e.g., dynamic array)

    also called RAII (Resource Acquisition Is Initialization)

    • acquire some resource (memory, file handle, connection, …) when object is constructed
    • release/clean up resource when object is destroyed (de-allocate memory, close connection, …)

    Motivation: Monotonous Counter Motivation 1

    • stores an integer
    • is initialized with 0
    • Invariant: count can only increase (cannot be decreased or reset)
    monotonous_counter c;   
    cout << c.reading();  // prints 0
    c.increment();
    cout << c.reading();  // prints 1
    c.increment();
    c.increment();
    cout << c.reading();  // prints 3
    struct frail_counter {
      int count;
    };
    
    frail_counter c; cout << c.count; // any value c.count++; c.count = 11;
    • integer member not automatically initialized to 0
    • one can freely modify any integer member of an aggregate
    ⇒ no advantage over just using an integer

    Motivation: Ascending Sequence Motivation 2

    • should store integers
    • Invariant: number of elements can only increase, i.e., one can only insert new elements, but not remove them
    • Invariant: elements must always be sorted in ascending order
    ascending_sequence s;  
    s.insert(5);
    • 5
    s.insert(-8);
    • -8
    • 5
    s.insert(42);
    • -8
    • 5
    • 42
    cout << s.size(); // prints 3 cout << s[0]; // prints -8 cout << s[2]; // prints 42
    struct chaotic_sequence {
      std::vector<int> nums;
    };
    
    chaotic_sequence s;
    s.nums.push_back(8);
    • 8
    s.nums.push_back(1);
    • 8
    • 1
    s.nums.push_back(4);
    • 8
    • 1
    • 4
    s.nums.pop_back(4);
    • 8
    • 1
    can violate requirements
    • numbers not necessarily sorted in ascending order
    • we can manipulate num without restriction (delete numbers, etc.)
    ⇒ no advantage over just using a plain std::vector

    Restricted Member Access Member Access Access

    Member Functions

    class monotonous_counter {
      int count_;  // ← data member
    
      void increment () {  // ← member function
        ++count_; 
      }
    };
    class ascending_sequence {
      std::vector<int> seq_;  // ← data member
    
      void insert (int x) {   // ← member function
        // insert x into nums    // at the right position
      }
    };
    • manipulate or query data members
    • control/restrict access to data members
    • hide low-level implementation details
    • ensure correctness: keep/guarantee invariants
    • ensure clarity: well-structured interfaces for users of type
    • ensure stability: internal data representation (mostly) independent from interface
    • avoid repetition/boilerplate: only one call necessary for potentially complex operations

    public vs. private Visibility public vs. private public/private

    Private members are only accessible through member functions:
    class ascending_sequence {
    private:
      std::vector<int> seq_;
      // … more private members
    public:
      void insert (int x) {  }
      auto size () const { return seq_.size(); }
      // … more public members
    };
    
    int main () { ascending_sequence s; s.insert(8); // 'insert' is public auto n = s.size(); // 'size' is public auto i = s.seq_[0]; // COMPILER ERROR: 'seq_' is private auto m = s.seq_.size(); // COMPILER ERROR s.seq_.push_back(1); // COMPILER ERROR }
    struct vs. class – main difference is default visibility:
    keyword usually used for
    struct simple aggregates of public data
    class private data, member functions, invariants, …

    const Member Functions const Members

    class ascending_sequence {
      std::vector<int> seq_;
    public:  
      void insert {  }
      auto size () const { return seq_.size(); }
    };
    
    int main () { ascending_sequence s; s.insert(88); // s is not const auto const& cs = s; cs.insert(5); // COMPILER ERROR: 'insert' is not const }

    A function taking a const(reference) parameter not only promises not to modify it, this promise is checked & enforced by the compiler:

    void foo (ascending_sequence const& s) {
      // 's' is const reference ^^^^^
      auto n = s.size();  //  'size' is const
      s.insert(5);  //  COMPILER ERROR: 'insert' is not const
    }
    class monotonous_counter {
      int count_;
    public:  
      int reading () const { 
        //  COMPILER ERROR: count_ is const:
        count_ += 2;
        return count_;
      }
    };
    class ascending_sequence {
      std::vector<int> seq_;
    public:  
      auto size () const {  // 'seq_' is const
        //  COMPILER ERROR: calling non-const 'push_back'
        seq_.push_back(0);  
    
        //  vector's member 'size()' is const
        return seq_.size();  
      }
    };

    Two member functions can have the same name (and parameter lists) if one is const-qualified and the other one isn't. This makes it possible to clearly distinguish read-only access from read/write actions.

    class interpolation { 
      int t_;
      
    public:
      
      // setter/getter pair:
      void threshold (int t)  { if (t > 0) t_ = t; }
      int  threshold () const { return t_; }
      // write access to a 'node'
      node&       at (int x) {  }
      // read-only access to a 'node'
      node const& at (int x) const {  }
    };

    Member Declaration vs. Definition Declaration vs. Definition Declarations

    class MyType {
      int n_;
      // more data members …
    public:
      // declaration + inline definition
      int count () const { return n_; } 
      // declaration only
      double foo (int, int);
    };
    
    // separate definition double MyType::foo (int x, int y) { // lots of stuff … }

    Definitions of complex member functions are usually put outside of the class (and into a separate source file).

    However, small member functions like interface adapter functions, getters (like count) should be implemented inline, i.e., directly in the class body for maximum performance.

    For now we will keep all member functions inline until we learn about Separate Compilation

    Operator Member Functions Operator Functions Operators

    special member function
    class X { …
      Y operator [] (int i) { … } 
    };
    enables the subscript operator:
    X x;
    Y y = x[0];
    class ascending_sequence {
    private:
      std::vector<int> seq_;
    public: 
      void insert (int x) {  }
      int operator [] (size_t index) const {    return seq_[index];  }
    };
    
    int main () { ascending_sequence s; // s.seq_:
    •  
    s.insert(9); // s.seq_:
    • 9
    s.insert(2); // s.seq_:
    • 2
    • 9
    s.insert(4); // s.seq_:
    • 2
    • 4
    • 9
    cout << s[0] << '\n'; // prints '2' cout << s[1] << '\n'; // prints '4' }

    Initialization

    Member Initialization Data Members

    Member Initializers C++11
    class counter {
      // counter should start at 0
      int count_ = 0;
    public:
      
    };
    class Foo {
      int i_ = 10;
      double x_ = 3.14;
    public:
      
    };
    Constructor Initialization Lists
    • constructor (ctor) = special member function that is executed when an object is created
    class counter {
      int count_;
    public:
      counter(): count_{0} { }
      
    };
    class Foo {
      int i_;     // 1st
      double x_;  // 2nd
    public:    
      Foo(): i_{10}, x_{3.14} { }
      // same order: i_ , x_ 
      
    };

    Make sure that the member order in initialization lists is always the same as the member declaration order!

    A different order in the initialization list might lead to undefined behavior such as accesses to uninitialized memory.

    Some compilers warn about this: g++/clang++ with -Wall or -Wreorder, — yet another reason to always activate and never ignore compiler warnings!

    Constructors

    • constructor (ctor) = special member function that is executed when an object is created
    • constructor's function name = type name
    • has no return type
    • can initialize data members via initialization list
    • can execute code before first usage of an object
    • can be used to establish invariants
    • default constructor = constructor that takes no parameters
    class Samples {
      int min_;
      int max_;
      std::vector v_;
    public:
      // default constructor:
      Samples (): min_{0}, max_{1}, v_{min_,max_} {    v_.reserve(8);  }
      explicit  // special constructor:
      Samples (int x): min_{x}, max_{x}, v_{x} {    v_.reserve(8);  }
      int add (int i)  {     if (i < min_) min_ = i;
        else if (i > max_) max_ = i;
        v_.push_back(i);  }
      int min () const { return min_; }
      int max () const { return max_; }
      
    };
    
    Samples s1; // default ctor // s1.v_:
    • 0
    • 1
    Samples s2 {3}; // special ctor // s2.v_:
    • 3
    Separate Definition of Constructors

    works the same as for other member functions

    class MyType { 
    public:
      MyType ();  // declaration
      
    };
    // separate definition
    MyType::MyType ():  {  }

    Make sure that the member order in initialization lists is always the same as the member declaration order!

    A different order in the initialization list might lead to undefined behavior such as accesses to uninitialized memory.

    Here, in the default constructor we need to make sure to access min_ and max_ in v_{min_,max_} only after they have been initialized.

    Some compilers warn about this: g++/clang++ with -Wall or -Wreorder, — yet another reason to always activate and never ignore compiler warnings!

    Default vs. Custom Constructors Custom Constructors Custom

    class BoringType { public: int i = 0; };
    BoringType obj1;     // 
    BoringType obj2 {};  // 
    class SomeType { 
    public:
      // special constructor:
      explicit SomeType (int x)  {  }
    };
    SomeType s1 {1};  //  special (int) constructor
    SomeType s2;      //  COMPILER ERROR: no default constructor!
    SomeType s3 {};   //  COMPILER ERROR: no default constructor!
    class MyType { 
    public:
      MyType () = default;
      // special constructor:
      explicit MyType (int x)  {  }
    };
    MyType m1 {1};  //  special (int) constructor
    MyType m2;      // 
    MyType m3 {};   // 
    If you use = default, make sure to initialize data members with member initializers.

    Explicit Constructors ↔ Implicit Conversions Explicit Constructors explicit

    // functions with a 'Counter' parameter
    void foo (Counter c) {  }
    void bar (Counter const& c) {  }
    Make user-defined constructors explicit by default!

    Implicit conversions are a major source of hard-to-find bugs!

    Only use non-explicit constructors, if direct conversions from the parameter type(s) is absolutely needed and has an unambiguous meaning.

    Some older teaching materials and people coming from C++98 might tell you that one only needs to worry about implicit conversions for single-parameter constructors. This is no longer the case as of C++11 since one can also implicitly construct objects from braced lists of values.

    Constructor Delegation Delegation

    C++11
    = call other constructor in an initialization list
    class Range {
      int a_;
      int b_;
    public:
      // 1) special constructor
      explicit   Range (int a, int b): a_{a}, b_{b} {
        if (b_ > a_) std::swap(a_,b_);
      }
      // 2) special [a,a] constructor - delegates to [a,b] ctor
      explicit Range (int a): Range{a,a} {}
      // 3) default constructor - delegates to [a,a] ctor
      Range (): Range{0} {}
      
    };
    
    Range r1; // 3) ⇒ r1.a_: 0 r1.b_: 0 Range r2 {3}; // 2) ⇒ r2.a_: 3 r2.b_: 3 Range r3 {4,9}; // 1) ⇒ r3.a_: 4 r3.b_: 9 Range r4 {8,2}; // 1) ⇒ r4.a_: 2 r4.b_: 8

    The Most Vexing Parse Vexing Parse

    Can't use empty parentheses for object construction due to an ambiguity in C++'s grammar:

    class A { … };
    
    A a (); // declares function 'a' // without parameters // and return type 'A'
    A a; // constructs an object of type A
    A a {}; // constructs an object of type A

    Design, Conventions & Style Design & Style Design

    General Guidelines Guidelines

    • Each type should have exactly one purpose

      because it reduces the likelihood of future modifications to it.

      • reduced risk of new bugs
      • keeps code depending on your type more stable
    • Keep data members private & use member functions to access/modify data

      so that one can only interact with your type through a stable interface.

      • avoids data corruption / allows guarantees of invariants
      • users of your type don't need to change their code if you change the type's internal implementation
    • const-qualify all non-modifying member functions

      in order to clearly advertise how and when the internal state of an object changes.

      • makes it harder to use your type incorrectly
      • enables compiler mutability checks
      • better reasoning about correctness, especially in scenarios with concurrent access to objects, e.g., from multiple threads.

    Interfaces should be easy to use correctly and hard to use incorrectly.
     —  Scott Meyers

    Users of a function or type should not be confused about its purpose, the meaning of parameters, pre/postconditions and side effects.

    Types in Interfaces

    #include <cstdint>
    #include <numeric_limits>
    class monotonous_counter {
    public:
      // public type alias
      using value_type = std::uint64_t;
    private:
      value_type count_ = 0;
    public:
      value_type reading () const { return count_; }
      
    };
    
    const auto max = std::numeric_limits<monotonous_counter::value_type>::max();
    Don't leak implementation details:
    • Only make type aliases public, if the aliased types are used in the public interface of your class, i.e., used as return types or parameters of public member functions.
    • Do not make type aliases public if the aliased types are only used in private member functions or for private data members.

    Member vs. Non-Member Member/Non-Member (Non-)Member?

    How to implement a feature / add new functionality?
    • only need to access public data (e.g. via member functions) ⇒ implement as free standing function
    • need to access private data ⇒ implement as member function
    Example: interval-like type gap

    How to implement a function that makes a new gap object with both bounds shifted by the same amount?

    class gap {
      int a_; 
      int b_;
    public:
      explicit   gap (int a, int b): a_{a}, b_{b} {}
      int a () const { return a_; }
      int b () const { return b_; }
    };

    Avoid Setter/Getter Pairs! Avoid Setters!

    • use action / verb functions instead of just setters
    • usually models problems better
    • more fine-grained control
    • better code readability / expression of intent
    descriptive actions
    class Account { 
      void deposit (Money const&);
      Money try_withdraw (Money const&);
      Money const& balance () const;
    };
    setter/getter pair
    class Account { 
      void set_balance (Money const&); 
      Money const& balance () const;
    };

    Naming

    Do not use leading underscores or double underscores in names of types, variables, functions, private data members, etc.!

    Names beginning with underscores and/or containing double underscores are reserved for the standard library and/or compiler-generated entities.

    Using names with leading underscores or double underscores can invoke undefined behavior!

    A common and unproblematic convention is to use trailing underscores for private data members.

      _member          m_ember          
      member__         member_
      m__ember

    Use Dedicated Types! Correctness

    • to restrict input parameter values
    • to ensure validity of intermediate results
    • to guarantee return value validity

    ⇒ compiler as correctness checker if it compiles, it should be correct

    // unambiguous interface:
    double volume (Cuboid const&);
    // input guarantee: angle is in radians
    Square make_rotated (Square const&,                     Radians angle);
    // input: only valid quantity (e.g. > 0)
    Gadget duplicate (Gadget const& original,                   Quantity times);
    // result guarantee: vector is normalized
    UnitVector3d dominant_direction (WindField const&);
    // avoid confusion with a good units library
    si::kg mass (EllipsoidShell const&,               si::g_cm3 density);
    bool has_cycles (DirectedGraph const&);
    
    // understandable control flow & logic: Taxon species1 = classify(image1); Taxon species2 = classify(image2); Taxon lca = taxonomy.lowest_common_ancestor( species1, species2);

    Example Implementations Complete Examples Example

    Example 1: Monotonous Counter monotonous_counter

    • new counters start at 0
    • can only count up, not down
    • read-only access to current count value
    #include <iostream>   // std::cout
    #include <cstdint>    // std::uint64_t
    class monotonous_counter {
    public:
      using value_type = std::uint64_t;
    private:
      value_type count_ = 0;  // initial
    public:
      monotonous_counter () = default;
      explicit   monotonous_counter (value_type init) noexcept:     count_{init} {}
      void increment () noexcept { ++count_; }
      [[nodiscard]] value_type   reading () const noexcept {     return count_; }
    };
    
    int main () { monotonous_counter c; c.increment(); std::cout << c.reading(); // prints 1 c.increment(); c.increment(); std::cout << c.reading(); // prints 3 }

    Example 2: Ascending Sequence ascending_sequence

    • stores integers
    • read-only access to stored elements by index
    • can only insert new elements, but not remove them
    • elements are always sorted in ascending order
    • content only modifiable through public interface

    The implementation of 'insert' and the role of the 'begin' and 'end' member functions will become much clearer after we have learned about iterators and the algorithms in the standard library.

    #include <iostream>   // std::cout
    #include <vector>     // std::vector
    #include <algorithm>  // std::lower_bound
    class ascending_sequence {
    public:
      using value_type = int;
    private:
      using storage_t = std::vector<value_type>;
      storage_t seq_;
    public:
      using size_type = storage_t::size_type;
      void insert (value_type x) {
        // use binary search to find insert position
        seq_.insert(      std::lower_bound(        seq_.begin(), seq_.end(), x), x);
      }
      [[nodiscard]] value_type   operator [] (size_type idx) const noexcept { 
        return seq_[idx]; }
      [[nodiscard]] size_type   size () const noexcept {     return seq_.size(); }
      // enable range based iteration
      [[nodiscard]] auto   begin () const noexcept {     return seq_.begin(); }
      [[nodiscard]] auto   end ()   const noexcept {     return seq_.end(); }
    };
    
    int main () { ascending_sequence s; // s.seq_:
    s.insert(7); // s.seq_:
    • 7
    s.insert(2); // s.seq_:
    • 2
    • 7
    s.insert(4); // s.seq_:
    • 2
    • 4
    • 7
    s.insert(9); // s.seq_:
    • 2
    • 4
    • 7
    • 9
    s.insert(5); // s.seq_:
    • 2
    • 4
    • 5
    • 7
    • 9
    std::cout << s[3]; // prints 7 for (auto x : s) { std::cout << x <<' '; // 2 4 5 7 9 } // use type aliases ascending_sequence::value_type x = 1; ascending_sequence::size_type n = 2; }