Programming in C++

Introduction to Object Oriented Programming (OOP)

Classes and Encapsulation

Gerald Senarclens de Grancy

Problems of a Purely Procedural Approach

Using most container types, it isn't obvious what the elements represent

std::vector<double> circle{25, 15, 7};  // what is the radius?

Using a map, we cannot mix data types

std::map<std::string, std::string> student {
  {"name", "Pat"},
  {"age", "23"},  // age is represented as string
};

If data integrity is not guaranteed, nasty bugs happen

struct Circle { double x, y, radius; };
Circle c { .x = 25, .y = 15, .radius = -7};  // invalid radius

The inability to guarantee the validity of an object is likely the worst downside of purely procedural approaches

Advantages of Object Oriented Programming (OOP)

In the real world, there is an infinite variation of data types
OOP enables you to model your own datatypes

  • Data consistency (validity) can be ensured via encapsulation
  • We usually think in terms of classes and concrete objects
    ⇒ map the objects in the problem domain to those in the program
  • Problems can be broken into smaller parts that can be solved easier
  • Classes are self-contained code units that facilitate maintenance
  • New ways of code reuse through advanced OOP techniques
  • OOP permits the use of design patterns for larger software systems

Terms

class
User defined data type
Provides data members/ fields (may be hidden)
Behavior is defined by the class' methods
Instance / object
Variables with the type of a class are called objects or instances
Classes can be thought of as object factories
Method
Function (or procedure) associated with a class
Public methods constitute the class' interface (API)
Class invariant
The invariant constrains the state stored in the object
Methods of the class have to preserve the invariant

Defining a C++ class

class ClassName {
public:  // private is the default for classes (public for structs)
  ClassName(.);  // constructors are supported
  ~ClassName(.);  // destructors are supported
  type member1;
  type member2;  // declare as many properties as needed
  type method1(param1, ...);
  type method2(.);  // declare as many methods as needed

private:  // these are only accessible within the class
  type priv_member_1_;
  type priv_member_2_;  // declare as many private properties as desired
  type priv_method1(param1, ...);
  type priv_method2(.);  // declare as many private methods as desired

};

Related Conventions

Be consitent (eg. C++ Core Guidelines or Google C++ Style Guide)

Always explain the reasons for your decisions

NL.16 (NL: Naming and layout suggestions)
Use the public before protected before private order
Variable Names
Private data members of classes with a trailing underscore
C.48, C.49
Prefer in-class initializers to member initializers in constructors for constant initializers
Prefer initialization to assignment in constructors

Encapsulation

Used to hide the state of a structured object inside a class

Prevents direct access to the data members by clients

Ensures that clients cannot violate the class invariant

The invariant can be tested

Access Control, C.133, C.134
Make classes' data members private, unless they are constants.

Example: class Circle

#include <iostream>
class Circle {
public:
  Circle() = default;  // use the compiler-generated default constructor
  Circle(double x, double y, double radius) : x{x}, y{y} {
    this->radius(radius);  // `this` allows to reuse `radius` as parameter
  }
  double x {0.0};  // use public member instead of trivial getters / setters
  double y {0.0};
  double radius() const { return radius_; }  // getter;
  void radius(double radius) {  // setter ensures a valid circle
    if (radius >= 0) { radius_ = radius; return; }
    std::cerr << "not allowed to set negative radius" << std::endl;
  }
  double area() const;
  double circumference() const;
private:
  double radius_ {1.0};  // default value is better than ctor default argument
};  // end class definition with a semicolon

int main() {
  Circle c;
  std::cout << "radius: " << c.radius() << std::endl;
  c.radius(1.5);
  std::cout << "radius: " << c.radius() << std::endl;
  c.radius(-1);
  std::cout << "radius: " << c.radius() << std::endl;
  Circle c2 {0, 0, -1};
  std::cout << "c2.radius: " << c2.radius() << std::endl;
}

Visualize execution

Separate Declaration and Definition

To use a class in other files, declare it in a header file

Classes are usually also defined in header files

Class methods are declared in the class definition but defined in a designated source file

C++ Core Guidelines

  • Explicit distinction between interface (*.hpp) and implementation (*.cpp) improves readability and simplifies maintenance

Example: Header File Contains the API

#ifndef INT_VECTOR_HPP
#define INT_VECTOR_HPP
#include <iostream>

namespace ds {  // namespace for our own data structures (ds)
class IntVector {  // very simplified int vector class
public:
  IntVector() = default;  // compiler generated default constructor
  ~IntVector() { delete[] elements_; }  // destructor
  IntVector(const IntVector&) = delete;  // no copy constructor
  IntVector& operator=(const IntVector& other) = delete;  // no copy assignment
  void push_back(int value);
  int pop_back();
  std::size_t size() { return size_; }  // optimized by compiler if in header
private:
  std::size_t size_ = 0;
  std::size_t space_ = 0;
  int* elements_ = nullptr;
  void resize(std::size_t new_space);
};
}
#endif  // INT_VECTOR_HPP
Download int_vector.hpp

Example: Source File

#include "int_vector.hpp"
#include <algorithm>
#include <iostream>

namespace ds {

void IntVector::resize(std::size_t new_space) {
  if (new_space <= space_) return;  // never shrink
  int* new_elements = new int[new_space];
  std::copy(elements_, elements_ + size_, new_elements);
  delete[] elements_;
  elements_ = new_elements;
  space_ = new_space;
}

void IntVector::push_back(int value) {
  if (space_ == 0) {
    resize(8);  // default size is 8
  } else if (space_ == size_) {
    resize(space_ * 2);
  }
  elements_[size_] = value;
  ++size_;
}

int IntVector::pop_back() {
  if (!size_) {
    std::cerr << "ERROR: cannot pop element of empty Vector" << std::endl;
    return 0;
  }
  --size_;
  return elements_[size_];
}

}
Download int_vector.cpp

Example: Use the class

The class can be used from any other file

#include "int_vector.hpp"
#include <iostream>

int main(void) {
  ds::IntVector v;
  v.push_back(5);
  v.push_back(1);
  v.push_back(10);
  std::cout << "last element: " << v.pop_back() << std::endl;
  std::cout << "last element: " << v.pop_back() << std::endl;
  std::cout << "last element: " << v.pop_back() << std::endl;
  std::cout << "last element: " << v.pop_back() << std::endl;
}
Download int_vector_main.cpp

Compile the program with clang++ int_vector_main.cpp int_vector.cpp

Questions
and feedback...