Bei einer funktionsreichen Sprache wie C++ ist es besonders wichtig, Overengineering zu vermeiden. Overengineering entsteht, wenn Entwickler für ein Problem unnötig komplexe oder überdimensionierte Lösungen einführen.
C++-Entwickler können versucht sein, möglichst viele der mit neueren Standards eingeführten Funktionen zu verwenden. Dadurch kann der Code am Ende deutlich komplizierter werden, als es eigentlich nötig wäre.
Das folgende Beispiel zeigt, wie sich mit C++-Metaprogrammierung ein Type-Erasure-Container erstellen lässt, dessen arithmetische Operationen zur Compile-Zeit ausgewertet werden. Das Beispiel demonstriert zwar die Leistungsfähigkeit und Flexibilität der C++-Metaprogrammierung, wirkt durch den Einsatz von Templates, Concepts und constexpr-Funktionen jedoch recht komplex:
#include <iostream>
#include <type_traits>
template<typename T>
concept Arithmetic = std::is_arithmetic_v<T>;
template<Arithmetic T>
struct AnyType {
constexpr AnyType(const T& value) : value_(value) {}
template<Arithmetic U>
constexpr auto add(const AnyType<U>& other) const {
return AnyType{ value_ + other.value_ };
}
template<Arithmetic U>
constexpr auto subtract(const AnyType<U>& other) const {
return AnyType{ value_ - other.value_ };
}
template<Arithmetic U>
constexpr auto multiply(const AnyType<U>& other) const {
return AnyType{ value_ * other.value_ };
}
template<Arithmetic U>
constexpr auto divide(const AnyType<U>& other) const {
static_assert(other.value_ != 0, "Division by zero");
return AnyType{ value_ / other.value_ };
}
template<Arithmetic U>
friend std::ostream& operator<<(std::ostream& os, const AnyType<U>& any) {
return os << any.value_;
}
private:
T value_;
};
int main() {
constexpr AnyType<int> x{ 5 };
constexpr AnyType<float> y{ 2.5f };
constexpr auto addition = x.add(y);
constexpr auto subtraction = x.subtract(y);
constexpr auto multiplication = x.multiply(y);
constexpr auto division = x.divide(y);
std::cout << "Addition: " << addition << std::endl;
std::cout << "Subtraction: " << subtraction << std::endl;
std::cout << "Multiplication: " << multiplication << std::endl;
std::cout << "Division: " << division << std::endl;
return 0;
}
In this example:
- Wir definieren ein Concept
Arithmetic, um die Template-Parameter auf arithmetische Typen zu beschränken. - Das Klassen-Template
AnyTypewird so definiert, dass es beliebige arithmetische Typen aufnehmen kann. - We provide member functions (
add,subtract,multiply,divide) that perform arithmetic operations betweenAnyTypeobjects of potentially different types. - Wir verwenden
constexpr, damit diese Operationen zur Compile-Zeit ausgewertet werden. - Wir überladen
operator<<, damitAnyType-Objekte anstd::ostreamausgegeben werden können.
Ja, für bestimmte Anforderungen kann solcher Code sinnvoll sein. Aber brauchen wir im Allgemeinen wirklich eine derart ausgefeilte Klasse, um grundlegende arithmetische Operationen auszuführen? Das wäre, als würde man mit einem Panzer auf eine Fliege schießen :)
Zusammengefasst: Lassen Sie sich nicht dazu verleiten, ausgefeilte neue Funktionen nur um ihrer selbst willen einzusetzen. Verwenden Sie sie dann, wenn Sie sie tatsächlich benötigen. Und versuchen Sie stets, die KISS- und YAGNI-Prinzipien zu befolgen:
Keep It Simple, Stupid (KISS) und You Ain’t Gonna Need It (YAGNI) sind Prinzipien, die für Einfachheit stehen und empfehlen, unnötige Funktionen erst dann einzuführen, wenn sie tatsächlich benötigt werden.
