|
| 1 | +# C++ Polymorphism Example |
| 2 | + |
| 3 | +## Summary |
| 4 | + |
| 5 | +This example shows two different ways to implement [polymorphism](https://en.wikipedia.org/wiki/Polymorphism_(computer_science)) in C++: |
| 6 | + |
| 7 | +1. The classic approach using an [abstract class](https://en.cppreference.com/w/cpp/language/abstract_class) which is used to define an interface and from which the implementation inherits. |
| 8 | +2. The modern approach using [concepts](https://en.cppreference.com/w/cpp/language/constraints) to declare features a type must have. |
| 9 | + |
| 10 | +In addition, this directory contains an example about how to perform unit tests using the [ut testing framework](https://github.com/boost-ext/ut). |
| 11 | + |
| 12 | +## Build Instructions |
| 13 | + |
| 14 | +### Preparation |
| 15 | + |
| 16 | +Create a subdirectory `build_polymorphism` and prepare the build system for the source located in `polymorphism` in this subdirectory: For release builds use |
| 17 | + |
| 18 | +```bash |
| 19 | +cmake -D CMAKE_BUILD_TYPE=Release -B build_polymorphism -S polymorphism |
| 20 | +``` |
| 21 | +For debug builds use |
| 22 | + |
| 23 | +```bash |
| 24 | +cmake -D CMAKE_BUILD_TYPE=Debug -B build_polymorphism -S polymorphism |
| 25 | +``` |
| 26 | + |
| 27 | +### Building the `polymorphism` project |
| 28 | + |
| 29 | +Call `cmake` from the top level like this: |
| 30 | + |
| 31 | +```bash |
| 32 | +cmake --build build_polymorphism --config Release --target polymorphism --parallel |
| 33 | +``` |
| 34 | + |
| 35 | +## Test Instructions |
| 36 | + |
| 37 | +### Build the tests |
| 38 | + |
| 39 | +```bash |
| 40 | +VERBOSE=1 cmake --build build_polymorphism --clean-first --config Debug --target all --parallel |
| 41 | +``` |
| 42 | + |
| 43 | +### Run the tests |
| 44 | + |
| 45 | +```bash |
| 46 | +ctest --test-dir build_polymorphism --output-on-failure --build-config Debug |
| 47 | +``` |
| 48 | + |
| 49 | +## Code Explanations |
| 50 | + |
| 51 | +### Classic Approach |
| 52 | + |
| 53 | +In classic C++, interfaces are defined via [abstract base classes](https://en.cppreference.com/w/cpp/language/abstract_class) from which the implementation (publicly) inherits. In this example, we have defined such an abstract class: |
| 54 | + |
| 55 | +```c++ |
| 56 | +class ISuperCoolFeatures { |
| 57 | +public: |
| 58 | + virtual std::string coolFeature() const = 0; // Pure virtual function |
| 59 | + virtual void set(std::string s) = 0; // Pure virtual function |
| 60 | + virtual ~ISuperCoolFeatures() = default; // Virtual destructor |
| 61 | +}; |
| 62 | +``` |
| 63 | +
|
| 64 | +Instead of defining the member function body (which is possible in a regular class), the member functions of an abstract interface class are declared "pure virtual" by adding `= 0` to their declaration. |
| 65 | +
|
| 66 | +Any so-called **implementation** of the interface publicly inherits from this abstract base class and then declares and defines member functions that match the declarations of the pure virtual functions in the abstract interface class: |
| 67 | +
|
| 68 | +```c++ |
| 69 | +class Impl |
| 70 | + : public ISuperCoolFeatures { |
| 71 | +
|
| 72 | +// ... private members and member functions omitted ... |
| 73 | +
|
| 74 | +public: |
| 75 | + std::string coolFeature() const override { return s_; } |
| 76 | + void set(std::string s) override |
| 77 | + { |
| 78 | + // ... implementation omitted ... |
| 79 | + } |
| 80 | +}; |
| 81 | +``` |
| 82 | + |
| 83 | +A variable (aka instantiation) of type `Impl` can be passed as argument to any function that expects an argument of its interface type `ISuperCoolFeatures`. |
| 84 | + |
| 85 | +In our example, we define a function which has one argument of type `ISuperCoolFeatures`, and returns a `std::string`: |
| 86 | + |
| 87 | +```c++ |
| 88 | +std::string consume(ISuperCoolFeatures& f); |
| 89 | +``` |
| 90 | +
|
| 91 | +We then pass an argument of type `Impl` to it, e.g. like this: |
| 92 | +
|
| 93 | +```c++ |
| 94 | +Impl i; |
| 95 | +consume(i); |
| 96 | +``` |
| 97 | + |
| 98 | +Since it is best practice, the example code puts the type and function definitions into namespaces. |
| 99 | + |
| 100 | +### Modern Approach |
| 101 | + |
| 102 | +In modern C++, interfaces can also be defined via [concepts](https://en.cppreference.com/w/cpp/language/constraints), which can be used to declare what features a concrete class must have. It is slightly more powerful in what can be described. |
| 103 | + |
| 104 | +It also has a great advantage that it is non-intrusive, i.e. it does not require a concrete class to inherit from a certain interface class. This is of advantage when you are not owner of the code of a class that you want to use and cannot change its declaration and definition. |
| 105 | + |
| 106 | +This means that objects of completely unrelated classes can be passed as arguments to a member function (aka method) or a free function, if they all have certain member functions defined. |
| 107 | + |
| 108 | +From a code structuring point of view, this means that the cohesion between different parts of the code is reduced. |
| 109 | + |
| 110 | +In this example, we have defined such an interface specification as a concept: |
| 111 | + |
| 112 | +```c++ |
| 113 | +template <typename T> |
| 114 | +concept has_super_cool_features = requires(T t, std::string s) { |
| 115 | + { t.coolFeature() } -> std::convertible_to<std::string>; |
| 116 | + { t.set(s) } -> std::same_as<void>; |
| 117 | +}; |
| 118 | +``` |
| 119 | + |
| 120 | +We then declare a function that takes arguments whose type adheres to the constraints defined by the concept: |
| 121 | + |
| 122 | +```c++ |
| 123 | +std::string consume(has_super_cool_features auto& s); |
| 124 | +``` |
| 125 | +
|
| 126 | +and can use it in the very same way like in the classic case: |
| 127 | +
|
| 128 | +```c++ |
| 129 | +Impl i; |
| 130 | +consume(i); |
| 131 | +``` |
| 132 | + |
| 133 | +In this approach, our implementation is defined **without** inheritance: |
| 134 | + |
| 135 | +```c++ |
| 136 | +class Impl { |
| 137 | + |
| 138 | +// ... |
| 139 | + |
| 140 | +public: |
| 141 | + std::string coolFeature() const { /* ... some code ... */ } |
| 142 | + void set(std::string s) |
| 143 | + { |
| 144 | + // code omitted |
| 145 | + } |
| 146 | +}; |
| 147 | +``` |
| 148 | +
|
| 149 | +To catch errors in the implementation before the class gets uses somewhere else in the code, it suffices to check against the concept like this: |
| 150 | +
|
| 151 | +```c++ |
| 152 | +static_assert(has_super_cool_features<Impl>); |
| 153 | +``` |
| 154 | + |
| 155 | +Please note, that this modern approach with its advantages in code decoupling, flexibility and a more powerful description of its features comes at a price: |
| 156 | + |
| 157 | +Functions that use `<some concept> auto` as parameter type are *generic* and this requires either their implicit or their explicit instantiation. |
| 158 | + |
| 159 | +Implicit instantiation of the function can take place if its **definition** is visible at the point of usage. |
| 160 | + |
| 161 | +In this example, we use explicit instantiation to show the possibility to clearly separate the definition (implementation) of the generic function from its declaration. |
| 162 | + |
| 163 | +You can find the definition of `consume` in `src/consume_class_that_adheres_to_concept.ipp` which is then included into `src/consume_class_that_adheres_to_concept.cpp`. In the latter file we also place all explicit instantiations of `consume`: |
| 164 | + |
| 165 | +```c++ |
| 166 | +// explicit template instantiation |
| 167 | +template std::string consume<Impl>(Impl&); |
| 168 | +``` |
| 169 | + |
| 170 | +As an alternative, the *definition* of `consume` could be in the header `include/polymorphism/consume_class_that_adheres_to_concept.hpp`. Then the explicit template instantiation is no longer required. |
| 171 | + |
| 172 | +## Testing |
| 173 | + |
| 174 | +In this example, I use [µt](https://github.com/boost-ext/ut) as testing framework for no good reason. It looks promising and lightweight, and it was written by [Krzysztof Jusiak](https://github.com/krzysztof-jusiak) whom I adore for his outstanding C++ programming skills and his [C++ Tip of The Week](https://github.com/tip-of-the-week/cpp) collection of crazy C++ riddles. |
| 175 | + |
| 176 | +The one-header version of it is pulled into the project via the CMake call |
| 177 | + |
| 178 | +```cmake |
| 179 | +FetchContent_Declare(ut |
| 180 | + URL https://raw.githubusercontent.com/boost-ext/ut/refs/heads/master/include/boost/ut.hpp |
| 181 | + DOWNLOAD_NO_EXTRACT TRUE |
| 182 | + SOURCE_DIR ut |
| 183 | +) |
| 184 | +FetchContent_MakeAvailable(ut) |
| 185 | +``` |
| 186 | + |
| 187 | +and the directory to which the header is downloaded is added to the search path set for the build of the test executables: |
| 188 | + |
| 189 | +```cmake |
| 190 | +target_include_directories(test_consume PUBLIC "${ut_SOURCE_DIR}") |
| 191 | +``` |
| 192 | + |
| 193 | +Please refer to [this slidedeck](https://boost-ext.github.io/ut/denver-cpp-2019/#/) for further documentation of this test library. |
0 commit comments