|
2 | 2 | title: Inheritance |
3 | 3 | --- |
4 | 4 |
|
| 5 | +[ToC] |
| 6 | + |
5 | 7 | # Creating Sub-types with Inheritance |
6 | 8 |
|
7 | 9 | Inheritance is one of the most important concepts in object oriented design, which brings a great deal of flexibility to us as programmers. A class defines a type of object, and a class which inherits from it defines a sub-type of that type. For example, we might have a class which represents shapes, and sub-classes which represent squares, circles, and triangles. Each of these are shapes, and so should be able to be used in any context that simply requires a shape, but each will have slightly different data needed to define it and different implementations of functions to calculate its perimeter or area. |
@@ -232,7 +234,11 @@ Our current method of overriding and calling functions in the way described abov |
232 | 234 | - We often don't want to pass our derived class by value: this will attempt to copy the object into a new object of type `Shape`, so any overrides will be lost. |
233 | 235 | - We should instead pass our argument by reference (or as a pointer, which we'll discuss in a later week). This will avoid the copying into a fresh object and instead will just pass the address in memory where the object we want to pass is stored. However, the function itself will still be treating the object as being of type `Shape` and hence will call the `Shape` versions of any functions. |
234 | 236 |
|
235 | | -We can solve this problem by declaring a member function `virtual` in the base class. In this case, the function is accessed in a different way to normal. Function definitions have addresses, and normally when a member function of a class is called the definition of that function for that is just looked up. So if we are using a `Shape &` reference to an object, even if that object was created as type `Circle`, we will still look up the definition of any functions for `Shape`, since that's the class that we're using. For virtual functions however, each object will store the address of the definition of the function as part of its data (this data is called a "virtual table"). If the object is created as an instance of the base class, this will be the address of the base function, but if the object is created as an instance of a derived class, then this will be the address of the derived function. When we call the function on the object, it will execute the function at the address stored in the virtual table, which is individual to the instance of the object, rather than using an address which applies to the whole class. This means it doesn't matter if we are using a `Shape &` reference or `Circle &`, it will still used the derived function for the class `Circle` because that was the address put into the virtual table when the object was created. This is also why **passing a reference (or pointer) is necessary for this to work**. If we pass by value we will create a _new_ object of type `Shape`, and because it is of type `Shape` the new object's virtual table will link to the `Shape` implementation. If we pass a _reference_, then the function will instead look at the memory location of the original object, and therefore look in the original object's virtual table, and thus find the implementation for the derived class. |
| 237 | +We can solve this problem by declaring a member function `virtual` in the base class. In this case, the function is accessed in a different way to normal. |
| 238 | +
|
| 239 | +Function definitions have addresses, and when an ordinary (not virtual) member function of a class is called the definition of that function can be straight-forwardly looked up for that class. So if we are using a `Shape &` reference to an object, even if that object was created as type `Circle`, we will still look up the definition of any functions for `Shape`, since that's the class that we're using. |
| 240 | +
|
| 241 | +For classes with virtual functions however, each object will store an additional pointer as part of its data that points to a special table for that class (called a "virtual table" or "v-table"). The v-table contains pointers to the virtual function definitions for that class (amongst other things, as we'll see later); there will be one of these v-tables for each class with virtual functions. If the object is created as an instance of the base class, this it will have a pointer to the v-table for the base class, which contains the addresses of the base function implementation(s). If the object is created as an instance of a derived class, then it will carry the address of the v-table for the derived type, which contains addresses for the derived type's function(s). When we call a virtual function on an object, it will first follow the pointer to the correct virtual table and then will execute the function at the relevant address stored in the virtual table. Since the pointer to the v-table is part of the object's data rather than part of its type information, it doesn't matter if we are using a `Shape &` reference or `Circle &`, it will still be directed to use the derived function for the class `Circle` because that is the virtual table that the object was pointed to when it was created. This is also why **passing a reference (or pointer) is necessary for this to work**. If we pass by value we will create a _new_ object of type `Shape`, and because it is of type `Shape` the new object will point to the `Shape` v-table. If we pass a _reference_, then the function will instead look at the memory location of the original object, and therefore look in the original object's virtual table, and thus find the implementation for the derived class. |
236 | 242 |
|
237 | 243 | Virtual functions open up fully polymorphic behaviour for our classes, and are important whenever a object of a derived class might be treated as a member of a base class, including: |
238 | 244 |
|
@@ -331,3 +337,178 @@ class Square : public Shape |
331 | 337 | - The use of pure virtual functions means that the `Shape` class more closely corresponds to our abstract notion of a shape as being something that we can't implement without more information. |
332 | 338 | - Note that we don't have to design the class so that we re-calculate the area and perimeter every time we call `getArea` and `getPerimeter`; we could store them in member variables like in our previous example. Think about the pros and cons of these two approaches! |
333 | 339 |
|
| 340 | +## Runtime Type Information (RTTI) |
| 341 | + |
| 342 | +In addition to the locations of virtual functions, the v-table also contains type identification information. This means that C++ can find out if a `Shape` pointer is pointing to a `Circle` or a `Square` by the same mechanism as it finds the overrides for virtual functions: it follows the pointer from the object to its v-table, and then it can look up the type information. In our programs we can access this using [`typeid`](https://en.cppreference.com/w/cpp/language/typeid.html). |
| 343 | + |
| 344 | +```cpp |
| 345 | +#include<typeinfo> |
| 346 | +... |
| 347 | + |
| 348 | +int main() |
| 349 | +{ |
| 350 | + std::unique_ptr<Shape> C = std::make_unique<Circle>(2.1); |
| 351 | + std::unique_ptr<Shape> S = std::make_unique<Square>(1.8); |
| 352 | + |
| 353 | + std::cout << typeid(*C).name() << std::endl; |
| 354 | + std::cout << typeid(*S).name() << std::endl; |
| 355 | + |
| 356 | + return 0; |
| 357 | +} |
| 358 | +``` |
| 359 | + |
| 360 | +`typeid` returns an `std::type_info`, which requires the `<typeinfo>` include. The `name()` member function just makes it more human readable. Note that we dereference the pointers first: the types of the _pointers_ are the same, but the types of the _data_ that they point to are not. `typeid` can also be used with a _type_ as an argument instead of an object, as we shall see in the example below: |
| 361 | + |
| 362 | +```cpp |
| 363 | +#include<typeinfo> |
| 364 | +... |
| 365 | + |
| 366 | +int main() |
| 367 | +{ |
| 368 | + std::unique_ptr<Shape> C = std::make_unique<Circle>(2.1); |
| 369 | + std::unique_ptr<Shape> S = std::make_unique<Square>(1.8); |
| 370 | + |
| 371 | + bool same_pointer_type = (typeid(C) == typeid(S)); // True |
| 372 | + bool same_underlying_type = (typeid(*C) == typeid(*S)); // False |
| 373 | + bool is_Circle_type = (typeid(*C) == typeid(Circle)); // True |
| 374 | + bool is_Shape_type = (typeid(*C) == typeid(Shape)); // False |
| 375 | + |
| 376 | + return 0; |
| 377 | +} |
| 378 | +``` |
| 379 | + |
| 380 | +Note that `typeid` will return the specific type of the object, so checking where your `Circle` object is a `Shape` will return false, since these are distinct types. |
| 381 | + |
| 382 | +Run time type information is rarely needed in C++, since the types are usually known and most polymorphic behaviour can (and should) be handled by overriding member functions. However, there are some times where we are dealing with polymorphic types and we need to ascertain what type something is. For example, lets say I have a `vector<Shape>` and I need to get all the `Circle` objects from this list. Rather than forcing every sub-class of `Shape` to implement some kind of `isCircle()` function or carry extra data around, it is better to just use `typeid`. |
| 383 | + |
| 384 | +`typeid` works well when you just need to check some precise type information, like to check if an object is a `Circle` or not. Inheritance trees however can be a little more complex; let's consider an example with an addition _polygon_ subclass. Circles are not polygons, but triangles, squares, pentagons and so on are. |
| 385 | + |
| 386 | +```cpp |
| 387 | +class Shape |
| 388 | +{ |
| 389 | + public: |
| 390 | + Shape() |
| 391 | + { |
| 392 | + } |
| 393 | + |
| 394 | + virtual double getArea() = 0; |
| 395 | + |
| 396 | + virtual double getPerimeter() = 0; |
| 397 | + |
| 398 | + virtual void printInfo() = 0; |
| 399 | +}; |
| 400 | + |
| 401 | +class Circle : public Shape |
| 402 | +{ |
| 403 | + public: |
| 404 | + Circle(double r) : radius(r){} |
| 405 | + |
| 406 | + void printInfo() |
| 407 | + { |
| 408 | + cout << "Circle; Radius = " << m_radius << "m, Area = " << m_area << " m^2, Perimeter = " |
| 409 | + << m_perimeter << "m." << endl; |
| 410 | + } |
| 411 | + |
| 412 | + double getArea() |
| 413 | + { |
| 414 | + return M_PI * radius * radius; |
| 415 | + } |
| 416 | + |
| 417 | + double getPerimeter() |
| 418 | + { |
| 419 | + return 2 * M_PI * radius; |
| 420 | + } |
| 421 | + |
| 422 | + protected: |
| 423 | + double radius; |
| 424 | +}; |
| 425 | + |
| 426 | +class Polygon : public Shape |
| 427 | +{ |
| 428 | + |
| 429 | +} |
| 430 | + |
| 431 | +class Square : public Polygon |
| 432 | +{ |
| 433 | + public: |
| 434 | + Square(double w) : width(w){} |
| 435 | + |
| 436 | + double getArea() |
| 437 | + { |
| 438 | + return width * width; |
| 439 | + } |
| 440 | + |
| 441 | + double getPerimeter() |
| 442 | + { |
| 443 | + return 4 * width; |
| 444 | + } |
| 445 | + |
| 446 | + void printInfo() |
| 447 | + { |
| 448 | + cout << "Square; Width = " << width << "m, Area = " << area << " m^2, Perimeter = " |
| 449 | + << perimeter << "m." << endl; |
| 450 | + } |
| 451 | + |
| 452 | + protected: |
| 453 | + double width; |
| 454 | +}; |
| 455 | + |
| 456 | +class IsocelesTriangle : public Polygon |
| 457 | +{ |
| 458 | + public: |
| 459 | + Square(double b, double h) : base(w), height(h){} |
| 460 | + |
| 461 | + double getArea() |
| 462 | + { |
| 463 | + return 0.5*base*height; |
| 464 | + } |
| 465 | + |
| 466 | + double getPerimeter() |
| 467 | + { |
| 468 | + return base + sqrt(4*height*height + base*base); |
| 469 | + } |
| 470 | + |
| 471 | + void printInfo() |
| 472 | + { |
| 473 | + cout << "Triangle; Base = " << base << "m, Height = " << height << "m, Area = " << area << " m^2, Perimeter = " |
| 474 | + << perimeter << "m." << endl; |
| 475 | + } |
| 476 | + |
| 477 | + protected: |
| 478 | + double base; |
| 479 | + double height; |
| 480 | +}; |
| 481 | +``` |
| 482 | +
|
| 483 | +Now if we turn back to our `vector<Shape>`, suppose we want to do something with only the objects that are _polygons_? Writing a manual check for each kind of polygon like: |
| 484 | +
|
| 485 | +```cpp |
| 486 | +bool isPolygon(std::unique_ptr<Shape> &S) |
| 487 | +{ |
| 488 | + return (typeid(*S) == typeid(Square)) || (typeid(*S) == typeid(IsocelesTriangle)); |
| 489 | +} |
| 490 | +``` |
| 491 | + |
| 492 | +since this is not extensible. Instead we can use C++'s `dynamic_cast` to check whether something can be safely cast to the `Polygon` type, which would mean that it is a sub-class of that type: |
| 493 | + |
| 494 | +```cpp |
| 495 | +bool isPolygon(Shape &S) |
| 496 | +{ |
| 497 | + return dynamic_cast<Polygon*>(S.get()) != nullptr; |
| 498 | +} |
| 499 | +``` |
| 500 | +
|
| 501 | +Note that `dynamic_cast` requires us to work with pointer types (or reference types), so we no longer dereference `S` but extract the pointer from it. If the `dynamic_cast` fails then the result is a `nullptr`. This can be very effectively used in `if` statements since a pointer in a conditional statement implicitly converts to false if `nullptr` and true otherwise. |
| 502 | +
|
| 503 | +```cpp |
| 504 | +if(dynamic_cast<Polygon*>(S.get())) |
| 505 | +{ |
| 506 | + // do something polygon specific |
| 507 | +} |
| 508 | +else |
| 509 | +{ |
| 510 | + // do something else |
| 511 | +} |
| 512 | +``` |
| 513 | + |
| 514 | +Dynamic casting can also be used to safely convert `Shape` pointers into new `Predator`, `Fox` or any other subclass pointer that's required for e.g. passing to another function that takes a more specific type. **You absolutely must check for null pointers if you are going to do this, and make sure to think carefully about any ownership issues when you generate new pointers.** |
0 commit comments