Skip to content

Commit ce16916

Browse files
authored
Merge pull request #10 from daixtrose/add-first-concepts-example
add a comparison of classic and modern polymorphism
2 parents 572aa33 + db2bb31 commit ce16916

16 files changed

+511
-0
lines changed

.coderabbit.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
2+
language: "en-US"
3+
early_access: false
4+
reviews:
5+
profile: "assertive"
6+
request_changes_workflow: false
7+
high_level_summary: true
8+
poem: true
9+
review_status: true
10+
collapse_walkthrough: false
11+
auto_review:
12+
enabled: true
13+
drafts: false
14+
chat:
15+
auto_reply: true

.github/workflows/build_examples.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@ jobs:
1414
- uses: actions/checkout@v4
1515
- name: configure
1616
run: cmake -D CMAKE_BUILD_TYPE=Release -B build_example_3 -S example_3
17+
1718
- name: build example_3
1819
run: cmake --build build_example_3 --config Release --target example_3
20+
1921
- name: create example_3 package
2022
run: cmake --build build_example_3 --config Release --target package
23+
2124
- name: check content of *.deb package
2225
id: check-packages
2326
working-directory: build_example_3
@@ -31,6 +34,7 @@ jobs:
3134
echo "RPM_PACKAGE_FILENAME=${RPM_PACKAGE_FILENAME}" >> $GITHUB_OUTPUT
3235
echo "Checking content of '$DEBIAN_PACKAGE_FILENAME'"
3336
dpkg -c ${DEBIAN_PACKAGE_FILENAME}
37+
3438
- name: Release
3539
uses: softprops/action-gh-release@v2
3640
## if: startsWith(github.ref, 'refs/tags/')
@@ -40,6 +44,15 @@ jobs:
4044
build_example_3/${{ steps.check-packages.outputs.DEBIAN_PACKAGE_FILENAME }}
4145
token: ${{ secrets.GITHUB_TOKEN }}
4246
tag_name: latest
47+
48+
- name: Configure polymorphism
49+
run: cmake -D CMAKE_BUILD_TYPE=Debug -B build_polymorphism -S polymorphism
50+
51+
- name: Build polymorphism
52+
run: cmake --build build_polymorphism --config Debug --target all --parallel
53+
54+
- name: Test polymorphism
55+
run: ctest --test-dir build_polymorphism --output-on-failure --build-config Debug
4356
# TODO: check https://github.com/marketplace/actions/cmake-swiss-army-knife
4457
# TODO: check https://github.com/actions/starter-workflows/blob/9f1db534549e072c20d5d1a79e0a4ff45a674caf/ci/cmake-multi-platform.yml#L20
4558

polymorphism/CMakeLists.txt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
cmake_minimum_required(VERSION 3.29)
2+
3+
project(polymorphism VERSION 1.0.0 LANGUAGES CXX)
4+
5+
# Enable testing by default when this is the top-level project
6+
option(POLYMORPHISM_ENABLE_TESTING "enable testing for polymorphism" ${PROJECT_IS_TOP_LEVEL})
7+
8+
add_executable(${PROJECT_NAME}
9+
src/consume_class_with_interface.cpp
10+
src/consume_class_that_adheres_to_concept.cpp
11+
src/main.cpp
12+
)
13+
14+
target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_23)
15+
target_include_directories(${PROJECT_NAME} PUBLIC include)
16+
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
17+
18+
if(POLYMORPHISM_ENABLE_TESTING)
19+
message(STATUS "Polymorphism testing enabled")
20+
enable_testing()
21+
add_subdirectory(test)
22+
else()
23+
message(STATUS "Polymorphism testing disabled")
24+
endif()

polymorphism/README.md

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
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.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#ifndef POYMORPHISM_CONSUME_CLASS_THAT_ADHERES_TO_CONCEPT_HPP
2+
#define POYMORPHISM_CONSUME_CLASS_THAT_ADHERES_TO_CONCEPT_HPP
3+
4+
#include <polymorphism/has_super_cool_features.hpp>
5+
6+
#include <string>
7+
8+
namespace modern {
9+
10+
// declaration with concept constraint
11+
std::string consume(has_super_cool_features auto& s);
12+
13+
} // namespace modern
14+
15+
#endif // POYMORPHISM_CONSUME_CLASS_THAT_ADHERES_TO_CONCEPT_HPP
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#ifndef POLYMORPHISM_CONSUME_CLASS_WITH_INTERFACE_HPP
2+
#define POLYMORPHISM_CONSUME_CLASS_WITH_INTERFACE_HPP
3+
4+
#include <polymorphism/i_super_cool_features.hpp>
5+
6+
namespace classic {
7+
8+
std::string consume(ISuperCoolFeatures& f);
9+
10+
} // namespace classic
11+
12+
#endif // POLYMORPHISM_CONSUME_CLASS_WITH_INTERFACE_HPP
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#ifndef POYMORPHISM_HAS_SUPER_COOL_FEATURES_HPP
2+
#define POYMORPHISM_HAS_SUPER_COOL_FEATURES_HPP
3+
4+
#include <string>
5+
6+
namespace modern {
7+
8+
/// @brief A concept defintion describing a class or struct that has two member functions.
9+
template <typename T>
10+
concept has_super_cool_features = requires(T t, std::string s) {
11+
{ t.coolFeature() } -> std::convertible_to<std::string>;
12+
{ t.set(s) } -> std::same_as<void>;
13+
};
14+
15+
} // namespace modern
16+
17+
#endif // POYMORPHISM_HAS_SUPER_COOL_FEATURES_HPP
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#ifndef POLYMORPHISM_I_SUPER_COOL_FEATURES_HPP
2+
#define POLYMORPHISM_I_SUPER_COOL_FEATURES_HPP
3+
4+
#include <string>
5+
6+
namespace classic {
7+
8+
/// @brief A typical interface definition via a class with pure virtual functions/methods.
9+
///
10+
/// This interface demonstrates classic polymorphism in C++.
11+
///
12+
/// @details
13+
/// - coolFeature(): Returns a string representing some cool feature.
14+
/// - set(std::string s): Sets some internal state of the implementing class.
15+
class ISuperCoolFeatures {
16+
public:
17+
[[nodiscard]] virtual std::string coolFeature() const = 0; // Pure virtual function
18+
virtual void set(std::string s) = 0; // Pure virtual function
19+
virtual ~ISuperCoolFeatures() = default; // Virtual destructor
20+
};
21+
22+
} // namespace classic
23+
24+
#endif // POLYMORPHISM_I_SUPER_COOL_FEATURES_HPP
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#ifndef POLYMORPHISM_IMPL_WITH_INTERFACE_HPP
2+
#define POLYMORPHISM_IMPL_WITH_INTERFACE_HPP
3+
4+
// make the inteface defintion visible
5+
#include <polymorphism/i_super_cool_features.hpp>
6+
7+
#include <string>
8+
9+
namespace classic {
10+
11+
class Impl
12+
: public ISuperCoolFeatures {
13+
private:
14+
std::string s_ { "<default value>" }; // with default member initializer (C++11)
15+
16+
public:
17+
std::string coolFeature() const noexcept override { return s_; }
18+
void set(std::string s) noexcept override
19+
{
20+
s_ = std::move(s);
21+
}
22+
};
23+
24+
} // namespace classic
25+
26+
#endif // POLYMORPHISM_IMPL_WITH_INTERFACE_HPP

0 commit comments

Comments
 (0)