Introduction to C/C++ Day 3 Mark Slater Adapted from original material by Andrew Bennieston Overview 1. Object Orientation 2. Classes 3. Public and Private Access 4. Operator Overloading 5. Designing a Particle Class 1. Object Orientation Object Oriented Programming (1) ● ● We are now in a position to fully introduce the concept of object-oriented (OO) programming. This is a different paradigm to the one we have been using so far - until now we have thought of a program as starting at the top and working down, calling functions along the way. This is procedural programming. x y z t v x y z v boost_z() main() x y z L t interval() Using a FourVector struct has tidied up the function calls somewhat, but it is still basically the same program... Object Oriented Programming (2) ● ● ● OO programming changes the focus so that we are thinking about the creation and manipulation of complex objects which have well-defined properties. The information associated with the object (here, an energy and three momenta) is encapsulated within the object, and hidden from the calling program, except through a well-defined interface. If you want to know the interval of a FourVector object, you ask the object and it tells you... you don't have to care about how it arrived at the number! Note that this was what we were doing with Opaque Pointers! FourVector v x boost_z() main() y z interval() L t The main program and the FourVector code are now disconnected. Anything to do with properties of the object is handled by the object itself! Benefits of OO There are some significant advantages to using good OO design: ➔ ➔ ➔ ➔ ➔ Simplified code – easy to transfer a single object rather than many objects Divides code into small, logically related pieces Each piece can be dealt with in isolation Improves sharing and reuse Complex behaviour can be created by combining objects in different ways The best way to learn good OO design is to practice, but beware – while good OO design will improve your code, bad design will render it unusable! 2. Classes Introduction to Classes ● ● Classes are how we create the user-defined objects we need in C++ Classes and structs are almost identical; just use the class keyword instead of struct, and add the public keyword (more on this later...) struct FourVector { double t; double x; double y; double z; }; class FourVector { public: double t; double x; double y; double z; }; Member Functions ● Classes can contain member functions as well as member variables. These have access to the data associated with an instance of the class: class FourVector { public: double interval() const; double boost_z(const double velocity); double double double double }; t; x; y; z; These are now member functions which return the interval, and perform the boost. They work on the variables attached to an object instance. Note that these are function declarations; you still need to define them! Defining Member Functions ● ● The code on the previous slide declares a class called FourVector which has two member functions and four member variables. It doesn't define the code associated with the functions. To do this, we must provide a function definition similar to before. The difference is that it must now be defined in the scope of the class, using the scope resolution operator :: double FourVector::interval() const { // Assume natural units double s = t*t – x*x – y*y – z*z; return s; } Aside – Using const ● ● In the previous declarations/definitions we have declared functions as const You can do this for: ➔ ➔ ➔ ● ● Variables (e.g. const int) – the contents of the variable can't be changed Function arguments – the function can't alter the object Member functions In this last case, what you're saying is that you guarantee that this member function does not change the state of the object – you can still access the variables (read their values) but attempting to change them results in a compiler error. Note that this becomes useful when you start passing objects of user-defined type around as function arguments. If an object is passed through a reference-to-const, you will only be able to call const member functions of that object! A Note on Naming Conventions ● ● ● ● Now we have local variables, member variables, classes, functions, and member functions, it can be important to think about naming conventions... It is hard to distinguish a member variable from a local variable inside a member function. To help with this, it can be useful to implement (and stick to!) a naming convention. Examples include: ➔ m_ or f_ prefix for member variables (“member” or “field”) ➔ Trailing underscore for member variables ➔ Function names all_lowercase, member functions in camelCase ➔ Classes with InitialLettersCapitalised ➔ Prefix class names with T or C (“type” or “class”; the ROOT convention...) Which scheme you use is entirely a matter of taste. The important thing is that you choose one and stick to it. Chances are your experiment will already have an established convention, so it is probably best to use that! Exercise 3 ● ● ● ● Modify your struct into a class with member functions boost_z() and interval(). You should be able to use a similar file structure as with the opaque pointers exercise earlier. You will need to change the declaration/definitions though! You now won't need to pass any arguments to the interval() function (not even a pointer) since all the things you need are member variables associated with an object of that type. You'll still need to pass a velocity to the boost_z() function! Hints: ➔ ➔ ➔ Member functions are accessed in the same way as member variables, i.e. object.myFunction() or ptr_to_object->myFunction() You can't initialise objects of class type using the array-style initialiser we used for structs; for now you'll have to set each field in turn. You must include the public keyword! Constructors (1) ● ● ● You'll notice that you have to assign values to the member variables individually, at present. To properly initialise objects of class type, there are special member functions known as constructors. These have the same name as the class itself, and are called whenever an object of that type is constructed. They are mostly used to initialise variables, but they can contain any code you like, so they can perform complex tasks like opening a file, establishing a database connection, allocating memory or computing some quantity. Constructors (2) The constructor can take any parameters you like and is defined just like any other member function. You can also have multiple constructors for a class, thanks to function overloading. class FourVector { public: // ctors FourVector() { t = 0; x = 0; y = 0; z = 0; } FourVector(const double t_, const double x_, const double y_, const double z_); FourVector(const FourVector& other); // member functions double interval(); double boost_z(); }; // member variables double t; double x; double y; double z; First ctor takes no arguments and sets all member vars to 0. Note that you can put simple function definitions in the class declaration! Second ctor takes four arguments. Note that we have declared it but not defined it – there is no function body! Third ctor is a copy constructor – it takes a reference to another object of the same type, allowing us to make copies... Constructors and Initialiser Lists ● Like any other member function, you can define constructors outside the class declaration. Here are the definitions for the second and third constructors, which we didn't provide in the declaration: FourVector::FourVector(const double t_, const double x_, const double y_, const double z_) : t(t_), x(x_), y(y_), z(z_) {} Initialiser lists FourVector::FourVector(const FourVector& other) : t(other.t), x(other.x), y(other.y), z(other.z) {} ● ● Note the use of initialiser lists. When a constructor is called, the member variables are all default-initialised before the body of the constructor runs. Assigning values in the constructor body results in a default-initialisation followed by assignment, while assigning in an initialiser list will initialise the variables with the values you specify, which is more efficient. In fact, const member variables and references must be set with initialiser lists, since once they have been initialised they cannot be modified – even in the body of the constructor! Destructors ● ● Destructors are the opposite of constructors; member functions which are called when an object is deleted (i.e. when stack objects go out of scope, or delete is called on heap objects). In analogy to constructors, they use the class name, but have a tilde ~ before the name: class SaferDynamicArray { public: SaferDynamicArray(int size) { array = new int[size]; } In the ctor we dynamically allocate an array... ~SaferDynamicArray() { delete[] array; } }; int* array; In the dtor we delete it to prevent a memory leak! RAII ● ● ● ● RAII stands for 'Resource Acquisition Is Initialisation' and is the concept behind the example on the previous slide. It deals with resource deallocation such as deleting memory allocated through new, closing files, ending database connections, etc. The idea is to use a constructor to initialise an object with an acquired resource (memory, file handle, database connection, etc.) and a destructor to free the resource once the object goes out of scope. Because the resource is tied to the object, and the destructor is always called when an object's lifetime comes to an end, the resource will always be deallocated. Exercise 4 ● Add constructors to your FourVector class. ● Points to think about: ➔ ➔ The different constructors you could provide (default, ctors with arguments, copy ctor...). Try to add them as appropriate. Does your FourVector need a destructor? Maybe try adding one anyway and use std::cout to print messages from the body of the ctor & dtor to see when they get called... 3. Public and Private Access Public and Private Access (1) ● ● ● Switching from structs to classes introduced access control. You had to use the public keyword to give your class members the same visibility as the struct members used to have. If a member function or member variable is declared public, any code can call the function or access the variable. If it is private, only other member functions of the same class can use it. Test this by attempting to compile your code with the public keyword changed to private... Public and Private Access (2) ● ● ● ● You should have seen a number of compiler errors when you changed from public to private. One of the main reasons for using private access is to hide implementation details of your class from code outside. For example, if you store a 3-vector, you could store (x,y,z) or (r,θ,ϕ) internally and still provide member functions to retrieve (x,y,z) or the length of the vector. The code on the outside doesn't need to know about the internal representation, so long as it can ask for the length, or a value in some coordinate system This allows us to do what we wanted this morning with opaque pointers – completely hide the implementation of the object so external code doesn't depend on it. Public and Private Access (3) ● ● ● Another example would be an optimisation of the FourVector class. We could have the interval pre-computed so that it isn't calculated each time you call the interval() method. However, if the user was to change one of the variables directly, the pre-computed interval would no longer be valid. Consequently, you should make the member variables private, and provide public access methods to read and write them. Public and Private Access (4) class MyClass { public: // ctors & dtors MyClass() {} ~MyClass() {} // public member functions double foo(); double bar(double v); }; private: double num; double a; void myfoo(); By default, the access mode is private for classes. Switch to public for the interface... You should make sure your constructors & destructors are public – otherwise nobody will be able to make an instance of your class! You can have private member functions as well as private member variables! You can switch between public and private multiple times by using the keywords as appropriate – but it may get confusing! Exercise 5 ● ● ● Apply the appropriate access rights to the member variables and member functions of your FourVector class. Make the variables private and provide additional public member functions to get and set their values. Move the code from the public interval() function to a private one which pre-computes the interval... you should call this from the ctor and whenever a variable is modified. 4. Operator Overloading Operator Overloading ● ● ● ● You now have a FourVector class that provides some useful member functions and can be used in a similar way to the built-in types. However, unlike the built-in types, you can't use mathematical operators on FourVectors. If you want to be able to do something like the code in the box, you have to tell the compiler how to perform this operation. You do this by providing code to implement the required operations, so that when the compiler sees an operator it knows what to do. This is called operator overloading and provides a powerful mechanism for making user-defined types behave identically to built-in types. int main() { FourVector a; FourVector b; FourVector c = a + b; } return 0; Operator Overloading ● As we've already seen, C++ provides a number of different operators, working on a number of different operands: <object1> <operator> <object2> // binary: *, +, / <object1> <operator> // unary postfix: ++, --, [] <operator> <object1> // unary prefix: ++, --, - ● ● When you apply an operator to an object of user-defined type, the compiler calls a function which you can provide the definition of. These are often (but not always) member functions. The declaration depends on whether they take one argument or two, in addition to the return type. Some examples follow: A = B; // A is return value, B is argument (member function of A) [copy assignment] C = (A + B); // C is return value, A and B are arguments (free function) [addition] (More) Operator Overloading class MyClass { public: MyClass() : a(0) {} MyClass(double val) : a(val) {} MyClass(const MyClass& other) : a(other.a) {} ~MyClass() {} Declaring operators for addition-with-assignment, and for copy-assignment. These affect the object itself so are declared as members. MyClass& operator+=(const MyClass& rhs) { a += rhs.a; return *this; } MyClass& operator=(const MyClass& rhs) { if (&rhs != this) { a = rhs.a; } return *this; } private: double a; }; MyClass operator+(const MyClass& lhs, const MyClass& rhs) { MyClass temp(lhs); temp += rhs; return temp; } The addition operator requires two parameters since it is declared outside of the class. We could have declared it inside, but it's better to have it as a free function. Note that it is implemented in terms of the += member operator! Streaming Operators ● You can also write operators to allow you to read and write objects of your new class using the C++ streams. These are operator>> (for input) and operator<< (for output) and they must be free (non-member) operators: std::ostream& operator<<(std::ostream& stream, const FourVector& v) { stream << "["; stream << v.getT() << ", " << v.getX() << ", "; stream << v.getY() << ", " << v.getZ() << "]"; return stream; } std::istream& operator>>(std::istream& stream, FourVector& v) { double t, x, y, z; stream >> t >> x >> y >> z; v.setT(t); v.setX(x); v.setY(y); v.setZ(z); return stream; } Exercise 6 ● ● Add overloads for the operators +=, -= and = to your FourVector class. Provide overloads for + and – in terms of the member operators. Implement the streaming operators and use them to simplify the input in your main function. Remember to check for self-assignment (A = A) in the = operator! Aside: UML Class Diagrams UML (Unified Modelling Language) class diagrams provide a graphical representation of the members of a class and how they relate to any other classes in your program. Member name(argument list) :return type Attributes (member variables) Private members indicated by – Public members indicated by + Operations (member functions) 5. Designing a Particle Class Designing a Particle Class In the homework exercise you’ll have to implement a Particle class, but let’s take a moment to think about the design of such a class… Summary We’ve covered several important concepts today: ➔ ➔ ➔ How to decompose a problem into a set of objects and their interactions How to represent these using classes, member functions and operators Which variables and methods to encapsulate within a class, and which to leave as 'free' functions You’ll use all of these in the homework exercise… Homework Exercise Now we have a design that includes Particle, ThreeVector and FourVector; for the homework exercise: ➔ ➔ ➔ ➔ Split your code into source and header files; use one header and one source file for each class, as well as a source file for your main() function Implement the Particle, FourVector and ThreeVector classes according to the interface we just designed Rewrite your solution to the Day 2 homework (sorting by invariant mass) to use these new classes Keep the directory structure and Makefiles, and remember to commit to git regularly!