Distributed Real-Time Control Systems Lecture 14 Intro to C++ Part III 1
Class Hierarchies The human brain is very efficient in finding common properties to different entities and classify them according to their properties or behaviour. Example: Mammals summarizes a large set of properties common to a class of animals (have dorsal spine, warm blood, feed from mother s milk in infanthood, etc.). This way, many efforts are saved in the classification and attribute assignment to different animals. How can we do this in C++? 2
Designing a Database System Imagine a factory of wooden blocks for kids wants to store their products in a database and be able to display their mass (weight) when queried. Initially the factory produces cylinders and cubes but wants a system that can incorporate new shapes. Cylinder Cube Other?... 3
The Cylinder Class FILE simple_cylinder.h #ifndef SIMPLE_CYLINDER_H #define SIMPLE_CYLINDER_H #include <iostream> //need std::cout using namespace std; constexpr double PI {3.1415; //needed to compute the area of the base class SimpleCylinder { float _r, _h, _d; // private: radius, height and density, resp. public: SimpleCylinder (float r, float h, float d) // constructor definition (inline) : _r {r, _h {h, _d {d { // initialization of member variables cout << Creating SimpleCylinder << endl; ~SimpleCylinder() { // destructor definition (inline) std::cout << Destroying SimpleCylinder << endl; float CalcMass() { //member function (inline) return PI*_r*_r*_h*_d; ; #endif //SIMPLE_CYLINDER_H 4
Sidenote: Const and Constexpr Two ways to define constants: Compile time constant expressions (constexpr) Expression that can be computed at compile time. Use when initializations are time consuming : initialization at compile time are done once, rather than every time the program runs. Run time constants (const) A constant that must be initialized at run time (possibly not known at compile time). Use when need non-modifiable variables to be initialized with run-time data. Const s or Constexpr s are preferred to aliases (#define ) because they have explicit type checking. What is the problem with the following code? #define LENGTH 3... cout << LENGTH/2 << endl; EXAMPLES: const int dmv = 17; //dmv is named constant int var = 17; //var is not a constant constexpr double square(double x) { return x x; //constexpr function constexpr double max1=4 square(dmv); //OK. 4*square(dmv) is a constexpr constexpr double max2= 4 square(var); // error: var is not a constant expression const double max3 = 4 square(var); // OK, may be evaluated at run time 5
The Cube Class FILE simple_cube.h #ifndef SIMPLE_CUBE_H #define SIMPLE_CUBE_H #include <iostream> //need std::cout using namespace std; class SimpleCube{ float _s, _d; // private: side lenght and material density, resp. public: SimpleCube (float s, float d) // constructor definition (inline) : _s {s, _d{d { // initialization of member variables cout << Creating SimpleCube\n << endl; ~SimpleCube() { // destructor definition (inline) std::cout << Destroying SimpleCube << endl; float CalcMass() { //member function (inline) return _s*_s*_s*_d; ; #endif //SIMPLE_CUBE_H 6
Using Cubes and Cylinders FILE simple_blocks.cpp #include <iostream> //need std::cout using namespace std; #include simple_cube.h #include simple_cylinder.h int main() { SimpleCylinder obj1 {2.0,1.0,0.9; SimpleCube obj2 {3.0,0.9; float m1 = obj1.calcmass(); float m2 = obj2.calcmass(); float M = m1+m2; cout << Total Mass << M << endl; Encapsulation: Instead of explicitly computing the mass, our code just asks the class to do the job for us. The client code does not need to know how its mass is computed. The client code does not need to care about how the cylinder class stores data internally. In the future, the class developer can change the class implementation (e.g. to increase efficiency) without breaking the client code. Compile with: g++ -std=c++11 I. simple_blocks.cpp o simple_blocks 7
Problems with this approach To add a new object type, we have to rewrite it from scratch. If we want to add a price field to all the blocks, we have to change all classes. Cannot have a collection of arbitrary blocks because their types may differ. 8
The Hierarchy Object oriented programming help us think about hierarchies of objects, from the more general to the more specific classes. Having a Solid class representing the common things among all blocks, solves the problems mentioned in the previous slide. How? More general - Superclass Solid Cube Sphere More specific - Subclass 9
Inheritance Definition (Inheritance): Inheritance is the mechanism that allows one class A get the properties of another class B. In this case we say that A inherits from B. Objects from class A can access the attributes and methods from class B without having to redefine them. The following definition introduces two terms that deal with the concept of inheritance between classes. Solid (B) Cube (A) Inherit-from Definition (Superclass/Subclass): Case class A inherits from class B, then B is called a Superclass of A. A is denoted the subclass of B. 10
The Superclass Also called base class. All solids have density. Let us also add a Constructor and Destructor; Note 1: The density variable (float _d) is protected. This means clients cannot access this variable but derived classes can. Note 2: A constructor is defined. This deletes the default constructor. Since the provided constructor demandsan argument (the solid density) and does not provide a default value, it is not possible to create an empty Solid object. #ifndef SOLID1_H #define SOLID1_H #include <iostream> using namespace std; FILE solid1.h class Solid1 { protected: //must be used by subclasses float _d; public: Solid1( float d ) : _d {d { cout << Creating Solid1 << endl; ~Solid1() { cout << Destroying Solid1 << endl; ; #endif // SOLID1_H 11
The Subclasses Also called derived classes FILE cylinder1.h #ifndef CYLINDER1_H #define CYLINDER1_H #include solid1.h constexpr double PI {3.1415; class Cylinder1 : public Solid1 { float _r, _h; public: Cylinder1 (float r, float h, float d) : _r {r, _h {h, Solid1 {d { cout<< Creating Cylinder1 <<endl; ~Cylinder1() { cout << DestroyingCylinder1 <<endl; float CalcMass(){return PI*_r*_r*_h*_d; ; #endif //CYLINDER1_H #ifndef CUBE1_H #define CUBE1_H #include solid1.h FILE cube1.h class Cube1 : public Solid1 { float _s; public: Cube1 (float s, float d) : _s {s, Solid1{d { cout << Creating Cube1 <<endl; ~Cube1() { cout << Destroying Cube1 << endl; float CalcMass(){return _s*_s*_s*_d; ; #endif //CUBE1_H 12
Access Protection Inheritance class Cube : public Solid { /* members of Solid maintain the protection level in Cube*/ ; class Cube : protected Solid { /* public members in Solid become protected in Cube, others remain */ ; class Cube : private Solid { /* all members of Solid become private in Cube */ ; It is saidthat class Cube inherits from Solid. Cube can access all members of Solid independently of their protection level. Access public protected private members of the same class members of derived classes not members yes yes yes yes yes no yes no no 13
Base Class Initialization Note the base class is initialized in the constructor of the derived classusing member initializer lists. FILE cylinder.h FILE cube.h... Cylinder (float r, float h, float d) : _r {r, _h {h, //member variable init Solid {d //base class init { std::cout << Creating Cylinder\n ;...... Cube (float s, float d) : _s {s, //member variable init Solid{d //base class init { stc::cout << Creating Cube\n ;... 14
Construction and Destruction of Derived Objects Constructors are invoked in order from the base classes to the derived classes. Destructors are invoked from the derived classes to the base classes. Objects are destroyed in the inverse order of their creation. What is the output of the following code? #include cube1.h #include cylinder1.h int main() { Cube1 obj1 {3.0, 0.9; Cylinder1 obj2 {2.0, 1.0, 0.9; Output: Creating Solid1 Creating Cube1 Creating Solid1 Creating Cylinder1 Destroying Cylinder1 Destroying Solid1 Destroying Cube1 Destroying Solid1 FILE blocks1.cpp 15
Friend Classes and Functions The friend qualifier allows access to protected or private fields to other classes or functions. Only functions or classes declared friend can use private class attributes Let us define a function SameVol that tests if a Cube and a Cylinder have the same volume. This function must access private data of the cylinder and cube objects, so it must be declared friend in the Cylinder and Cube classes. // The friend function bool SameVol(Cube a, Cylinder b) { return a._s*a_s*a._s == PI*b._r*b._r*b._h; class Cube { friend bool SameVol(Cube, Cylinder); ; class Cylinder { friend bool SameVol(Cube, Cylinder); ; 16
References to Objects The reference to an object is a C++ feature. (Not available in C). It can be seen as a constant pointer that is automatically dereferenced. int i =3; int *p = &i; int &j = i; Symbol Type Mem Addr Value i, j int 10000 3 p int * 10004 10000 We say that j is a reference to i. This means to give variable i a second name: j (alias). References must be initialized at construction (like consts). 17
Reference vs Value Arguments #include <iostream> using namespace std; void symm(int & j) { //REF ARG j = -1 * j; int main() { int i = 3; symm( i ); cout << result: << i; #include <iostream> using namespace std; void symm(int j) { //VALUE ARG j = -1 * j; int main() { int i = 3; symm( i ); cout << result: << i; result: -3 result: 3 18
Efficiency Issues Passing objects to functions by reference is much more efficient whendealing withlarge objects. Passing values to functions by value implies a copy of the argument (clone) to thecalled function. obj obj clone main func main func 19
Operator Overloading Operators are like functions. They can also be overloaded. We can redefine what the following operators do when applied to objects of our classes: + - * / = < > += -= *= /= << >> <<= >>= ==!= <= >= ++ -- % & ^! ~ &= ^= = && %= [] () ->* -> new delete new[] delete[], 20
Operators are functions When we write It is the same as writing: MyClass a, b;... a+b; MyClass a, b;... a.operator+(b); operator+() is a function. 21
Example: Overloaded Operator for Cubes FILE cube1a.h Let us redefine the == operator in the class Cube to test if two cubes have the same shape and density. If we need the same functionality for Cylinders, we need to overide the == operator for Cylinders. Could we override the base class operator == instead? Yes, butthis will require abstract classes and virtual functions next lecture. #ifndef CUBE1A_H #define CUBE1A_H #include solid1.h class Cube1a : public Solid1 { float _s; public: Cube1a (float s, float d) : _s {s, Solid1{d { cout << Creating Cube1a <<endl; ~Cube1a() { cout << Destroying Cube1a << endl; float CalcMass(){return _s*_s*_s*_d; bool operator==(cube1a &c) { return (_s==c._s) && (_d==c._d); ; #endif //CUBE1A_H 22
Example: Testing Overloaded Operator for Cubes FILE blocks1a.h What is the output of the following code? Note that the method just created does not distinguish clones from the object itself. How can we do this? #include "cube1a.h int main() { Cube1a c1 {3.0, 0.9; Cube1a c2 {3.0, 0.9; Cube1a c3 {3.0, 1.0; cout << (c1==c1) << " " << (c1==c2) << " " << (c1==c3) << endl; Output: Creating Solid1 Creating Cube1 1 1 0... Destroying Cube1 Destroying Solid1 23
The this pointer FILE cube1a.h The keyword this represents a pointer to the object whose member function is being executed - it is a pointer to the object itself. One of its uses can be to check if a parameter passed to a member function is the object itself Let us overload the boolean operator && to provide this functionality. #ifndef CUBE1A_H #define CUBE1A_H #include solid1.h class Cube1a : public Solid1 { float _s; public: Cube1a (float s, float d) : _s {s, Solid1{d { cout << Creating Cube1a <<endl; ~Cube1a() { cout << Destroying Cube1a\n ; float CalcMass(){return _s*_s*_s*_d; bool operator==(cube1a &c) { return (_s==c._s) && (_d==c._d); bool operator&&(cube1a &c) { return this == &c; ; #endif //CUBE1A_H 24
Example FILE blocks1a.h Try the following code. What is the output now? #include "cube1a.h int main() { Cube1a c1 {3.0, 0.9; Cube1a c2 {3.0, 0.9; Cube1a c3 {3.0, 1.0; cout << (c1&&c1) << " " << (c1&&c2) << " " << (c1&&c3) << endl; Output: Creating Solid1 Creating Cube1 1 0 0... Destroying Cube1 Destroying Solid1 25
Assignment Operator When we write MyClass a, b;... b = a; It is the same as writing: MyClass a, b;... b.operator=(a); operator=() is called the assignment operator. The compiler provides a default assignment operator for all classes. The default assignment operator just copies the values of the member variables of the right and argument to the left hand object. If the intended behavior is different, one has to override the assignment operator. 26
Assignment Operator Let us define an assignment operator to our Cube class. The assignment operator returns a reference to the current object. Consider the assignment c2=c1: The purpose is to assign the properties of c1 to c2. We are invoking a member function of c2 with argument c1. Variable _s refers to c2 and c._s refers to c1. The invocation of base the class (Solid) assignment operator is not automatic, must be done explicitly. FILE cube1b.h #ifndef CUBE1B_H #define CUBE1B_H #include solid1.h class Cube1b : public Solid1 { float _s; public: Cube1b (float s, float d): _s {s, Solid1{d { cout << Creating Cube1b <<endl; ~Cube1b() { cout << Destroying Cube1b\n ; float CalcMass(){return _s*_s*_s*_d; Cube1b& operator= (const Cube1b & c ) { if ( this == &c) return (*this) ; Solid::operator=(c); //base class assignment _s = c._s; cout << Cube1b assignment << endl; return (*this); ; #endif //CUBE1B_H 27
Cascading Operators Why returning *this (reference to the object itself)? After assigning the values, our objective is complete. However we should also support operations like: c3 = c2 = c1 ó c3 = (c2 = c1) ó c3.operator=( c2.operator=(c1) ) This means: assign c1 to c2, then assign c2 to c3. What happens in this case: The assignment operator of c2 is called with a parameter of type reference to c1. The member variables of c1 are assigned to c2 and a reference to c2 is returned. The assignment operator of c3 is called with the reference to c2. Then the member variables of c2 are assigned to c3. 28
The copy constructor Cube c1 {1.0,1.0; Cube c2 {2.0,2.0; c2 = c1; Code 1 Code 2 Cube c1 {1.0,1.0 Cube c2 {c1; What are the differences? The result is quite similar but what happens is substantially different. In the left, the compiler invokes: 1. Constructor of c1 with float arguments 2. Constructor of c2 with float arguments 3. The assignment operator= of object c2 with argument c1. In the right, c2 is constructed with argument c1. This requires a special constructor: the Copy Constructor. The compiler provides a default copy constructor for all classes. The default copy constructor just initializes the values of object to construct with the values of the member variables of the object in the argument. 29
Overriding Copy Constructor for Cubes class Cube : public Solid { public: // Copy constructor Cube (const Cube & c) : Solid {c //init base class { _s = c._s; ; The copy constructor does not return a value. The argument is const because it should not be changed. The compiler always create a default copy constructor that copies each member variable (including for base class). Define your own copy constructor if behaviour different from default is needed. In this case you must also implement the initialization of the base classes. 30
Overriding Copy Constructor for Cubes The copy constructor does not return a value. The argument is const because it should not be changed. The compiler always create a default copy constructor that copies each member variable (including for base class). Define your own copy constructor if behaviour different from default is needed. In this case you must also implement the initialization of the base classes. FILE cube1b.h #ifndef CUBE1B_H #define CUBE1B_H #include solid1.h class Cube1b : public Solid1 { float _s; public: Cube1b (float s, float d): _s {s, Solid1{d { cout << Creating Cube1b <<endl; ~Cube1b() {cout << Destroying Cube1b\n ; float CalcMass(){return _s*_s*_s*_d; Cube1b& operator= (const Cube1b & c ) { if ( this == &c) return (*this) ; Solid::operator=(c); _s = c._s; cout << Cube1b assignment << endl; return (*this); Cube1b(const Cube1b & c ) : Solid1{c._d //base class initialization {_s = c._s; cout << Cube1b copy constructor << endl; ; #endif //CUBE1B_H 31
Automatic Use Copy Constructor The copy constructor is automatically invoked in: object initialization. passing and returning value arguments in functions. Compiler optimizations may circumvent some constructors (E.g. return value optimization). FILE blocks1b.h #include "cube1b.h Cube1b func(cube1b c) { return Cube1b {c; int main() { Cube1b c1 {1.0,1.0; Cube1b c2 {c1; Cube1b c3 = c1; c3 = c2; Cube1b c4 = func(c1) Output: Creating Solid1 Creating Cube1b Creating Solid1 Cube1b copy constructor Creating Solid1 Cube1b copy constructor Cube1b assignment Creating Solid1 Cube1b copy constructor Creating Solid1 Cube1b copy constructor Destroying c1 {1.0,1.0 c2 {c1 Cube1b c3 = c1 c3 = c2 func(c1) Cube1b c4 =... 32
Private Constructors and Deleted Functions Sometimes objects should not be cloned. E.g. when holding unique resources in the system. To prevent object cloning, the copy constructor and the assignment operator should be made private. Cloning can only be done by member functions. Alternativelly, they can be marked as deleted. In this case their cloning is impossible Other member functions can be deleted, e.g. to prevent implicit type conversion in constructors. // class with private clone operations class A { private: A (const A &); A& operator = (const A & );... ; // class with deleted clone operations class B { A (const A &) = delete; A& operator = (const A & ) = delete; ; //class with deleted contructor for int class C { public: C (double); //can initialize from double C (int) = deleted; //but not from int 33