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

    Simple Custom TypesSimple Custom TypesClasses

    Fundamental Types
    void, bool, char, int, double, …
    Aggregate Types
    • may contain one/many fundamental or other types
    • default construction / destruction / copy / assignment
    • standard memory layout
    • unrestricted member access
    • no user-defined member initialization
    Non-Trivial Types

    may have/be

    • member functions / restricted member access
    • user-defined construction / member initialization
    • user-defined destruction / copy / assignment
    • non-standard memory layout
    • polymorphic (contain virtual member functions)

    Why?

    • invariants = behavior and/or data properties that never change
    • avoid data corruption by controlling/restricting access to data members
    • 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 or clean up resource when object is destroyed (de-allocate memory, close connection, …)

    Example 1: Monotonous Counter Example 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

    Example 2: Ascending Sequence Example 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 neccessarily 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
    • corectness: keep/guarantee invariants
    • clearity: well-structured interfaces for users of type
    • stability: internal data representation (mostly) independed from interface
    • avoids 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:
    struct point {
    
    
    };

    =

    class point {
    public:
    
    };
    class point {
    
    
    };

    =

    struct point {
    private:
    
    };
    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 Initialization Init

    Member Initialization Data Members

    class counter {
      int count_;
    public:
      int reading() { return count_; }
    };
    
    counter c; // 'count_' not initialized! cout << c.reading(); // can be anything!
    Member Initializers
    class counter {
      // counter should start at 0
      int count_ = 0;
    public:
      
    };
    class Foo {
      int i_ = 10;
      double x_ = 3.14;
    public:
      
    };
    Constructor Initilization 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 initialzation 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<int> 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 initialzation 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!

    Constructor Delegation Delegation

    = 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

    Type 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 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 withdraw(Money const&);
      Money const& balance() const;
    };
    setter/getter pair
    class Account { 
      void balance(Money const&); 
      Money const& balance() const;
    };

    Naming

    Understandable
    class IPv6_Address {};
    class ThreadPool {};
    class cuboid {};
    
    double volume(cuboid const&) {}
    Too generic
    class Manager {};
    class Starter {};
    class Pool {};
    
    int get_number(Pool const&) {}
    Example Style 1
    class type_name {};
    int free_function() {}
    int member_function() {}
    int localVariable;
    int memberVariable_;
    Example Style 2
    class TypeName {};
    int free_function() {}
    int memberFunction() {}
    int localVariable;
    int memberVariable_;
    Do not use leading underscores in names of types, variables, functions, private data members, etc.!

    Names beginning with underscores are reserved for the standard library and/or compiler-generated entities! Using names with leading underscores can lead to collisions and will in many cases invoke undefined behavior.

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

      _member            member_

    Example Implementations Complete Examples Examples

    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): count_{init} {} void increment() { ++count_; } value_type reading() const { 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 modifyable 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); } value_type operator [] (size_type index) const { return seq_[index]; } size_type size() const { return seq_.size(); } // enable range based iteration auto begin() const { return seq_.begin(); } auto end() const { 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; }