-
Notifications
You must be signed in to change notification settings - Fork 0
add a comparison of classic and modern polymorphism #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a0bea38
0f46bcb
b2c3038
65ebad8
64002ae
4d1aeda
8b242de
b654f5d
3ceb45e
a661554
9e220f1
8b1b7f6
5ecc7d1
56728e2
67cab71
d84f1f2
74e90e5
dcc6a09
2e834ac
d909b5c
55afa89
4c937f9
850b2eb
533f18f
644c31f
ff39cad
11e04b0
917436a
38aa34e
07e948a
e0d3d98
8cad8a1
ba67fbf
17c651f
8434ea3
e26e0bb
4edbbca
d9d524a
b21206d
5e22a5f
0386212
db2bb31
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json | ||
language: "en-US" | ||
early_access: false | ||
reviews: | ||
profile: "assertive" | ||
request_changes_workflow: false | ||
high_level_summary: true | ||
poem: true | ||
review_status: true | ||
collapse_walkthrough: false | ||
auto_review: | ||
enabled: true | ||
drafts: false | ||
chat: | ||
auto_reply: true |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
cmake_minimum_required(VERSION 3.29) | ||
|
||
project(polymorphism VERSION 1.0.0 LANGUAGES CXX) | ||
daixtrose marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
# Enable testing by default when this is the top-level project | ||
option(POLYMORPHISM_ENABLE_TESTING "enable testing for polymorphism" ${PROJECT_IS_TOP_LEVEL}) | ||
daixtrose marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
add_executable(${PROJECT_NAME} | ||
src/consume_class_with_interface.cpp | ||
src/consume_class_that_adheres_to_concept.cpp | ||
src/main.cpp | ||
) | ||
daixtrose marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_23) | ||
target_include_directories(${PROJECT_NAME} PUBLIC include) | ||
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) | ||
|
||
if(POLYMORPHISM_ENABLE_TESTING) | ||
message(STATUS "Polymorphism testing enabled") | ||
enable_testing() | ||
add_subdirectory(test) | ||
else() | ||
message(STATUS "Polymorphism testing disabled") | ||
endif() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
# C++ Polymorphism Example | ||
|
||
## Summary | ||
|
||
This example shows two different ways to implement [polymorphism](https://en.wikipedia.org/wiki/Polymorphism_(computer_science)) in C++: | ||
|
||
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. | ||
2. The modern approach using [concepts](https://en.cppreference.com/w/cpp/language/constraints) to declare features a type must have. | ||
|
||
In addition, this directory contains an example about how to perform unit tests using the [ut testing framework](https://github.com/boost-ext/ut). | ||
daixtrose marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Comment on lines
+1
to
+11
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) Great introduction and summary! The introduction provides a clear and concise overview of the two approaches to polymorphism in C++ and mentions the use of the ut testing framework. This sets the stage well for the rest of the document. Consider adding a brief explanation of why understanding these two approaches is important for C++ developers. This could help motivate readers to explore the examples further. |
||
## Build Instructions | ||
|
||
### Preparation | ||
|
||
Create a subdirectory `build_polymorphism` and prepare the build system for the source located in `polymorphism` in this subdirectory: For release builds use | ||
|
||
```bash | ||
cmake -D CMAKE_BUILD_TYPE=Release -B build_polymorphism -S polymorphism | ||
``` | ||
For debug builds use | ||
|
||
```bash | ||
cmake -D CMAKE_BUILD_TYPE=Debug -B build_polymorphism -S polymorphism | ||
``` | ||
|
||
### Building the `polymorphism` project | ||
|
||
Call `cmake` from the top level like this: | ||
|
||
```bash | ||
cmake --build build_polymorphism --config Release --target polymorphism --parallel | ||
``` | ||
Comment on lines
+12
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) Clear and comprehensive build instructions. The build instructions are well-structured and provide clear commands for both release and debug builds. This will be very helpful for users trying to build the project. Consider making the following improvements:
cmake -D CMAKE_BUILD_TYPE=Release -B build_polymorphism -S polymorphism
+ For debug builds use 🧰 Tools🪛 Markdownlint
|
||
|
||
## Test Instructions | ||
|
||
### Build the tests | ||
|
||
```bash | ||
VERBOSE=1 cmake --build build_polymorphism --clean-first --config Debug --target all --parallel | ||
``` | ||
|
||
### Run the tests | ||
|
||
```bash | ||
ctest --test-dir build_polymorphism --output-on-failure --build-config Debug | ||
``` | ||
Comment on lines
+35
to
+47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) Concise test instructions. The test instructions are clear and provide the necessary commands to build and run the tests. Consider adding the following to enhance this section:
|
||
|
||
## Code Explanations | ||
|
||
### Classic Approach | ||
|
||
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: | ||
|
||
```c++ | ||
class ISuperCoolFeatures { | ||
public: | ||
virtual std::string coolFeature() const = 0; // Pure virtual function | ||
virtual void set(std::string s) = 0; // Pure virtual function | ||
virtual ~ISuperCoolFeatures() = default; // Virtual destructor | ||
}; | ||
``` | ||
|
||
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. | ||
|
||
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: | ||
|
||
```c++ | ||
class Impl | ||
: public ISuperCoolFeatures { | ||
|
||
// ... private members and member functions omitted ... | ||
|
||
public: | ||
std::string coolFeature() const override { return s_; } | ||
void set(std::string s) override | ||
{ | ||
// ... implementation omitted ... | ||
} | ||
}; | ||
``` | ||
|
||
A variable (aka instantiation) of type `Impl` can be passed as argument to any function that expects an argument of its interface type `ISuperCoolFeatures`. | ||
|
||
In our example, we define a function which has one argument of type `ISuperCoolFeatures`, and returns a `std::string`: | ||
|
||
```c++ | ||
std::string consume(ISuperCoolFeatures& f); | ||
``` | ||
|
||
We then pass an argument of type `Impl` to it, e.g. like this: | ||
|
||
```c++ | ||
Impl i; | ||
consume(i); | ||
``` | ||
|
||
Since it is best practice, the example code puts the type and function definitions into namespaces. | ||
|
||
### Modern Approach | ||
|
||
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. | ||
|
||
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. | ||
|
||
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. | ||
|
||
From a code structuring point of view, this means that the cohesion between different parts of the code is reduced. | ||
|
||
In this example, we have defined such an interface specification as a concept: | ||
|
||
```c++ | ||
template <typename T> | ||
concept has_super_cool_features = requires(T t, std::string s) { | ||
{ t.coolFeature() } -> std::convertible_to<std::string>; | ||
{ t.set(s) } -> std::same_as<void>; | ||
}; | ||
``` | ||
|
||
We then declare a function that takes arguments whose type adheres to the constraints defined by the concept: | ||
|
||
```c++ | ||
std::string consume(has_super_cool_features auto& s); | ||
``` | ||
|
||
and can use it in the very same way like in the classic case: | ||
|
||
```c++ | ||
Impl i; | ||
consume(i); | ||
``` | ||
|
||
In this approach, our implementation is defined **without** inheritance: | ||
|
||
```c++ | ||
class Impl { | ||
|
||
// ... | ||
|
||
public: | ||
std::string coolFeature() const { /* ... some code ... */ } | ||
void set(std::string s) | ||
{ | ||
// code omitted | ||
} | ||
}; | ||
``` | ||
|
||
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: | ||
|
||
```c++ | ||
static_assert(has_super_cool_features<Impl>); | ||
``` | ||
|
||
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: | ||
|
||
Functions that use `<some concept> auto` as parameter type are *generic* and this requires either their implicit or their explicit instantiation. | ||
|
||
Implicit instantiation of the function can take place if its **definition** is visible at the point of usage. | ||
|
||
In this example, we use explicit instantiation to show the possibility to clearly separate the definition (implementation) of the generic function from its declaration. | ||
|
||
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`: | ||
|
||
```c++ | ||
// explicit template instantiation | ||
template std::string consume<Impl>(Impl&); | ||
``` | ||
|
||
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. | ||
Comment on lines
+49
to
+170
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) Comprehensive code explanations for both approaches. The explanations for both the classic and modern approaches to polymorphism are detailed and well-structured. The code snippets effectively illustrate the concepts. Consider making the following improvements:
|
||
|
||
## Testing | ||
|
||
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. | ||
|
||
The one-header version of it is pulled into the project via the CMake call | ||
|
||
```cmake | ||
FetchContent_Declare(ut | ||
URL https://raw.githubusercontent.com/boost-ext/ut/refs/heads/master/include/boost/ut.hpp | ||
DOWNLOAD_NO_EXTRACT TRUE | ||
SOURCE_DIR ut | ||
) | ||
FetchContent_MakeAvailable(ut) | ||
``` | ||
|
||
and the directory to which the header is downloaded is added to the search path set for the build of the test executables: | ||
|
||
```cmake | ||
target_include_directories(test_consume PUBLIC "${ut_SOURCE_DIR}") | ||
``` | ||
|
||
Please refer to [this slidedeck](https://boost-ext.github.io/ut/denver-cpp-2019/#/) for further documentation of this test library. | ||
Comment on lines
+172
to
+193
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) Informative testing section with helpful CMake snippets. The testing section provides clear information about using the µt testing framework, including CMake code snippets for including the framework. The link to further documentation is a valuable addition. Consider making the following improvements:
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
#ifndef POYMORPHISM_CONSUME_CLASS_THAT_ADHERES_TO_CONCEPT_HPP | ||
#define POYMORPHISM_CONSUME_CLASS_THAT_ADHERES_TO_CONCEPT_HPP | ||
daixtrose marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
#include <polymorphism/has_super_cool_features.hpp> | ||
|
||
#include <string> | ||
|
||
namespace modern { | ||
|
||
// declaration with concept constraint | ||
std::string consume(has_super_cool_features auto& s); | ||
daixtrose marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
+10
to
+11
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) Enhance function documentation While the function declaration looks good and uses modern C++ features as intended, the documentation could be improved for better clarity. Consider using a Doxygen-style comment for better documentation. Here's a suggested improvement: /**
* @brief Consumes an object that adheres to the has_super_cool_features concept.
* @param s Reference to an object satisfying the has_super_cool_features concept.
* @return A string result after consuming the object.
*/
std::string consume(has_super_cool_features auto& s); |
||
|
||
} // namespace modern | ||
|
||
#endif // POYMORPHISM_CONSUME_CLASS_THAT_ADHERES_TO_CONCEPT_HPP |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
#ifndef POLYMORPHISM_CONSUME_CLASS_WITH_INTERFACE_HPP | ||
#define POLYMORPHISM_CONSUME_CLASS_WITH_INTERFACE_HPP | ||
Comment on lines
+1
to
+2
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) Consider using a more unique prefix for the include guard macro. While the typo has been fixed, consider using a more specific prefix to ensure uniqueness and prevent potential conflicts with other libraries. For example: -#ifndef POLYMORPHISM_CONSUME_CLASS_WITH_INTERFACE_HPP
-#define POLYMORPHISM_CONSUME_CLASS_WITH_INTERFACE_HPP
+#ifndef CPLUSPLUS_PRIMER_POLYMORPHISM_CONSUME_CLASS_WITH_INTERFACE_HPP
+#define CPLUSPLUS_PRIMER_POLYMORPHISM_CONSUME_CLASS_WITH_INTERFACE_HPP
// ... (rest of the file)
-#endif // POLYMORPHISM_CONSUME_CLASS_WITH_INTERFACE_HPP
+#endif // CPLUSPLUS_PRIMER_POLYMORPHISM_CONSUME_CLASS_WITH_INTERFACE_HPP This change would make the include guard more specific to your project. Also applies to: 12-12 |
||
|
||
#include <polymorphism/i_super_cool_features.hpp> | ||
|
||
namespace classic { | ||
|
||
std::string consume(ISuperCoolFeatures& f); | ||
|
||
} // namespace classic | ||
Comment on lines
+6
to
+10
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) Namespace and function declaration look good. Consider adding a brief comment. The use of the "classic" namespace and the function declaration for namespace classic {
// Consumes an object implementing the ISuperCoolFeatures interface
// and returns a string representation of the result.
std::string consume(ISuperCoolFeatures& f);
} // namespace classic This addition would provide context for developers who might use this function in the future. |
||
|
||
#endif // POLYMORPHISM_CONSUME_CLASS_WITH_INTERFACE_HPP |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
#ifndef POYMORPHISM_HAS_SUPER_COOL_FEATURES_HPP | ||
#define POYMORPHISM_HAS_SUPER_COOL_FEATURES_HPP | ||
daixtrose marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
#include <string> | ||
|
||
namespace modern { | ||
|
||
/// @brief A concept defintion describing a class or struct that has two member functions. | ||
template <typename T> | ||
concept has_super_cool_features = requires(T t, std::string s) { | ||
{ t.coolFeature() } -> std::convertible_to<std::string>; | ||
{ t.set(s) } -> std::same_as<void>; | ||
}; | ||
Comment on lines
+8
to
+13
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) Concept definition is well-structured The Some suggestions for improvement:
|
||
|
||
} // namespace modern | ||
|
||
#endif // POYMORPHISM_HAS_SUPER_COOL_FEATURES_HPP |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
#ifndef POLYMORPHISM_I_SUPER_COOL_FEATURES_HPP | ||
#define POLYMORPHISM_I_SUPER_COOL_FEATURES_HPP | ||
|
||
#include <string> | ||
|
||
namespace classic { | ||
|
||
/// @brief A typical interface definition via a class with pure virtual functions/methods. | ||
/// | ||
/// This interface demonstrates classic polymorphism in C++. | ||
/// | ||
/// @details | ||
/// - coolFeature(): Returns a string representing some cool feature. | ||
/// - set(std::string s): Sets some internal state of the implementing class. | ||
class ISuperCoolFeatures { | ||
public: | ||
[[nodiscard]] virtual std::string coolFeature() const = 0; // Pure virtual function | ||
virtual void set(std::string s) = 0; // Pure virtual function | ||
virtual ~ISuperCoolFeatures() = default; // Virtual destructor | ||
}; | ||
Comment on lines
+15
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) LGTM: Well-designed interface with proper use of C++ features. The Consider adding the virtual ~ISuperCoolFeatures() override = default; This change would ensure that if the interface is inherited from another base class in the future, the destructor will be correctly overridden. |
||
|
||
} // namespace classic | ||
|
||
#endif // POLYMORPHISM_I_SUPER_COOL_FEATURES_HPP |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
#ifndef POLYMORPHISM_IMPL_WITH_INTERFACE_HPP | ||
#define POLYMORPHISM_IMPL_WITH_INTERFACE_HPP | ||
|
||
// make the inteface defintion visible | ||
#include <polymorphism/i_super_cool_features.hpp> | ||
|
||
#include <string> | ||
|
||
namespace classic { | ||
|
||
class Impl | ||
: public ISuperCoolFeatures { | ||
private: | ||
std::string s_ { "<default value>" }; // with default member initializer (C++11) | ||
Comment on lines
+11
to
+14
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) LGTM: Class declaration and member initialization are well-implemented. The Consider adding a brief comment explaining the purpose of the private:
std::string s_ { "<default value>" }; // Stores the current feature value |
||
|
||
public: | ||
std::string coolFeature() const noexcept override { return s_; } | ||
void set(std::string s) noexcept override | ||
{ | ||
s_ = std::move(s); | ||
} | ||
Comment on lines
+17
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) LGTM: Public methods implementation. The Consider changing the void set(const std::string& s) noexcept override
{
s_ = s; // The move will be implicitly applied here
} This allows for more flexible use of the method with both lvalues and rvalues. |
||
}; | ||
Comment on lines
+16
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion LGTM: Public methods are correctly implemented. The Consider changing the void set(const std::string& s) noexcept override
{
s_ = s; // The move will be implicitly applied here for rvalues
} This allows for more flexible use of the method with both lvalues and rvalues, potentially improving performance in some cases. |
||
|
||
} // namespace classic | ||
|
||
#endif // POLYMORPHISM_IMPL_WITH_INTERFACE_HPP |
Uh oh!
There was an error while loading. Please reload this page.